feat: rbac初步对接完成
This commit is contained in:
+1050
File diff suppressed because it is too large
Load Diff
@@ -85,6 +85,14 @@ function AppRoutes() {
|
||||
{hasFetchedMenus && dynamicRoutes.length === 0 && (
|
||||
<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 path="*" element={<Navigate to="/" replace />} />
|
||||
</Routes>
|
||||
|
||||
+55
-36
@@ -1,25 +1,42 @@
|
||||
import { get, post } from '@/lib/request'
|
||||
|
||||
|
||||
// ==================== Types ====================
|
||||
|
||||
export interface SystemUser {
|
||||
id: string; account: string; name: string; nickName?: string; phone?: string
|
||||
avatar?: SystemOss; avatarId?: string; clientId?: string; tenantId?: string
|
||||
createdAt: string; updatedAt: string; roles?: string[] // Adjusted based on backend info
|
||||
menus?: any[]
|
||||
gender?: number
|
||||
id: string
|
||||
name: string
|
||||
account: string
|
||||
nickName?: string
|
||||
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 {
|
||||
id: string; name: string; code: string; sort?: number
|
||||
menus?: SystemMenu[]; createdAt: string; updatedAt: string
|
||||
id: string
|
||||
name: string
|
||||
code: string
|
||||
sort?: number
|
||||
menuIds?: string[] // 关联菜单 ID 列表
|
||||
}
|
||||
|
||||
export interface SystemMenu {
|
||||
id: string; name: string; title?: string; code?: string; path?: string
|
||||
icon?: string; locale?: string; parentId?: string; permission?: string
|
||||
sort?: number; category?: number; children?: SystemMenu[]
|
||||
createdAt: string; updatedAt: string
|
||||
id: string
|
||||
parentId?: string
|
||||
category?: number // 1=菜单 2=按钮/权限
|
||||
name: string // 路由名(英文)
|
||||
title?: string // 显示标题(中文)
|
||||
code?: string // 权限标识
|
||||
path?: string // 路由路径
|
||||
permission?: string // 操作权限标识
|
||||
locale?: string // 国际化 key
|
||||
icon?: string // 图标名称
|
||||
sort?: number // 排序
|
||||
children?: SystemMenu[]
|
||||
}
|
||||
|
||||
export interface SystemOss {
|
||||
@@ -29,35 +46,37 @@ export interface SystemOss {
|
||||
}
|
||||
|
||||
export interface SystemClient {
|
||||
id: string; clientId: string; name: string; grantType?: string
|
||||
activeTimeout?: number; additionalInfo?: string
|
||||
createdAt: string; updatedAt: string
|
||||
id: string
|
||||
clientId: string
|
||||
name: string
|
||||
grantType?: string
|
||||
additionalInfo?: string
|
||||
activeTimeout?: number // Token有效期(秒)
|
||||
}
|
||||
|
||||
export interface OperationLog {
|
||||
id: string; operatorId: string; operatorName: string
|
||||
clientId: string; clientName: string
|
||||
method: string; path: string; title: string
|
||||
statusCode: number; duration: number // ms
|
||||
ip: string; userAgent?: string
|
||||
requestBody?: string; responseBody?: string
|
||||
createdAt: string
|
||||
id: string
|
||||
clientId: string
|
||||
ip: string
|
||||
method: string
|
||||
path: string
|
||||
status: number
|
||||
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 LoginParams { account: string; password: string; captcha: string; captchaId: string }
|
||||
export interface LoginResponse { token: string; userInfo: SystemUser }
|
||||
|
||||
// ==================== Auth ====================
|
||||
|
||||
export async function getCaptcha() {
|
||||
return get<{ code: number; data: CaptchaRes; msg: string }>('/auth/captcha')
|
||||
export interface DictInfo {
|
||||
id: string
|
||||
type: string
|
||||
label: string
|
||||
value: string
|
||||
sort: number
|
||||
desc: string
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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 })
|
||||
}
|
||||
@@ -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 })
|
||||
}
|
||||
@@ -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
@@ -1,25 +1,31 @@
|
||||
import { get } from '@/lib/request'
|
||||
import { USE_MOCK, delay, paginate, mockResponse } from '@/mock'
|
||||
import { post } from '@/lib/request'
|
||||
import { USE_MOCK, delay, paginate } from '@/mock'
|
||||
import { mockLogs } from '@/mock/system/logs'
|
||||
import type { OperationLog } from '../system'
|
||||
import type { PageResult } from '@/lib/request'
|
||||
|
||||
const BASE_URL = '/sys/log'
|
||||
|
||||
export async function getLogList(params: {
|
||||
current: number;
|
||||
pageSize: number;
|
||||
operatorName?: string;
|
||||
clientId?: string;
|
||||
method?: string;
|
||||
path?: string;
|
||||
status?: number;
|
||||
}) {
|
||||
if (USE_MOCK) {
|
||||
await delay()
|
||||
let list = [...mockLogs]
|
||||
if (params.operatorName) list = list.filter(l => l.operatorName?.includes(params.operatorName!))
|
||||
if (params.clientId && params.clientId !== 'all') list = list.filter(l => l.clientId === params.clientId)
|
||||
if (params.method) list = list.filter(l => l.method === params.method)
|
||||
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
@@ -1,24 +1,31 @@
|
||||
import { get, post, put, del } from '@/lib/request'
|
||||
import { USE_MOCK, delay, mockResponse } from '@/mock'
|
||||
import { get, post } from '@/lib/request'
|
||||
import { USE_MOCK, delay } from '@/mock'
|
||||
import { mockMenuTree } from '@/mock/system/menus'
|
||||
import type { SystemMenu } from '../system'
|
||||
|
||||
const BASE_URL = '/sys/menu'
|
||||
|
||||
export async function getMenuTree() {
|
||||
if (USE_MOCK) { await delay(); return mockResponse(mockMenuTree) }
|
||||
return get<{ code: number; data: SystemMenu[]; msg: string }>('/system/menus/tree')
|
||||
if (USE_MOCK) { await delay(); return mockMenuTree }
|
||||
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>) {
|
||||
if (USE_MOCK) { await delay(); return mockResponse(null, '创建成功') }
|
||||
return post<{ code: number; data: null; msg: string }>('/system/menus', data)
|
||||
if (USE_MOCK) { await delay(); return null }
|
||||
return post<null>(`${BASE_URL}/create`, data)
|
||||
}
|
||||
|
||||
export async function updateMenu(id: string, data: Partial<SystemMenu>) {
|
||||
if (USE_MOCK) { await delay(); return mockResponse(null, '更新成功') }
|
||||
return put<{ code: number; data: null; msg: string }>(`/system/menus/${id}`, data)
|
||||
export async function updateMenu(data: Partial<SystemMenu> & { id: string }) {
|
||||
if (USE_MOCK) { await delay(); return null }
|
||||
return post<null>(`${BASE_URL}/update`, data)
|
||||
}
|
||||
|
||||
export async function deleteMenu(id: string) {
|
||||
if (USE_MOCK) { await delay(); return mockResponse(null, '删除成功') }
|
||||
return del<{ code: number; data: null; msg: string }>(`/system/menus/${id}`)
|
||||
export async function deleteMenu(ids: string[]) {
|
||||
if (USE_MOCK) { await delay(); return null }
|
||||
return post<null>(`${BASE_URL}/delete`, { ids })
|
||||
}
|
||||
|
||||
+20
-17
@@ -1,13 +1,16 @@
|
||||
import { get, post, put, del } from '@/lib/request'
|
||||
import { USE_MOCK, delay, paginate, mockDate, mockResponse } from '@/mock'
|
||||
import { post } from '@/lib/request'
|
||||
import { USE_MOCK, delay, paginate } from '@/mock'
|
||||
import type { SystemRole } from '../system'
|
||||
import type { PageResult } from '@/lib/request'
|
||||
|
||||
const BASE_URL = '/sys/role'
|
||||
|
||||
const mockRoles: SystemRole[] = [
|
||||
{ id: '1', name: '超级管理员', code: 'super_admin', sort: 1, createdAt: mockDate(120), updatedAt: mockDate(1) },
|
||||
{ id: '2', name: '系统管理员', code: 'admin', sort: 2, createdAt: mockDate(120), updatedAt: mockDate(1) },
|
||||
{ id: '3', name: '运营专员', code: 'operator', sort: 3, createdAt: mockDate(90), updatedAt: mockDate(1) },
|
||||
{ id: '4', name: '内容审核', code: 'auditor', sort: 4, createdAt: mockDate(90), updatedAt: mockDate(1) },
|
||||
{ id: '5', name: '客服', code: 'customer_service', sort: 5, createdAt: mockDate(30), updatedAt: mockDate(1) },
|
||||
{ id: '1', name: '超级管理员', code: 'super_admin', sort: 1 },
|
||||
{ id: '2', name: '系统管理员', code: 'admin', sort: 2 },
|
||||
{ id: '3', name: '运营专员', code: 'operator', sort: 3 },
|
||||
{ id: '4', name: '内容审核', code: 'auditor', sort: 4 },
|
||||
{ id: '5', name: '客服', code: 'customer_service', sort: 5 },
|
||||
]
|
||||
|
||||
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()
|
||||
let list = [...mockRoles]
|
||||
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>) {
|
||||
if (USE_MOCK) { await delay(); return mockResponse(null, '创建成功') }
|
||||
return post<{ code: number; data: null; msg: string }>('/system/roles', data)
|
||||
if (USE_MOCK) { await delay(); return null }
|
||||
return post<null>(`${BASE_URL}/create`, data)
|
||||
}
|
||||
|
||||
export async function updateRole(id: string, data: Partial<SystemRole>) {
|
||||
if (USE_MOCK) { await delay(); return mockResponse(null, '更新成功') }
|
||||
return put<{ code: number; data: null; msg: string }>(`/system/roles/${id}`, data)
|
||||
export async function updateRole(data: Partial<SystemRole> & { id: string }) {
|
||||
if (USE_MOCK) { await delay(); return null }
|
||||
return post<null>(`${BASE_URL}/update`, data)
|
||||
}
|
||||
|
||||
export async function deleteRole(id: string) {
|
||||
if (USE_MOCK) { await delay(); return mockResponse(null, '删除成功') }
|
||||
return del<{ code: number; data: null; msg: string }>(`/system/roles/${id}`)
|
||||
export async function deleteRole(ids: string[]) {
|
||||
if (USE_MOCK) { await delay(); return null }
|
||||
return post<null>(`${BASE_URL}/delete`, { ids })
|
||||
}
|
||||
|
||||
+21
-13
@@ -1,7 +1,10 @@
|
||||
import { get, post, put, del } from '@/lib/request'
|
||||
import { USE_MOCK, delay, paginate, mockResponse } from '@/mock'
|
||||
import { post } from '@/lib/request'
|
||||
import { USE_MOCK, delay, paginate } from '@/mock'
|
||||
import { mockUsers } from '@/mock/system/users'
|
||||
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 }) {
|
||||
if (USE_MOCK) {
|
||||
@@ -9,22 +12,27 @@ export async function getUserList(params: { current: number; pageSize: number; a
|
||||
let list = [...mockUsers]
|
||||
if (params.account) list = list.filter(u => u.account.includes(params.account!))
|
||||
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>) {
|
||||
if (USE_MOCK) { await delay(); return mockResponse(null, '创建成功') }
|
||||
return post<{ code: number; data: null; msg: string }>('/system/users', data)
|
||||
export async function createUser(data: Partial<SystemUser> & { password?: string }) {
|
||||
if (USE_MOCK) { await delay(); return null }
|
||||
return post<null>(`${BASE_URL}/create`, data)
|
||||
}
|
||||
|
||||
export async function updateUser(id: string, data: Partial<SystemUser>) {
|
||||
if (USE_MOCK) { await delay(); return mockResponse(null, '更新成功') }
|
||||
return put<{ code: number; data: null; msg: string }>(`/system/users/${id}`, data)
|
||||
export async function updateUser(data: Partial<SystemUser> & { id: string }) {
|
||||
if (USE_MOCK) { await delay(); return null }
|
||||
return post<null>(`${BASE_URL}/update`, data)
|
||||
}
|
||||
|
||||
export async function deleteUser(id: string) {
|
||||
if (USE_MOCK) { await delay(); return mockResponse(null, '删除成功') }
|
||||
return del<{ code: number; data: null; msg: string }>(`/system/users/${id}`)
|
||||
export async function deleteUser(ids: string[]) {
|
||||
if (USE_MOCK) { await delay(); return null }
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -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,4 +1,4 @@
|
||||
import React from 'react'
|
||||
|
||||
import { motion } from 'framer-motion'
|
||||
|
||||
export default function AIBanner() {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Command } from 'cmdk'
|
||||
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 { useAuthStore } from '@/store/auth'
|
||||
import './cmdk.css' // We'll add some styles for cmdk
|
||||
@@ -10,7 +10,7 @@ export default function CommandPalette() {
|
||||
const { cmdKOpen, setCmdKOpen, setThemeHue } = useAppStore()
|
||||
const menus = useAuthStore(s => s.menus)
|
||||
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(() => {
|
||||
const down = (e: KeyboardEvent) => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
interface Particle {
|
||||
x: number;
|
||||
|
||||
@@ -32,7 +32,7 @@ export default function TabBar() {
|
||||
// Auto-register tab on route change
|
||||
useEffect(() => {
|
||||
const path = location.pathname
|
||||
if (path === '/login') return
|
||||
if (path === '/login' || path === '/') return
|
||||
const title = resolveTitle(menus || [], path)
|
||||
|| path.split('/').filter(Boolean).pop()?.replace(/^\w/, c => c.toUpperCase()) || 'Page'
|
||||
addTab({ path, title, closable: path !== '/dashboard' })
|
||||
|
||||
@@ -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 }
|
||||
+12
-14
@@ -1,5 +1,5 @@
|
||||
import { NavLink, Outlet, useNavigate, useLocation } from 'react-router-dom'
|
||||
import { AnimatePresence, motion } from 'framer-motion'
|
||||
import { NavLink, useNavigate, useLocation, useOutlet } from 'react-router-dom'
|
||||
import { motion } from 'framer-motion'
|
||||
import CommandPalette from '@/components/CommandPalette'
|
||||
import {
|
||||
LayoutDashboard, Users, Shield, MessageSquare, FolderTree, Leaf,
|
||||
@@ -210,6 +210,7 @@ function LayoutShell({ sidebarOpen, mobileMenuOpen, toggleSidebar, setMobileMenu
|
||||
}) {
|
||||
const { setCmdKOpen } = useAppStore()
|
||||
const location = useLocation()
|
||||
const outlet = useOutlet()
|
||||
const spotlightRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -272,7 +273,7 @@ function LayoutShell({ sidebarOpen, mobileMenuOpen, toggleSidebar, setMobileMenu
|
||||
<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")}>
|
||||
<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>
|
||||
</Avatar>
|
||||
{sidebarOpen && (
|
||||
@@ -345,17 +346,14 @@ function LayoutShell({ sidebarOpen, mobileMenuOpen, toggleSidebar, setMobileMenu
|
||||
<ScrollArea className="flex-1">
|
||||
<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">
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key={location.pathname}
|
||||
initial={{ opacity: 0, y: 15 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -15 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<Outlet />
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
<motion.div
|
||||
key={location.pathname}
|
||||
initial={{ opacity: 0, y: 15 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
{outlet}
|
||||
</motion.div>
|
||||
</div>
|
||||
</main>
|
||||
</ScrollArea>
|
||||
|
||||
+7
-2
@@ -28,6 +28,10 @@ request.interceptors.request.use(
|
||||
|
||||
request.interceptors.response.use(
|
||||
response => {
|
||||
const newToken = response.headers['x-refresh-token']
|
||||
if (newToken) {
|
||||
localStorage.setItem('token', newToken) // 静默替换
|
||||
}
|
||||
const res = response.data
|
||||
if (res.code !== undefined && res.code !== 200) {
|
||||
if (res.code === 401) {
|
||||
@@ -37,7 +41,8 @@ request.interceptors.response.use(
|
||||
}
|
||||
return Promise.reject(new Error(res.msg || '请求失败'))
|
||||
}
|
||||
return res
|
||||
// 统一返回 data 字段,调用方直接拿到业务数据
|
||||
return res.data
|
||||
},
|
||||
(error: AxiosError) => {
|
||||
if (error.response?.status === 401) {
|
||||
@@ -68,5 +73,5 @@ export function del<T = unknown>(url: string, config?: AxiosRequestConfig): Prom
|
||||
export default request
|
||||
|
||||
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 }
|
||||
|
||||
+1
-3
@@ -4,7 +4,5 @@ import './index.css'
|
||||
import App from './App'
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
<App />
|
||||
)
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { mockDate } from '../index'
|
||||
import type { SystemClient } from '@/api/system'
|
||||
|
||||
export const mockClients: SystemClient[] = [
|
||||
{ id: '1', clientId: 'plant', name: 'Plant 花园服务', grantType: 'password,refresh_token', activeTimeout: 7200, createdAt: mockDate(90), updatedAt: mockDate(1) },
|
||||
{ id: '2', clientId: 'radio', name: 'Radio 电台服务', grantType: 'password,refresh_token', activeTimeout: 7200, createdAt: mockDate(90), updatedAt: mockDate(1) },
|
||||
{ id: '3', clientId: 'admin', name: '管理后台', grantType: 'password', activeTimeout: 3600, createdAt: mockDate(120), 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 },
|
||||
{ id: '3', clientId: 'admin', name: '管理后台', grantType: 'password', activeTimeout: 3600 },
|
||||
]
|
||||
|
||||
+16
-64
@@ -1,70 +1,22 @@
|
||||
import { mockDate, mockId } from '../index'
|
||||
import type { OperationLog } from '@/api/system'
|
||||
|
||||
const methods = ['GET', 'POST', 'PUT', 'DELETE']
|
||||
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)] }
|
||||
const clients = ['sundynix-admin', 'plant', 'radio', 'system']
|
||||
|
||||
export const mockLogs: OperationLog[] = Array.from({ length: 120 }, (_, i) => {
|
||||
const api = rand(apis)
|
||||
const client = rand(clients)
|
||||
const op = rand(operators)
|
||||
const method = ['GET', 'POST', 'PUT', 'DELETE'][Math.floor(Math.random() * 4)]
|
||||
return {
|
||||
id: mockId(),
|
||||
operatorId: op.id,
|
||||
operatorName: op.name,
|
||||
clientId: client.id,
|
||||
clientName: client.name,
|
||||
method: api.method,
|
||||
path: api.path,
|
||||
title: api.title,
|
||||
statusCode: api.status,
|
||||
duration: Math.floor(Math.random() * 500) + 5,
|
||||
ip: rand(ips),
|
||||
userAgent: rand(uas),
|
||||
requestBody: api.method === 'GET' ? undefined : '{"page":1}',
|
||||
responseBody: api.status === 200 ? '{"code":0,"msg":"ok"}' : '{"code":1,"msg":"error"}',
|
||||
createdAt: mockDate(i * 0.3),
|
||||
id: `log-${i + 1}`,
|
||||
userId: 'admin',
|
||||
clientId: clients[Math.floor(Math.random() * clients.length)],
|
||||
ip: `192.168.1.${Math.floor(Math.random() * 255)}`,
|
||||
method,
|
||||
path: `/api/v1/resource/${Math.floor(Math.random() * 10)}`,
|
||||
status: Math.random() > 0.1 ? 200 : 500,
|
||||
latency: Math.floor(Math.random() * 500) * 1000000,
|
||||
agent: 'Mozilla/5.0',
|
||||
errorMessage: '',
|
||||
body: method === 'GET' ? '' : '{"key": "value"}',
|
||||
resp: '{"code": 200}',
|
||||
createdAt: Date.now() / 1000 - Math.floor(Math.random() * 86400 * 30),
|
||||
}
|
||||
}).sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
|
||||
}).sort((a, b) => b.createdAt - a.createdAt)
|
||||
|
||||
+31
-34
@@ -1,68 +1,65 @@
|
||||
import { mockDate } from '../index'
|
||||
import type { SystemMenu } from '@/api/system'
|
||||
|
||||
const d = (days: number) => mockDate(days)
|
||||
|
||||
export const mockMenuTree: SystemMenu[] = [
|
||||
{
|
||||
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',
|
||||
sort: 1, category: 1, createdAt: d(90), updatedAt: d(1),
|
||||
sort: 1, category: 1,
|
||||
children: [
|
||||
{ id: '11', name: 'users', title: '用户管理', path: '/system/users', icon: 'users', parentId: '10', sort: 0, category: 1, createdAt: d(90), updatedAt: d(1) },
|
||||
{ id: '12', name: 'roles', title: '角色管理', path: '/system/roles', icon: 'shield', parentId: '10', sort: 1, category: 1, createdAt: d(90), updatedAt: d(1) },
|
||||
{ id: '13', name: 'menus', title: '菜单管理', path: '/system/menus', icon: 'menu', parentId: '10', sort: 2, category: 1, createdAt: d(90), updatedAt: d(1) },
|
||||
{ id: '14', name: 'clients', title: '客户端管理', path: '/system/clients', icon: 'monitor', parentId: '10', sort: 3, category: 1, createdAt: d(90), updatedAt: d(1) },
|
||||
{ id: '15', name: 'files', title: '文件管理', path: '/system/files', icon: 'folder', parentId: '10', sort: 4, category: 1, createdAt: d(90), updatedAt: d(1) },
|
||||
{ id: '16', name: 'logs', title: '操作日志', path: '/system/logs', icon: 'scroll', parentId: '10', sort: 5, 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 },
|
||||
{ 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 },
|
||||
{ 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 },
|
||||
{ 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',
|
||||
sort: 2, category: 1, createdAt: d(90), updatedAt: d(1),
|
||||
id: '20', name: 'plant', title: 'Plant 服务', icon: 'leaf', sort: 2, category: 1,
|
||||
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: [
|
||||
{ id: '201', name: 'badge', title: '徽章配置', path: '/plant/achievement/badge', icon: 'award', parentId: '200', sort: 0, 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, 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 },
|
||||
],
|
||||
},
|
||||
{
|
||||
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: [
|
||||
{ id: '211', name: 'topic', title: '话题管理', path: '/plant/community/topic', icon: 'message', parentId: '210', sort: 0, 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, 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 },
|
||||
]
|
||||
},
|
||||
{
|
||||
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: [
|
||||
{ id: '221', name: 'wikiClass', title: '分类管理', path: '/plant/wiki/class', icon: 'tree', parentId: '220', sort: 0, 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, 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 },
|
||||
]
|
||||
},
|
||||
{
|
||||
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: [
|
||||
{ id: '231', name: 'exchangeOrder', title: '兑换订单', path: '/plant/exchange/order', icon: 'list', parentId: '230', sort: 0, 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, 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 },
|
||||
]
|
||||
},
|
||||
{ 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',
|
||||
sort: 3, category: 1, createdAt: d(90), updatedAt: d(1),
|
||||
sort: 3, category: 1,
|
||||
children: [
|
||||
{ id: '31', name: 'channel', title: '频道管理', path: '/radio/channel', icon: 'radio', parentId: '30', sort: 0, category: 1, createdAt: d(90), updatedAt: d(1) },
|
||||
{ id: '32', name: 'program', title: '节目管理', path: '/radio/program', icon: 'music', parentId: '30', sort: 1, category: 1, createdAt: d(90), updatedAt: d(1) },
|
||||
{ id: '33', name: 'radioCategory', title: '频道分类', path: '/radio/category', icon: 'category', parentId: '30', sort: 2, category: 1, createdAt: d(90), updatedAt: d(1) },
|
||||
{ id: '34', name: 'subscription', title: '订阅管理', path: '/radio/subscription', icon: 'user', parentId: '30', sort: 3, 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 },
|
||||
{ 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 },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { mockDate } from '../index'
|
||||
|
||||
import type { SystemRole } from '@/api/system'
|
||||
|
||||
export const mockRoles: SystemRole[] = [
|
||||
{ id: '1', name: '超级管理员', code: 'super_admin', sort: 0, createdAt: mockDate(120), updatedAt: mockDate(1) },
|
||||
{ id: '2', name: '运营', code: 'operator', sort: 1, createdAt: mockDate(90), updatedAt: mockDate(1) },
|
||||
{ id: '3', name: '编辑', code: 'editor', sort: 2, createdAt: mockDate(90), updatedAt: mockDate(1) },
|
||||
{ id: '4', name: '访客', code: 'guest', sort: 3, createdAt: mockDate(60), updatedAt: mockDate(1) },
|
||||
{ id: '1', name: '超级管理员', code: 'super_admin', sort: 0 },
|
||||
{ id: '2', name: '运营', code: 'operator', sort: 1 },
|
||||
{ id: '3', name: '编辑', code: 'editor', sort: 2 },
|
||||
{ id: '4', name: '访客', code: 'guest', sort: 3 },
|
||||
]
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { mockDate } from '../index'
|
||||
|
||||
import type { SystemUser } from '@/api/system'
|
||||
|
||||
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: '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: '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: '4', account: 'wangwu', name: '王五', phone: '13600136000', clientId: 'plant', createdAt: mockDate(15), updatedAt: mockDate(3) },
|
||||
{ id: '5', account: 'zhaoliu', name: '赵六', phone: '13500135000', createdAt: mockDate(7), updatedAt: mockDate(1) },
|
||||
{ id: '1', account: 'admin', name: '系统管理员', phone: '13800138000', roles: ['super_admin'] },
|
||||
{ id: '2', account: 'zhangsan', name: '张三', phone: '13900139000', roles: ['operator'] },
|
||||
{ id: '3', account: 'lisi', name: '李四', phone: '13700137000', roles: ['editor'] },
|
||||
{ id: '4', account: 'wangwu', name: '王五', phone: '13600136000' },
|
||||
{ id: '5', account: 'zhaoliu', name: '赵六', phone: '13500135000' },
|
||||
]
|
||||
|
||||
@@ -2,8 +2,8 @@ import { useState, useEffect, useCallback } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import {
|
||||
Users, Leaf, Radio, Folder, Activity, BarChart3,
|
||||
RefreshCw, Wifi, WifiOff, Zap, Clock, HardDrive,
|
||||
Cpu, MemoryStick, ArrowUpRight, TrendingUp, Server,
|
||||
RefreshCw, Wifi, Zap, Clock,
|
||||
Cpu, ArrowUpRight, TrendingUp, Server,
|
||||
CheckCircle2, AlertTriangle, XCircle, Globe, Database,
|
||||
Gauge,
|
||||
} 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' },
|
||||
}
|
||||
|
||||
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> = {
|
||||
'svc-plant': <Leaf className="h-4 w-4" />,
|
||||
'svc-radio': <Radio className="h-4 w-4" />,
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
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 LoginCharacters from '@/components/LoginCharacters'
|
||||
import ParticleBackground from '@/components/ParticleBackground'
|
||||
@@ -32,8 +32,7 @@ export default function LoginPage() {
|
||||
|
||||
const fetchCaptcha = async () => {
|
||||
try {
|
||||
const res = await getCaptcha()
|
||||
const d = (res as any).data
|
||||
const d = await getCaptcha()
|
||||
setCaptchaId(d.captchaId)
|
||||
setCaptchaImg(d.captchaImg)
|
||||
} catch { /* ignore */ }
|
||||
@@ -46,8 +45,7 @@ export default function LoginPage() {
|
||||
if (!account || !password) { setError('请输入账号和密码'); return }
|
||||
setLoading(true); setError('')
|
||||
try {
|
||||
const res = await apiLogin({ account, password, captcha, captchaId })
|
||||
const d = (res as any).data
|
||||
const d = await apiLogin({ account, password, captcha, captchaId })
|
||||
loginStore(d.userInfo, d.token)
|
||||
navigate('/dashboard', { replace: true })
|
||||
} catch (err: any) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { useState, useEffect, useCallback, useRef } from 'react'
|
||||
import { Plus, Search, Pencil, Trash2, Loader2, MoreHorizontal, GalleryHorizontalEnd, Eye, Upload } from 'lucide-react'
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { Plus, Search, Pencil, Trash2, Loader2, MoreHorizontal, GalleryHorizontalEnd } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { getBannerList, saveBanner, updateBanner, deleteBanner } from '@/api/plant'
|
||||
import type { Banner } from '@/api/plant'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
|
||||
export default function BannerPage() {
|
||||
const [banners, setBanners] = useState<Banner[]>([])
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
@@ -16,9 +16,9 @@ export default function ExchangePage() {
|
||||
const [items, setItems] = useState<ExchangeItem[]>([])
|
||||
const [orders, setOrders] = useState<ExchangeOrder[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [total, setTotal] = useState(0)
|
||||
const [, setTotal] = useState(0)
|
||||
const [page, setPage] = useState(1)
|
||||
const [search, setSearch] = useState('')
|
||||
const [search] = useState('')
|
||||
const [tab, setTab] = useState('items')
|
||||
const [dialogOpen, setDialogOpen] = useState(false)
|
||||
const [form, setForm] = useState({ name: '', description: '', points: 100, stock: 10, sort: 0 })
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
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 { Input } from '@/components/ui/input'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
@@ -22,7 +21,7 @@ export default function SubscriptionPage() {
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [total, setTotal] = useState(0)
|
||||
const [page, setPage] = useState(1)
|
||||
const [search, setSearch] = useState('')
|
||||
const [search] = useState('')
|
||||
const [statusFilter, setStatusFilter] = useState('all')
|
||||
|
||||
const fetchList = useCallback(async () => {
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'
|
||||
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'
|
||||
|
||||
export default function ClientsPage() {
|
||||
@@ -26,7 +26,7 @@ export default function ClientsPage() {
|
||||
|
||||
const fetchList = useCallback(async () => {
|
||||
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) }
|
||||
}, [page, search])
|
||||
|
||||
@@ -37,7 +37,7 @@ export default function ClientsPage() {
|
||||
|
||||
const handleSubmit = async () => {
|
||||
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) }
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
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 { cn } from '@/lib/utils'
|
||||
|
||||
@@ -35,7 +35,7 @@ export default function FilesPage() {
|
||||
|
||||
const fetchList = useCallback(async () => {
|
||||
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) }
|
||||
}, [page, search])
|
||||
|
||||
|
||||
+31
-30
@@ -16,7 +16,7 @@ export default function Logs() {
|
||||
const [logs, setLogs] = useState<OperationLog[]>([])
|
||||
const [total, setTotal] = useState(0)
|
||||
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 })
|
||||
|
||||
// Dialog State for viewing details
|
||||
@@ -27,9 +27,9 @@ export default function Logs() {
|
||||
setLoading(true)
|
||||
try {
|
||||
const res = await getLogList({ ...pagination, ...search })
|
||||
if (res.data) {
|
||||
setLogs(res.data.list)
|
||||
setTotal(res.data.total)
|
||||
if (res) {
|
||||
setLogs(res.list)
|
||||
setTotal(res.total)
|
||||
}
|
||||
} finally {
|
||||
setLoading(false)
|
||||
@@ -44,7 +44,7 @@ export default function Logs() {
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
setSearch({ operatorName: '', clientId: 'all', path: '' })
|
||||
setSearch({ path: '', method: '', status: undefined })
|
||||
if (pagination.current === 1) setTimeout(() => fetchLogs(), 0)
|
||||
else setPagination({ ...pagination, current: 1 })
|
||||
}
|
||||
@@ -79,25 +79,27 @@ export default function Logs() {
|
||||
</CardTitle>
|
||||
|
||||
<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">
|
||||
<SelectValue placeholder="客户端来源" />
|
||||
<SelectValue placeholder="请求方法" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">所有客户端</SelectItem>
|
||||
<SelectItem value="gateway">API 网关</SelectItem>
|
||||
<SelectItem value="plant">Plant 服务</SelectItem>
|
||||
<SelectItem value="radio">Radio 服务</SelectItem>
|
||||
<SelectItem value="all">所有方法</SelectItem>
|
||||
<SelectItem value="GET">GET</SelectItem>
|
||||
<SelectItem value="POST">POST</SelectItem>
|
||||
<SelectItem value="PUT">PUT</SelectItem>
|
||||
<SelectItem value="DELETE">DELETE</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<div className="relative">
|
||||
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="操作者账号..."
|
||||
placeholder="状态码 (如 200, 400)..."
|
||||
type="number"
|
||||
className="pl-9 w-40 h-9"
|
||||
value={search.operatorName}
|
||||
onChange={e => setSearch({ ...search, operatorName: e.target.value })}
|
||||
value={search.status || ''}
|
||||
onChange={e => setSearch({ ...search, status: e.target.value ? Number(e.target.value) : undefined })}
|
||||
onKeyDown={e => e.key === 'Enter' && handleSearch()}
|
||||
/>
|
||||
</div>
|
||||
@@ -145,10 +147,10 @@ export default function Logs() {
|
||||
) : (
|
||||
logs.map(log => (
|
||||
<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>
|
||||
<Badge variant="outline" className="bg-primary/5 font-normal">
|
||||
{log.clientName || log.clientId || 'System'}
|
||||
{log.clientId || 'System'}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
@@ -158,29 +160,28 @@ export default function Logs() {
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<span className="font-medium text-sm">{log.title}</span>
|
||||
<span className="text-xs text-muted-foreground font-mono">{log.path}</span>
|
||||
<span className="font-medium text-sm text-muted-foreground font-mono">{log.path}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{log.statusCode === 200 ? (
|
||||
{log.status === 200 ? (
|
||||
<div className="flex items-center text-emerald-600 text-sm">
|
||||
<Activity className="w-4 h-4 mr-1" /> 200 OK
|
||||
</div>
|
||||
) : (
|
||||
<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>
|
||||
)}
|
||||
</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" />
|
||||
{log.duration}ms
|
||||
{(log.latency / 1000000).toFixed(0)}ms
|
||||
</div>
|
||||
</TableCell>
|
||||
<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 className="text-right pr-6">
|
||||
<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="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-medium">{activeLog.operatorName}</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">操作者ID:</span> <span className="font-medium">{activeLog.userId}</span></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 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>
|
||||
|
||||
{activeLog.requestBody && (
|
||||
{activeLog.body && (
|
||||
<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>
|
||||
<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">
|
||||
{JSON.stringify(JSON.parse(activeLog.requestBody), null, 2)}
|
||||
{JSON.stringify(JSON.parse(activeLog.body), null, 2)}
|
||||
</pre>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeLog.responseBody && (
|
||||
{activeLog.resp && (
|
||||
<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>
|
||||
<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">
|
||||
{JSON.stringify(JSON.parse(activeLog.responseBody), null, 2)}
|
||||
{JSON.stringify(JSON.parse(activeLog.resp), null, 2)}
|
||||
</pre>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
|
||||
+91
-19
@@ -1,13 +1,14 @@
|
||||
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 { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
|
||||
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 { 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 type { SystemMenu } from '@/api/system'
|
||||
@@ -21,7 +22,11 @@ function MenuRow({
|
||||
}) {
|
||||
const [expanded, setExpanded] = useState(true)
|
||||
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 (
|
||||
<>
|
||||
@@ -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() {
|
||||
const [menus, setMenus] = useState<SystemMenu[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
@@ -89,13 +127,13 @@ export default function Menus() {
|
||||
// Dialog State
|
||||
const [dialogOpen, setDialogOpen] = useState(false)
|
||||
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 () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const res = await getMenuTree()
|
||||
if (res.data) setMenus(res.data)
|
||||
if (res) setMenus(res)
|
||||
} finally { setLoading(false) }
|
||||
}
|
||||
|
||||
@@ -116,18 +154,23 @@ export default function Menus() {
|
||||
|
||||
const openCreateDialog = (parentId: string = '') => {
|
||||
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)
|
||||
}
|
||||
|
||||
const openEditDialog = (menu: SystemMenu) => {
|
||||
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)
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
if (editingMenu) await updateMenu(editingMenu.id, formData)
|
||||
if (editingMenu) await updateMenu({ id: editingMenu.id, ...formData })
|
||||
else await createMenu(formData)
|
||||
setDialogOpen(false)
|
||||
fetchMenus()
|
||||
@@ -135,7 +178,7 @@ export default function Menus() {
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
if (confirm('确定要删除这个菜单节点吗?其子节点也会受到影响。')) {
|
||||
await deleteMenu(id)
|
||||
await deleteMenu([id])
|
||||
fetchMenus()
|
||||
}
|
||||
}
|
||||
@@ -191,11 +234,24 @@ export default function Menus() {
|
||||
</Card>
|
||||
|
||||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<DialogContent className="sm:max-w-[500px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{editingMenu ? '编辑菜单' : '新增菜单'}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<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">
|
||||
<Label className="text-right">上级菜单</Label>
|
||||
<div className="col-span-3">
|
||||
@@ -213,21 +269,37 @@ export default function Menus() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="title" className="text-right">菜单名称</Label>
|
||||
<Input id="title" value={formData.title} onChange={e => setFormData({ ...formData, title: e.target.value })} className="col-span-3" />
|
||||
<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" placeholder={formData.category === 1 ? '如: 用户管理' : '如: 新增用户'} />
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<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 className="grid grid-cols-4 items-center gap-4">
|
||||
<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" />
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<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 图标名" />
|
||||
<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">
|
||||
<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" />
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="icon" className="text-right">图标</Label>
|
||||
<div className="col-span-3">
|
||||
<IconPicker value={formData.icon} onChange={v => setFormData({ ...formData, icon: v })} />
|
||||
</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">
|
||||
<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" />
|
||||
|
||||
+13
-10
@@ -34,9 +34,9 @@ export default function Roles() {
|
||||
setLoading(true)
|
||||
try {
|
||||
const res = await getRoleList({ current: 1, pageSize: 10, ...search })
|
||||
if (res.data) {
|
||||
setRoles(res.data.list)
|
||||
setTotal(res.data.total)
|
||||
if (res) {
|
||||
setRoles(res.list)
|
||||
setTotal(res.total)
|
||||
}
|
||||
} finally {
|
||||
setLoading(false)
|
||||
@@ -45,7 +45,7 @@ export default function Roles() {
|
||||
|
||||
const fetchMenus = async () => {
|
||||
const res = await getMenuTree()
|
||||
if (res.data) setAllMenus(res.data)
|
||||
if (res) setAllMenus(res)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
@@ -69,7 +69,7 @@ export default function Roles() {
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
if (editingRole) await updateRole(editingRole.id, formData)
|
||||
if (editingRole) await updateRole({ id: editingRole.id, ...formData })
|
||||
else await createRole(formData)
|
||||
setDialogOpen(false)
|
||||
fetchRoles()
|
||||
@@ -77,20 +77,23 @@ export default function Roles() {
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
if (confirm('确定要删除这个角色吗?')) {
|
||||
await deleteRole(id)
|
||||
await deleteRole([id])
|
||||
fetchRoles()
|
||||
}
|
||||
}
|
||||
|
||||
const openPermDialog = (role: SystemRole) => {
|
||||
setActiveRole(role)
|
||||
// mock some selected ids
|
||||
setSelectedMenuIds(['1', '10', '11', '12'])
|
||||
// Actually get the role's menus
|
||||
setSelectedMenuIds(role.menuIds || [])
|
||||
setPermDialogOpen(true)
|
||||
}
|
||||
|
||||
const handleSavePerms = async () => {
|
||||
// In real app, call assignMenusToRole API
|
||||
if (activeRole) {
|
||||
await updateRole({ id: activeRole.id, menuIds: selectedMenuIds })
|
||||
fetchRoles()
|
||||
}
|
||||
setPermDialogOpen(false)
|
||||
}
|
||||
|
||||
@@ -182,7 +185,7 @@ export default function Roles() {
|
||||
<TableCell className="font-mono text-xs">{role.code}</TableCell>
|
||||
<TableCell>{role.sort}</TableCell>
|
||||
<TableCell className="text-muted-foreground text-sm">
|
||||
{new Date(role.createdAt).toLocaleDateString('zh-CN')}
|
||||
-
|
||||
</TableCell>
|
||||
<TableCell className="text-right pr-6">
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
|
||||
+17
-24
@@ -32,9 +32,9 @@ export default function UserManage() {
|
||||
setLoading(true)
|
||||
try {
|
||||
const res = await getUserList({ current: 1, pageSize: 10, ...search })
|
||||
if (res.data) {
|
||||
setUsers(res.data.list)
|
||||
setTotal(res.data.total)
|
||||
if (res) {
|
||||
setUsers(res.list)
|
||||
setTotal(res.total)
|
||||
}
|
||||
} finally {
|
||||
setLoading(false)
|
||||
@@ -43,7 +43,7 @@ export default function UserManage() {
|
||||
|
||||
const fetchRoles = async () => {
|
||||
const res = await getRoleList({ current: 1, pageSize: 100 })
|
||||
if (res.data) setAllRoles(res.data.list)
|
||||
if (res) setAllRoles(res.list)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
@@ -67,17 +67,17 @@ export default function UserManage() {
|
||||
const openEditDialog = (user: SystemUser) => {
|
||||
setEditingUser(user)
|
||||
setFormData({
|
||||
account: user.account, name: user.name, phone: user.phone || '', clientId: user.clientId || '',
|
||||
roleIds: user.roles?.map(r => r.id) || []
|
||||
account: user.account, name: user.name, phone: user.phone || '', clientId: '',
|
||||
roleIds: user.roles || []
|
||||
})
|
||||
setDialogOpen(true)
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
if (editingUser) {
|
||||
await updateUser(editingUser.id, formData)
|
||||
await updateUser({ id: editingUser.id, ...formData })
|
||||
} else {
|
||||
await createUser(formData)
|
||||
await createUser({ ...formData, password: '123' }) // 默认密码
|
||||
}
|
||||
setDialogOpen(false)
|
||||
fetchUsers()
|
||||
@@ -85,7 +85,7 @@ export default function UserManage() {
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
if (confirm('确定要删除这个用户吗?')) {
|
||||
await deleteUser(id)
|
||||
await deleteUser([id])
|
||||
fetchUsers()
|
||||
}
|
||||
}
|
||||
@@ -147,7 +147,6 @@ export default function UserManage() {
|
||||
<TableHead className="w-[100px] pl-6">账号</TableHead>
|
||||
<TableHead>姓名</TableHead>
|
||||
<TableHead>手机号</TableHead>
|
||||
<TableHead>客户端来源</TableHead>
|
||||
<TableHead>角色</TableHead>
|
||||
<TableHead>创建时间</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>{user.name}</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>
|
||||
<div className="flex gap-1 flex-wrap">
|
||||
{user.roles?.map(r => (
|
||||
<Badge key={r.id} variant="default" className="text-[10px] h-5 font-normal bg-emerald-500 hover:bg-emerald-600 text-white">
|
||||
{r.name}
|
||||
{user.roles?.map(code => {
|
||||
const role = allRoles.find(r => r.code === code)
|
||||
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>
|
||||
)) || <span className="text-muted-foreground text-xs">无角色</span>}
|
||||
)
|
||||
}) || <span className="text-muted-foreground text-xs">无角色</span>}
|
||||
</div>
|
||||
</TableCell>
|
||||
<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 className="text-right pr-6">
|
||||
<DropdownMenu>
|
||||
|
||||
+3
-4
@@ -1,7 +1,6 @@
|
||||
import { create } from 'zustand'
|
||||
import type { SystemUser, SystemMenu } from '@/api/system'
|
||||
import { getUserMenuTree } from '@/api/systemCrud'
|
||||
import { logout as apiLogout } from '@/api/system'
|
||||
import { getUserInfo, logout as apiLogout } from '@/api/system/auth'
|
||||
|
||||
const TOKEN_KEY = 'token'
|
||||
const USER_KEY = 'user'
|
||||
@@ -66,8 +65,8 @@ export const useAuthStore = create<AuthState>((set, get) => ({
|
||||
refreshMenus: async () => {
|
||||
if (!get().isAuthenticated) return
|
||||
try {
|
||||
const res = await getUserMenuTree()
|
||||
const menus = (res.data as any).menus || []
|
||||
const userInfo = await getUserInfo()
|
||||
const menus = userInfo.menus || []
|
||||
set({ menus, permissions: extractPermissions(menus), hasFetchedMenus: true })
|
||||
} catch (e) {
|
||||
console.error('获取菜单失败:', e)
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"paths": { "@/*": ["./src/*"] },
|
||||
"types": ["vite/client"],
|
||||
"skipLibCheck": true,
|
||||
"ignoreDeprecations": "6.0",
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
|
||||
Reference in New Issue
Block a user