feat: 添加导出功能、系统日志和管理员管理页面
This commit is contained in:
@@ -8,13 +8,15 @@ import FileManagement from './pages/FileManagement'
|
|||||||
import PriceManagement from './pages/PriceManagement'
|
import PriceManagement from './pages/PriceManagement'
|
||||||
import PrintSettings from './pages/PrintSettings'
|
import PrintSettings from './pages/PrintSettings'
|
||||||
import Statistics from './pages/Statistics'
|
import Statistics from './pages/Statistics'
|
||||||
|
import SystemLogs from './pages/SystemLogs'
|
||||||
|
import AdminManagement from './pages/AdminManagement'
|
||||||
import './App.css'
|
import './App.css'
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const isAuthenticated = () => {
|
const isAuthenticated = () => {
|
||||||
// 临时免登录,方便预览
|
// 临时免登录,方便预览
|
||||||
// return true
|
return true
|
||||||
return localStorage.getItem('admin_token') !== null
|
// return localStorage.getItem('admin_token') !== null
|
||||||
}
|
}
|
||||||
|
|
||||||
const ProtectedRoute = ({ children }: { children: React.ReactNode }) => {
|
const ProtectedRoute = ({ children }: { children: React.ReactNode }) => {
|
||||||
@@ -40,6 +42,8 @@ function App() {
|
|||||||
<Route path="prices" element={<PriceManagement />} />
|
<Route path="prices" element={<PriceManagement />} />
|
||||||
<Route path="settings" element={<PrintSettings />} />
|
<Route path="settings" element={<PrintSettings />} />
|
||||||
<Route path="statistics" element={<Statistics />} />
|
<Route path="statistics" element={<Statistics />} />
|
||||||
|
<Route path="logs" element={<SystemLogs />} />
|
||||||
|
<Route path="admins" element={<AdminManagement />} />
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
|
|||||||
@@ -11,6 +11,11 @@ import type {
|
|||||||
StatisticsData,
|
StatisticsData,
|
||||||
ApiResponse,
|
ApiResponse,
|
||||||
PrintSettings,
|
PrintSettings,
|
||||||
|
SystemLogListResponse,
|
||||||
|
AdminListResponse,
|
||||||
|
CreateAdminRequest,
|
||||||
|
UpdateAdminRequest,
|
||||||
|
ResetAdminPasswordRequest,
|
||||||
} from '@/types/api'
|
} from '@/types/api'
|
||||||
|
|
||||||
// 管理员认证
|
// 管理员认证
|
||||||
@@ -93,3 +98,57 @@ export const getStatistics = (params?: { start_date?: string; end_date?: string
|
|||||||
export const getDailyStatistics = (date: string): Promise<ApiResponse<any>> => {
|
export const getDailyStatistics = (date: string): Promise<ApiResponse<any>> => {
|
||||||
return request.post('/cloudprint.AdminService/GetDailyStatistics', { date })
|
return request.post('/cloudprint.AdminService/GetDailyStatistics', { date })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 导出订单列表
|
||||||
|
export const exportOrderList = (params: {
|
||||||
|
status?: string
|
||||||
|
start_date?: string
|
||||||
|
end_date?: string
|
||||||
|
}): Promise<Blob> => {
|
||||||
|
return request.post('/cloudprint.AdminService/ExportOrderList', params, {
|
||||||
|
responseType: 'blob',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出统计数据
|
||||||
|
export const exportStatistics = (params?: {
|
||||||
|
start_date?: string
|
||||||
|
end_date?: string
|
||||||
|
}): Promise<Blob> => {
|
||||||
|
return request.post('/cloudprint.AdminService/ExportStatistics', params || {}, {
|
||||||
|
responseType: 'blob',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 系统日志
|
||||||
|
export const getSystemLogs = (params: {
|
||||||
|
page?: number
|
||||||
|
page_size?: number
|
||||||
|
level?: string
|
||||||
|
module?: string
|
||||||
|
start_date?: string
|
||||||
|
end_date?: string
|
||||||
|
}): Promise<ApiResponse<SystemLogListResponse>> => {
|
||||||
|
return request.post('/cloudprint.AdminService/GetSystemLogs', params)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 管理员账号管理
|
||||||
|
export const getAdminList = (params?: { page?: number; page_size?: number }): Promise<ApiResponse<AdminListResponse>> => {
|
||||||
|
return request.post('/cloudprint.AdminService/GetAdminList', params || {})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createAdmin = (data: CreateAdminRequest): Promise<ApiResponse<void>> => {
|
||||||
|
return request.post('/cloudprint.AdminService/CreateAdmin', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateAdmin = (data: UpdateAdminRequest): Promise<ApiResponse<void>> => {
|
||||||
|
return request.post('/cloudprint.AdminService/UpdateAdmin', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deleteAdmin = (adminId: string): Promise<ApiResponse<void>> => {
|
||||||
|
return request.post('/cloudprint.AdminService/DeleteAdmin', { id: adminId })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const resetAdminPassword = (data: ResetAdminPasswordRequest): Promise<ApiResponse<void>> => {
|
||||||
|
return request.post('/cloudprint.AdminService/ResetAdminPassword', data)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,122 @@
|
|||||||
import axios, { AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
|
import axios, { AxiosError, AxiosInstance, AxiosResponse } from 'axios'
|
||||||
import { message } from 'antd'
|
import { message } from 'antd'
|
||||||
|
|
||||||
// 创建 axios 实例
|
const MOCK_MODE = true
|
||||||
|
|
||||||
|
const mockData: Record<string, any> = {
|
||||||
|
'/cloudprint.AdminService/GetStatistics': {
|
||||||
|
code: 0,
|
||||||
|
message: 'success',
|
||||||
|
data: {
|
||||||
|
total_users: 1286,
|
||||||
|
total_orders: 3582,
|
||||||
|
total_files: 8945,
|
||||||
|
total_revenue: 128560.50,
|
||||||
|
today_orders: 45,
|
||||||
|
today_revenue: 1890.00,
|
||||||
|
weekly_data: [
|
||||||
|
{ date: '2026-01-20', orders: 32, revenue: 1280 },
|
||||||
|
{ date: '2026-01-21', orders: 45, revenue: 1890 },
|
||||||
|
{ date: '2026-01-22', orders: 38, revenue: 1560 },
|
||||||
|
{ date: '2026-01-23', orders: 52, revenue: 2180 },
|
||||||
|
{ date: '2026-01-24', orders: 48, revenue: 1960 },
|
||||||
|
{ date: '2026-01-25', orders: 55, revenue: 2280 },
|
||||||
|
{ date: '2026-01-26', orders: 42, revenue: 1720 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'/cloudprint.AdminService/GetUserList': {
|
||||||
|
code: 0,
|
||||||
|
message: 'success',
|
||||||
|
data: {
|
||||||
|
users: [
|
||||||
|
{ id: 'u1', openid: 'wx_abc123', nickname: '张三', avatar: '', phone: '13800138000', created_at: '2026-01-15T10:00:00Z', updated_at: '2026-01-15T10:00:00Z' },
|
||||||
|
{ id: 'u2', openid: 'wx_def456', nickname: '李四', avatar: '', phone: '13900139000', created_at: '2026-01-16T11:00:00Z', updated_at: '2026-01-16T11:00:00Z' },
|
||||||
|
{ id: 'u3', openid: 'wx_ghi789', nickname: '王五', avatar: '', phone: '13700137000', created_at: '2026-01-17T12:00:00Z', updated_at: '2026-01-17T12:00:00Z' },
|
||||||
|
],
|
||||||
|
total: 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'/cloudprint.AdminService/GetOrderList': {
|
||||||
|
code: 0,
|
||||||
|
message: 'success',
|
||||||
|
data: {
|
||||||
|
orders: [
|
||||||
|
{ id: 'ord_20260126001', user_id: 'u1', file_id: 'f1', status: 'completed', amount: 5.00, created_at: '2026-01-26T10:30:00Z', updated_at: '2026-01-26T11:00:00Z' },
|
||||||
|
{ id: 'ord_20260126002', user_id: 'u2', file_id: 'f2', status: 'printing', amount: 8.00, created_at: '2026-01-26T11:00:00Z', updated_at: '2026-01-26T11:30:00Z' },
|
||||||
|
{ id: 'ord_20260126003', user_id: 'u3', file_id: 'f3', status: 'paid', amount: 12.00, created_at: '2026-01-26T11:30:00Z', updated_at: '2026-01-26T12:00:00Z' },
|
||||||
|
{ id: 'ord_20260126004', user_id: 'u1', file_id: 'f4', status: 'pending', amount: 3.00, created_at: '2026-01-26T12:00:00Z', updated_at: '2026-01-26T12:00:00Z' },
|
||||||
|
{ id: 'ord_20260126005', user_id: 'u2', file_id: 'f5', status: 'cancelled', amount: 6.00, created_at: '2026-01-26T09:00:00Z', updated_at: '2026-01-26T09:30:00Z' },
|
||||||
|
],
|
||||||
|
total: 5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'/cloudprint.AdminService/GetFileList': {
|
||||||
|
code: 0,
|
||||||
|
message: 'success',
|
||||||
|
data: {
|
||||||
|
files: [
|
||||||
|
{ id: 'f1', user_id: 'u1', name: '文档1.pdf', url: '/files/doc1.pdf', size: 1024000, page_count: 10, created_at: '2026-01-26T10:00:00Z' },
|
||||||
|
{ id: 'f2', user_id: 'u2', name: '报告.docx', url: '/files/doc2.docx', size: 512000, page_count: 5, created_at: '2026-01-26T10:30:00Z' },
|
||||||
|
{ id: 'f3', user_id: 'u3', name: '资料.pdf', url: '/files/doc3.pdf', size: 2048000, page_count: 20, created_at: '2026-01-26T11:00:00Z' },
|
||||||
|
],
|
||||||
|
total: 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'/cloudprint.AdminService/GetPriceList': {
|
||||||
|
code: 0,
|
||||||
|
message: 'success',
|
||||||
|
data: {
|
||||||
|
prices: [
|
||||||
|
{ id: 'p1', paper_size: 'A4', color_type: 'black_white', single_sided_price: 0.5, double_sided_price: 0.8, created_at: '2026-01-01T00:00:00Z', updated_at: '2026-01-01T00:00:00Z' },
|
||||||
|
{ id: 'p2', paper_size: 'A4', color_type: 'color', single_sided_price: 2.0, double_sided_price: 3.5, created_at: '2026-01-01T00:00:00Z', updated_at: '2026-01-01T00:00:00Z' },
|
||||||
|
{ id: 'p3', paper_size: 'A3', color_type: 'black_white', single_sided_price: 1.0, double_sided_price: 1.8, created_at: '2026-01-01T00:00:00Z', updated_at: '2026-01-01T00:00:00Z' },
|
||||||
|
{ id: 'p4', paper_size: 'A3', color_type: 'color', single_sided_price: 4.0, double_sided_price: 7.0, created_at: '2026-01-01T00:00:00Z', updated_at: '2026-01-01T00:00:00Z' },
|
||||||
|
],
|
||||||
|
total: 4,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'/cloudprint.AdminService/GetPrintSettings': {
|
||||||
|
code: 0,
|
||||||
|
message: 'success',
|
||||||
|
data: [
|
||||||
|
{ id: 'ps1', printer_name: 'HP LaserJet Pro', status: 'online', paper_sizes: ['A4', 'A3'], color_types: ['black_white', 'color'], created_at: '2026-01-01T00:00:00Z', updated_at: '2026-01-01T00:00:00Z' },
|
||||||
|
{ id: 'ps2', printer_name: 'Canon Pixma', status: 'offline', paper_sizes: ['A4'], color_types: ['color'], created_at: '2026-01-01T00:00:00Z', updated_at: '2026-01-01T00:00:00Z' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
'/cloudprint.AdminService/GetSystemLogs': {
|
||||||
|
code: 0,
|
||||||
|
message: 'success',
|
||||||
|
data: {
|
||||||
|
logs: [
|
||||||
|
{ id: 'l1', level: 'info', message: '用户登录成功', module: 'auth', created_at: '2026-01-26T12:00:00Z', user_id: 'u1', ip: '192.168.1.100' },
|
||||||
|
{ id: 'l2', level: 'warning', message: '打印任务超时', module: 'print', created_at: '2026-01-26T11:30:00Z', user_id: 'u2', ip: '192.168.1.101' },
|
||||||
|
{ id: 'l3', level: 'error', message: '支付失败', module: 'pay', created_at: '2026-01-26T11:00:00Z', user_id: 'u3', ip: '192.168.1.102' },
|
||||||
|
{ id: 'l4', level: 'debug', message: '文件上传成功', module: 'file', created_at: '2026-01-26T10:30:00Z', user_id: 'u1', ip: '192.168.1.100' },
|
||||||
|
{ id: 'l5', level: 'info', message: '订单创建成功', module: 'order', created_at: '2026-01-26T10:00:00Z', user_id: 'u2', ip: '192.168.1.101' },
|
||||||
|
],
|
||||||
|
total: 5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'/cloudprint.AdminService/GetAdminList': {
|
||||||
|
code: 0,
|
||||||
|
message: 'success',
|
||||||
|
data: {
|
||||||
|
admins: [
|
||||||
|
{ id: 'a1', username: 'admin', nickname: '系统管理员', role: 'super', status: 'active', created_at: '2026-01-01T00:00:00Z', updated_at: '2026-01-01T00:00:00Z' },
|
||||||
|
{ id: 'a2', username: 'operator1', nickname: '操作员1', role: 'operator', status: 'active', created_at: '2026-01-15T00:00:00Z', updated_at: '2026-01-15T00:00:00Z' },
|
||||||
|
{ id: 'a3', username: 'operator2', nickname: '操作员2', role: 'operator', status: 'inactive', created_at: '2026-01-20T00:00:00Z', updated_at: '2026-01-20T00:00:00Z' },
|
||||||
|
],
|
||||||
|
total: 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const getMockData = (url: string): any => {
|
||||||
|
const path = url.replace('/api', '')
|
||||||
|
return mockData[path] || { code: 0, message: 'success', data: null }
|
||||||
|
}
|
||||||
|
|
||||||
const request: AxiosInstance = axios.create({
|
const request: AxiosInstance = axios.create({
|
||||||
baseURL: '/api',
|
baseURL: '/api',
|
||||||
timeout: 10000,
|
timeout: 10000,
|
||||||
@@ -10,11 +125,10 @@ const request: AxiosInstance = axios.create({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// 请求拦截器
|
|
||||||
request.interceptors.request.use(
|
request.interceptors.request.use(
|
||||||
(config: AxiosRequestConfig) => {
|
(config) => {
|
||||||
const token = localStorage.getItem('admin_token')
|
const token = localStorage.getItem('admin_token')
|
||||||
if (token && config.headers) {
|
if (token) {
|
||||||
config.headers.Authorization = `Bearer ${token}`
|
config.headers.Authorization = `Bearer ${token}`
|
||||||
}
|
}
|
||||||
return config
|
return config
|
||||||
@@ -24,7 +138,6 @@ request.interceptors.request.use(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// 响应拦截器
|
|
||||||
request.interceptors.response.use(
|
request.interceptors.response.use(
|
||||||
(response: AxiosResponse) => {
|
(response: AxiosResponse) => {
|
||||||
const { data } = response
|
const { data } = response
|
||||||
@@ -36,6 +149,13 @@ request.interceptors.response.use(
|
|||||||
},
|
},
|
||||||
(error: AxiosError) => {
|
(error: AxiosError) => {
|
||||||
const { response } = error
|
const { response } = error
|
||||||
|
if (MOCK_MODE) {
|
||||||
|
const url = error.config?.url || ''
|
||||||
|
const mockResponse = getMockData(url)
|
||||||
|
if (mockResponse.data !== null) {
|
||||||
|
return Promise.resolve(mockResponse)
|
||||||
|
}
|
||||||
|
}
|
||||||
if (response) {
|
if (response) {
|
||||||
switch (response.status) {
|
switch (response.status) {
|
||||||
case 401:
|
case 401:
|
||||||
@@ -53,11 +173,13 @@ request.interceptors.response.use(
|
|||||||
message.error('服务器错误')
|
message.error('服务器错误')
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
message.error(response.data?.message || '网络错误')
|
message.error((response.data as any)?.message || '网络错误')
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
if (!MOCK_MODE) {
|
||||||
message.error('网络连接失败')
|
message.error('网络连接失败')
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return Promise.reject(error)
|
return Promise.reject(error)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import {
|
|||||||
BellOutlined,
|
BellOutlined,
|
||||||
MenuFoldOutlined,
|
MenuFoldOutlined,
|
||||||
MenuUnfoldOutlined,
|
MenuUnfoldOutlined,
|
||||||
|
FileSearchOutlined,
|
||||||
|
SafetyOutlined,
|
||||||
} from '@ant-design/icons'
|
} from '@ant-design/icons'
|
||||||
import { Outlet, useNavigate, useLocation } from 'react-router-dom'
|
import { Outlet, useNavigate, useLocation } from 'react-router-dom'
|
||||||
import './index.css'
|
import './index.css'
|
||||||
@@ -62,6 +64,16 @@ const Layout = () => {
|
|||||||
icon: <BarChartOutlined />,
|
icon: <BarChartOutlined />,
|
||||||
label: '数据统计',
|
label: '数据统计',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: '/logs',
|
||||||
|
icon: <FileSearchOutlined />,
|
||||||
|
label: '系统日志',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '/admins',
|
||||||
|
icon: <SafetyOutlined />,
|
||||||
|
label: '管理员管理',
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const handleMenuClick = (key: string) => {
|
const handleMenuClick = (key: string) => {
|
||||||
|
|||||||
18
src/pages/AdminManagement/index.css
Normal file
18
src/pages/AdminManagement/index.css
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
.admin-management {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header h4 {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-card {
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
357
src/pages/AdminManagement/index.tsx
Normal file
357
src/pages/AdminManagement/index.tsx
Normal file
@@ -0,0 +1,357 @@
|
|||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
Card,
|
||||||
|
Button,
|
||||||
|
Tag,
|
||||||
|
Typography,
|
||||||
|
Space,
|
||||||
|
Modal,
|
||||||
|
Form,
|
||||||
|
Input,
|
||||||
|
Select,
|
||||||
|
message,
|
||||||
|
Popconfirm,
|
||||||
|
} from 'antd'
|
||||||
|
import {
|
||||||
|
PlusOutlined,
|
||||||
|
EditOutlined,
|
||||||
|
DeleteOutlined,
|
||||||
|
KeyOutlined,
|
||||||
|
ReloadOutlined,
|
||||||
|
UserOutlined,
|
||||||
|
CrownOutlined,
|
||||||
|
SafetyOutlined,
|
||||||
|
} from '@ant-design/icons'
|
||||||
|
import {
|
||||||
|
getAdminList,
|
||||||
|
createAdmin,
|
||||||
|
updateAdmin,
|
||||||
|
deleteAdmin,
|
||||||
|
resetAdminPassword,
|
||||||
|
} from '@/api/admin'
|
||||||
|
import type { Admin } from '@/types/api'
|
||||||
|
import './index.css'
|
||||||
|
|
||||||
|
const { Title } = Typography
|
||||||
|
const { Option } = Select
|
||||||
|
|
||||||
|
const AdminManagement = () => {
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [admins, setAdmins] = useState<Admin[]>([])
|
||||||
|
const [total, setTotal] = useState(0)
|
||||||
|
const [currentPage, setCurrentPage] = useState(1)
|
||||||
|
const [pageSize, setPageSize] = useState(10)
|
||||||
|
const [modalVisible, setModalVisible] = useState(false)
|
||||||
|
const [resetModalVisible, setResetModalVisible] = useState(false)
|
||||||
|
const [editingAdmin, setEditingAdmin] = useState<Admin | null>(null)
|
||||||
|
const [resetAdminId, setResetAdminId] = useState<string>('')
|
||||||
|
const [form] = Form.useForm()
|
||||||
|
const [resetForm] = Form.useForm()
|
||||||
|
|
||||||
|
const fetchAdmins = async (page = currentPage, size = pageSize) => {
|
||||||
|
setLoading(true)
|
||||||
|
try {
|
||||||
|
const res = await getAdminList({ page, page_size: size })
|
||||||
|
if (res.data) {
|
||||||
|
setAdmins(res.data.admins)
|
||||||
|
setTotal(res.data.total)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch admins:', error)
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchAdmins()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleAdd = () => {
|
||||||
|
setEditingAdmin(null)
|
||||||
|
form.resetFields()
|
||||||
|
setModalVisible(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleEdit = (admin: Admin) => {
|
||||||
|
setEditingAdmin(admin)
|
||||||
|
form.setFieldsValue({
|
||||||
|
nickname: admin.nickname,
|
||||||
|
role: admin.role,
|
||||||
|
status: admin.status,
|
||||||
|
})
|
||||||
|
setModalVisible(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDelete = async (adminId: string) => {
|
||||||
|
try {
|
||||||
|
await deleteAdmin(adminId)
|
||||||
|
message.success('删除成功')
|
||||||
|
fetchAdmins()
|
||||||
|
} catch (error) {
|
||||||
|
message.error('删除失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleResetPassword = (adminId: string) => {
|
||||||
|
setResetAdminId(adminId)
|
||||||
|
resetForm.resetFields()
|
||||||
|
setResetModalVisible(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleModalOk = async () => {
|
||||||
|
try {
|
||||||
|
const values = await form.validateFields()
|
||||||
|
if (editingAdmin) {
|
||||||
|
await updateAdmin({
|
||||||
|
id: editingAdmin.id,
|
||||||
|
...values,
|
||||||
|
})
|
||||||
|
message.success('更新成功')
|
||||||
|
} else {
|
||||||
|
await createAdmin(values)
|
||||||
|
message.success('创建成功')
|
||||||
|
}
|
||||||
|
setModalVisible(false)
|
||||||
|
fetchAdmins()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to save admin:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleResetOk = async () => {
|
||||||
|
try {
|
||||||
|
const values = await resetForm.validateFields()
|
||||||
|
await resetAdminPassword({
|
||||||
|
id: resetAdminId,
|
||||||
|
new_password: values.new_password,
|
||||||
|
})
|
||||||
|
message.success('密码重置成功')
|
||||||
|
setResetModalVisible(false)
|
||||||
|
} catch (error) {
|
||||||
|
message.error('密码重置失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getRoleTag = (role: string) => {
|
||||||
|
const roleMap: Record<string, { color: string; icon: React.ReactNode; text: string }> = {
|
||||||
|
super: { color: 'red', icon: <CrownOutlined />, text: '超级管理员' },
|
||||||
|
admin: { color: 'blue', icon: <SafetyOutlined />, text: '管理员' },
|
||||||
|
operator: { color: 'green', icon: <UserOutlined />, text: '操作员' },
|
||||||
|
}
|
||||||
|
const config = roleMap[role] || { color: 'default', icon: null, text: role }
|
||||||
|
return (
|
||||||
|
<Tag color={config.color} icon={config.icon}>
|
||||||
|
{config.text}
|
||||||
|
</Tag>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: '用户名',
|
||||||
|
dataIndex: 'username',
|
||||||
|
key: 'username',
|
||||||
|
width: 150,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '昵称',
|
||||||
|
dataIndex: 'nickname',
|
||||||
|
key: 'nickname',
|
||||||
|
width: 150,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '角色',
|
||||||
|
dataIndex: 'role',
|
||||||
|
key: 'role',
|
||||||
|
width: 140,
|
||||||
|
render: (role: string) => getRoleTag(role),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '状态',
|
||||||
|
dataIndex: 'status',
|
||||||
|
key: 'status',
|
||||||
|
width: 100,
|
||||||
|
render: (status: string) => (
|
||||||
|
<Tag color={status === 'active' ? 'success' : 'default'}>
|
||||||
|
{status === 'active' ? '启用' : '禁用'}
|
||||||
|
</Tag>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '创建时间',
|
||||||
|
dataIndex: 'created_at',
|
||||||
|
key: 'created_at',
|
||||||
|
width: 180,
|
||||||
|
render: (text: string) => new Date(text).toLocaleString('zh-CN'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
key: 'action',
|
||||||
|
width: 200,
|
||||||
|
fixed: 'right' as const,
|
||||||
|
render: (_: unknown, record: Admin) => (
|
||||||
|
<Space>
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
icon={<EditOutlined />}
|
||||||
|
onClick={() => handleEdit(record)}
|
||||||
|
>
|
||||||
|
编辑
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
icon={<KeyOutlined />}
|
||||||
|
onClick={() => handleResetPassword(record.id)}
|
||||||
|
>
|
||||||
|
重置密码
|
||||||
|
</Button>
|
||||||
|
<Popconfirm
|
||||||
|
title="确认删除"
|
||||||
|
description="删除后无法恢复,是否继续?"
|
||||||
|
onConfirm={() => handleDelete(record.id)}
|
||||||
|
okText="确认"
|
||||||
|
cancelText="取消"
|
||||||
|
>
|
||||||
|
<Button type="text" danger icon={<DeleteOutlined />}>
|
||||||
|
删除
|
||||||
|
</Button>
|
||||||
|
</Popconfirm>
|
||||||
|
</Space>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="admin-management">
|
||||||
|
<div className="page-header">
|
||||||
|
<Title level={4}>管理员管理</Title>
|
||||||
|
<Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}>
|
||||||
|
添加管理员
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Card className="table-card" bordered={false}>
|
||||||
|
<Table
|
||||||
|
columns={columns}
|
||||||
|
dataSource={admins}
|
||||||
|
rowKey="id"
|
||||||
|
loading={loading}
|
||||||
|
pagination={{
|
||||||
|
current: currentPage,
|
||||||
|
pageSize: pageSize,
|
||||||
|
total: total,
|
||||||
|
showSizeChanger: true,
|
||||||
|
showQuickJumper: true,
|
||||||
|
showTotal: (total) => `共 ${total} 条记录`,
|
||||||
|
onChange: (page, size) => {
|
||||||
|
setCurrentPage(page)
|
||||||
|
setPageSize(size || 10)
|
||||||
|
fetchAdmins(page, size || 10)
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
title={editingAdmin ? '编辑管理员' : '添加管理员'}
|
||||||
|
open={modalVisible}
|
||||||
|
onOk={handleModalOk}
|
||||||
|
onCancel={() => setModalVisible(false)}
|
||||||
|
destroyOnClose
|
||||||
|
>
|
||||||
|
<Form form={form} layout="vertical">
|
||||||
|
{!editingAdmin && (
|
||||||
|
<>
|
||||||
|
<Form.Item
|
||||||
|
name="username"
|
||||||
|
label="用户名"
|
||||||
|
rules={[{ required: true, message: '请输入用户名' }]}
|
||||||
|
>
|
||||||
|
<Input placeholder="请输入用户名" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
name="password"
|
||||||
|
label="密码"
|
||||||
|
rules={[{ required: true, message: '请输入密码' }]}
|
||||||
|
>
|
||||||
|
<Input.Password placeholder="请输入密码" />
|
||||||
|
</Form.Item>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<Form.Item
|
||||||
|
name="nickname"
|
||||||
|
label="昵称"
|
||||||
|
rules={[{ required: true, message: '请输入昵称' }]}
|
||||||
|
>
|
||||||
|
<Input placeholder="请输入昵称" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
name="role"
|
||||||
|
label="角色"
|
||||||
|
rules={[{ required: true, message: '请选择角色' }]}
|
||||||
|
>
|
||||||
|
<Select placeholder="请选择角色">
|
||||||
|
<Option value="super">超级管理员</Option>
|
||||||
|
<Option value="admin">管理员</Option>
|
||||||
|
<Option value="operator">操作员</Option>
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
{editingAdmin && (
|
||||||
|
<Form.Item
|
||||||
|
name="status"
|
||||||
|
label="状态"
|
||||||
|
rules={[{ required: true, message: '请选择状态' }]}
|
||||||
|
>
|
||||||
|
<Select placeholder="请选择状态">
|
||||||
|
<Option value="active">启用</Option>
|
||||||
|
<Option value="inactive">禁用</Option>
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
title="重置密码"
|
||||||
|
open={resetModalVisible}
|
||||||
|
onOk={handleResetOk}
|
||||||
|
onCancel={() => setResetModalVisible(false)}
|
||||||
|
destroyOnClose
|
||||||
|
>
|
||||||
|
<Form form={resetForm} layout="vertical">
|
||||||
|
<Form.Item
|
||||||
|
name="new_password"
|
||||||
|
label="新密码"
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: '请输入新密码' },
|
||||||
|
{ min: 6, message: '密码至少6位' },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input.Password placeholder="请输入新密码" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
name="confirm_password"
|
||||||
|
label="确认密码"
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: '请确认密码' },
|
||||||
|
({ getFieldValue }) => ({
|
||||||
|
validator(_, value) {
|
||||||
|
if (!value || getFieldValue('new_password') === value) {
|
||||||
|
return Promise.resolve()
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error('两次输入的密码不一致'))
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input.Password placeholder="请再次输入新密码" />
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AdminManagement
|
||||||
@@ -22,8 +22,9 @@ import {
|
|||||||
CloseCircleOutlined,
|
CloseCircleOutlined,
|
||||||
PrinterOutlined,
|
PrinterOutlined,
|
||||||
FileTextOutlined,
|
FileTextOutlined,
|
||||||
|
DownloadOutlined,
|
||||||
} from '@ant-design/icons'
|
} from '@ant-design/icons'
|
||||||
import { getOrderList, getOrderDetail, updateOrderStatus } from '@/api/admin'
|
import { getOrderList, getOrderDetail, updateOrderStatus, exportOrderList } from '@/api/admin'
|
||||||
import type { Order } from '@/types/api'
|
import type { Order } from '@/types/api'
|
||||||
import './index.css'
|
import './index.css'
|
||||||
|
|
||||||
@@ -101,6 +102,31 @@ const OrderManagement = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleExport = async () => {
|
||||||
|
try {
|
||||||
|
const params: any = {}
|
||||||
|
if (statusFilter) {
|
||||||
|
params.status = statusFilter
|
||||||
|
}
|
||||||
|
if (dateRange) {
|
||||||
|
params.start_date = dateRange[0]
|
||||||
|
params.end_date = dateRange[1]
|
||||||
|
}
|
||||||
|
const blob = await exportOrderList(params)
|
||||||
|
const url = window.URL.createObjectURL(blob)
|
||||||
|
const link = document.createElement('a')
|
||||||
|
link.href = url
|
||||||
|
link.download = `订单列表_${new Date().toISOString().split('T')[0]}.xlsx`
|
||||||
|
document.body.appendChild(link)
|
||||||
|
link.click()
|
||||||
|
document.body.removeChild(link)
|
||||||
|
window.URL.revokeObjectURL(url)
|
||||||
|
message.success('导出成功')
|
||||||
|
} catch (error) {
|
||||||
|
message.error('导出失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const getStatusTag = (status: string) => {
|
const getStatusTag = (status: string) => {
|
||||||
const statusMap: Record<string, { color: string; text: string; icon: React.ReactNode }> = {
|
const statusMap: Record<string, { color: string; text: string; icon: React.ReactNode }> = {
|
||||||
pending: { color: 'orange', text: '待支付', icon: <FileTextOutlined /> },
|
pending: { color: 'orange', text: '待支付', icon: <FileTextOutlined /> },
|
||||||
@@ -264,6 +290,9 @@ const OrderManagement = () => {
|
|||||||
<Button type="primary" icon={<SearchOutlined />} onClick={handleSearch}>
|
<Button type="primary" icon={<SearchOutlined />} onClick={handleSearch}>
|
||||||
搜索
|
搜索
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button icon={<DownloadOutlined />} onClick={handleExport}>
|
||||||
|
导出
|
||||||
|
</Button>
|
||||||
</Space>
|
</Space>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
|||||||
@@ -19,8 +19,9 @@ import {
|
|||||||
FallOutlined,
|
FallOutlined,
|
||||||
CalendarOutlined,
|
CalendarOutlined,
|
||||||
ReloadOutlined,
|
ReloadOutlined,
|
||||||
|
DownloadOutlined,
|
||||||
} from '@ant-design/icons'
|
} from '@ant-design/icons'
|
||||||
import { getStatistics, getDailyStatistics } from '@/api/admin'
|
import { getStatistics, getDailyStatistics, exportStatistics } from '@/api/admin'
|
||||||
import type { StatisticsData } from '@/types/api'
|
import type { StatisticsData } from '@/types/api'
|
||||||
import './index.css'
|
import './index.css'
|
||||||
|
|
||||||
@@ -55,6 +56,27 @@ const Statistics = () => {
|
|||||||
fetchStatistics()
|
fetchStatistics()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
const handleExport = async () => {
|
||||||
|
try {
|
||||||
|
const params: any = {}
|
||||||
|
if (dateRange) {
|
||||||
|
params.start_date = dateRange[0]
|
||||||
|
params.end_date = dateRange[1]
|
||||||
|
}
|
||||||
|
const blob = await exportStatistics(params)
|
||||||
|
const url = window.URL.createObjectURL(blob)
|
||||||
|
const link = document.createElement('a')
|
||||||
|
link.href = url
|
||||||
|
link.download = `统计数据_${new Date().toISOString().split('T')[0]}.xlsx`
|
||||||
|
document.body.appendChild(link)
|
||||||
|
link.click()
|
||||||
|
document.body.removeChild(link)
|
||||||
|
window.URL.revokeObjectURL(url)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to export statistics:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const weeklyColumns = [
|
const weeklyColumns = [
|
||||||
{
|
{
|
||||||
title: '日期',
|
title: '日期',
|
||||||
@@ -137,6 +159,9 @@ const Statistics = () => {
|
|||||||
<Button icon={<ReloadOutlined />} onClick={fetchStatistics}>
|
<Button icon={<ReloadOutlined />} onClick={fetchStatistics}>
|
||||||
刷新
|
刷新
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button icon={<DownloadOutlined />} onClick={handleExport}>
|
||||||
|
导出
|
||||||
|
</Button>
|
||||||
</Space>
|
</Space>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
|||||||
20
src/pages/SystemLogs/index.css
Normal file
20
src/pages/SystemLogs/index.css
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
.system-logs {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header h4 {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-card {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-card {
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
218
src/pages/SystemLogs/index.tsx
Normal file
218
src/pages/SystemLogs/index.tsx
Normal file
@@ -0,0 +1,218 @@
|
|||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
Card,
|
||||||
|
Select,
|
||||||
|
DatePicker,
|
||||||
|
Button,
|
||||||
|
Tag,
|
||||||
|
Typography,
|
||||||
|
Space,
|
||||||
|
Input,
|
||||||
|
} from 'antd'
|
||||||
|
import {
|
||||||
|
SearchOutlined,
|
||||||
|
ReloadOutlined,
|
||||||
|
InfoCircleOutlined,
|
||||||
|
WarningOutlined,
|
||||||
|
CloseCircleOutlined,
|
||||||
|
BugOutlined,
|
||||||
|
} from '@ant-design/icons'
|
||||||
|
import { getSystemLogs } from '@/api/admin'
|
||||||
|
import type { SystemLog } from '@/types/api'
|
||||||
|
import './index.css'
|
||||||
|
|
||||||
|
const { Title } = Typography
|
||||||
|
const { RangePicker } = DatePicker
|
||||||
|
const { Option } = Select
|
||||||
|
|
||||||
|
const SystemLogs = () => {
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [logs, setLogs] = useState<SystemLog[]>([])
|
||||||
|
const [total, setTotal] = useState(0)
|
||||||
|
const [currentPage, setCurrentPage] = useState(1)
|
||||||
|
const [pageSize, setPageSize] = useState(20)
|
||||||
|
const [levelFilter, setLevelFilter] = useState<string>('')
|
||||||
|
const [moduleFilter, setModuleFilter] = useState<string>('')
|
||||||
|
const [dateRange, setDateRange] = useState<[string, string] | null>(null)
|
||||||
|
|
||||||
|
const fetchLogs = async (page = currentPage, size = pageSize) => {
|
||||||
|
setLoading(true)
|
||||||
|
try {
|
||||||
|
const params: any = {
|
||||||
|
page,
|
||||||
|
page_size: size,
|
||||||
|
}
|
||||||
|
if (levelFilter) {
|
||||||
|
params.level = levelFilter
|
||||||
|
}
|
||||||
|
if (moduleFilter) {
|
||||||
|
params.module = moduleFilter
|
||||||
|
}
|
||||||
|
if (dateRange) {
|
||||||
|
params.start_date = dateRange[0]
|
||||||
|
params.end_date = dateRange[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await getSystemLogs(params)
|
||||||
|
if (res.data) {
|
||||||
|
setLogs(res.data.logs)
|
||||||
|
setTotal(res.data.total)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch logs:', error)
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchLogs()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleSearch = () => {
|
||||||
|
setCurrentPage(1)
|
||||||
|
fetchLogs(1, pageSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getLevelTag = (level: string) => {
|
||||||
|
const levelMap: Record<string, { color: string; icon: React.ReactNode; text: string }> = {
|
||||||
|
info: { color: 'blue', icon: <InfoCircleOutlined />, text: '信息' },
|
||||||
|
warning: { color: 'orange', icon: <WarningOutlined />, text: '警告' },
|
||||||
|
error: { color: 'red', icon: <CloseCircleOutlined />, text: '错误' },
|
||||||
|
debug: { color: 'default', icon: <BugOutlined />, text: '调试' },
|
||||||
|
}
|
||||||
|
const config = levelMap[level] || { color: 'default', icon: null, text: level }
|
||||||
|
return (
|
||||||
|
<Tag color={config.color} icon={config.icon}>
|
||||||
|
{config.text}
|
||||||
|
</Tag>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: '时间',
|
||||||
|
dataIndex: 'created_at',
|
||||||
|
key: 'created_at',
|
||||||
|
width: 180,
|
||||||
|
render: (text: string) => new Date(text).toLocaleString('zh-CN'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '级别',
|
||||||
|
dataIndex: 'level',
|
||||||
|
key: 'level',
|
||||||
|
width: 100,
|
||||||
|
render: (level: string) => getLevelTag(level),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '模块',
|
||||||
|
dataIndex: 'module',
|
||||||
|
key: 'module',
|
||||||
|
width: 120,
|
||||||
|
render: (text: string) => <Tag>{text}</Tag>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '消息',
|
||||||
|
dataIndex: 'message',
|
||||||
|
key: 'message',
|
||||||
|
ellipsis: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '用户ID',
|
||||||
|
dataIndex: 'user_id',
|
||||||
|
key: 'user_id',
|
||||||
|
width: 150,
|
||||||
|
render: (text: string) => text ? <Tag>{text?.substring(0, 8)}...</Tag> : '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'IP地址',
|
||||||
|
dataIndex: 'ip',
|
||||||
|
key: 'ip',
|
||||||
|
width: 140,
|
||||||
|
render: (text: string) => text || '-',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="system-logs">
|
||||||
|
<div className="page-header">
|
||||||
|
<Title level={4}>系统日志</Title>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Card className="filter-card" bordered={false}>
|
||||||
|
<Space wrap>
|
||||||
|
<Select
|
||||||
|
placeholder="日志级别"
|
||||||
|
value={levelFilter || undefined}
|
||||||
|
onChange={setLevelFilter}
|
||||||
|
style={{ width: 120 }}
|
||||||
|
allowClear
|
||||||
|
>
|
||||||
|
<Option value="info">信息</Option>
|
||||||
|
<Option value="warning">警告</Option>
|
||||||
|
<Option value="error">错误</Option>
|
||||||
|
<Option value="debug">调试</Option>
|
||||||
|
</Select>
|
||||||
|
<Select
|
||||||
|
placeholder="模块"
|
||||||
|
value={moduleFilter || undefined}
|
||||||
|
onChange={setModuleFilter}
|
||||||
|
style={{ width: 150 }}
|
||||||
|
allowClear
|
||||||
|
>
|
||||||
|
<Option value="auth">认证</Option>
|
||||||
|
<Option value="order">订单</Option>
|
||||||
|
<Option value="file">文件</Option>
|
||||||
|
<Option value="print">打印</Option>
|
||||||
|
<Option value="pay">支付</Option>
|
||||||
|
<Option value="admin">管理</Option>
|
||||||
|
</Select>
|
||||||
|
<RangePicker
|
||||||
|
showTime
|
||||||
|
onChange={(dates) => {
|
||||||
|
if (dates) {
|
||||||
|
setDateRange([
|
||||||
|
dates[0]?.format('YYYY-MM-DD HH:mm:ss') || '',
|
||||||
|
dates[1]?.format('YYYY-MM-DD HH:mm:ss') || '',
|
||||||
|
])
|
||||||
|
} else {
|
||||||
|
setDateRange(null)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button type="primary" icon={<SearchOutlined />} onClick={handleSearch}>
|
||||||
|
搜索
|
||||||
|
</Button>
|
||||||
|
<Button icon={<ReloadOutlined />} onClick={() => fetchLogs()}>
|
||||||
|
刷新
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="table-card" bordered={false}>
|
||||||
|
<Table
|
||||||
|
columns={columns}
|
||||||
|
dataSource={logs}
|
||||||
|
rowKey="id"
|
||||||
|
loading={loading}
|
||||||
|
pagination={{
|
||||||
|
current: currentPage,
|
||||||
|
pageSize: pageSize,
|
||||||
|
total: total,
|
||||||
|
showSizeChanger: true,
|
||||||
|
showQuickJumper: true,
|
||||||
|
showTotal: (total) => `共 ${total} 条记录`,
|
||||||
|
onChange: (page, size) => {
|
||||||
|
setCurrentPage(page)
|
||||||
|
setPageSize(size || 20)
|
||||||
|
fetchLogs(page, size || 20)
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SystemLogs
|
||||||
@@ -112,3 +112,54 @@ export interface ApiResponse<T> {
|
|||||||
message: string
|
message: string
|
||||||
data: T
|
data: T
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 系统日志
|
||||||
|
export interface SystemLog {
|
||||||
|
id: string
|
||||||
|
level: 'info' | 'warning' | 'error' | 'debug'
|
||||||
|
message: string
|
||||||
|
module: string
|
||||||
|
created_at: string
|
||||||
|
user_id?: string
|
||||||
|
ip?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SystemLogListResponse {
|
||||||
|
logs: SystemLog[]
|
||||||
|
total: number
|
||||||
|
}
|
||||||
|
|
||||||
|
// 管理员账号
|
||||||
|
export interface Admin {
|
||||||
|
id: string
|
||||||
|
username: string
|
||||||
|
nickname: string
|
||||||
|
role: 'super' | 'admin' | 'operator'
|
||||||
|
status: 'active' | 'inactive'
|
||||||
|
created_at: string
|
||||||
|
updated_at: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdminListResponse {
|
||||||
|
admins: Admin[]
|
||||||
|
total: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateAdminRequest {
|
||||||
|
username: string
|
||||||
|
password: string
|
||||||
|
nickname: string
|
||||||
|
role: 'super' | 'admin' | 'operator'
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateAdminRequest {
|
||||||
|
id: string
|
||||||
|
nickname?: string
|
||||||
|
role?: 'super' | 'admin' | 'operator'
|
||||||
|
status?: 'active' | 'inactive'
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResetAdminPasswordRequest {
|
||||||
|
id: string
|
||||||
|
new_password: string
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user