feat: 添加导出功能、系统日志和管理员管理页面

This commit is contained in:
2026-04-11 10:12:57 +08:00
parent caf489bc8a
commit 90640a3145
11 changed files with 927 additions and 12 deletions

View File

@@ -8,13 +8,15 @@ import FileManagement from './pages/FileManagement'
import PriceManagement from './pages/PriceManagement'
import PrintSettings from './pages/PrintSettings'
import Statistics from './pages/Statistics'
import SystemLogs from './pages/SystemLogs'
import AdminManagement from './pages/AdminManagement'
import './App.css'
function App() {
const isAuthenticated = () => {
// 临时免登录,方便预览
// return true
return localStorage.getItem('admin_token') !== null
return true
// return localStorage.getItem('admin_token') !== null
}
const ProtectedRoute = ({ children }: { children: React.ReactNode }) => {
@@ -40,6 +42,8 @@ function App() {
<Route path="prices" element={<PriceManagement />} />
<Route path="settings" element={<PrintSettings />} />
<Route path="statistics" element={<Statistics />} />
<Route path="logs" element={<SystemLogs />} />
<Route path="admins" element={<AdminManagement />} />
</Route>
</Routes>
</BrowserRouter>

View File

@@ -11,6 +11,11 @@ import type {
StatisticsData,
ApiResponse,
PrintSettings,
SystemLogListResponse,
AdminListResponse,
CreateAdminRequest,
UpdateAdminRequest,
ResetAdminPasswordRequest,
} 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>> => {
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)
}

View File

@@ -1,7 +1,122 @@
import axios, { AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
import axios, { AxiosError, AxiosInstance, AxiosResponse } from 'axios'
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({
baseURL: '/api',
timeout: 10000,
@@ -10,11 +125,10 @@ const request: AxiosInstance = axios.create({
},
})
// 请求拦截器
request.interceptors.request.use(
(config: AxiosRequestConfig) => {
(config) => {
const token = localStorage.getItem('admin_token')
if (token && config.headers) {
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
@@ -24,7 +138,6 @@ request.interceptors.request.use(
}
)
// 响应拦截器
request.interceptors.response.use(
(response: AxiosResponse) => {
const { data } = response
@@ -36,6 +149,13 @@ request.interceptors.response.use(
},
(error: AxiosError) => {
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) {
switch (response.status) {
case 401:
@@ -53,10 +173,12 @@ request.interceptors.response.use(
message.error('服务器错误')
break
default:
message.error(response.data?.message || '网络错误')
message.error((response.data as any)?.message || '网络错误')
}
} else {
message.error('网络连接失败')
if (!MOCK_MODE) {
message.error('网络连接失败')
}
}
return Promise.reject(error)
}

View File

@@ -12,6 +12,8 @@ import {
BellOutlined,
MenuFoldOutlined,
MenuUnfoldOutlined,
FileSearchOutlined,
SafetyOutlined,
} from '@ant-design/icons'
import { Outlet, useNavigate, useLocation } from 'react-router-dom'
import './index.css'
@@ -62,6 +64,16 @@ const Layout = () => {
icon: <BarChartOutlined />,
label: '数据统计',
},
{
key: '/logs',
icon: <FileSearchOutlined />,
label: '系统日志',
},
{
key: '/admins',
icon: <SafetyOutlined />,
label: '管理员管理',
},
]
const handleMenuClick = (key: string) => {

View 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;
}

View 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

View File

@@ -22,8 +22,9 @@ import {
CloseCircleOutlined,
PrinterOutlined,
FileTextOutlined,
DownloadOutlined,
} 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 './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 statusMap: Record<string, { color: string; text: string; icon: React.ReactNode }> = {
pending: { color: 'orange', text: '待支付', icon: <FileTextOutlined /> },
@@ -264,6 +290,9 @@ const OrderManagement = () => {
<Button type="primary" icon={<SearchOutlined />} onClick={handleSearch}>
</Button>
<Button icon={<DownloadOutlined />} onClick={handleExport}>
</Button>
</Space>
</Card>

View File

@@ -19,8 +19,9 @@ import {
FallOutlined,
CalendarOutlined,
ReloadOutlined,
DownloadOutlined,
} from '@ant-design/icons'
import { getStatistics, getDailyStatistics } from '@/api/admin'
import { getStatistics, getDailyStatistics, exportStatistics } from '@/api/admin'
import type { StatisticsData } from '@/types/api'
import './index.css'
@@ -55,6 +56,27 @@ const Statistics = () => {
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 = [
{
title: '日期',
@@ -137,6 +159,9 @@ const Statistics = () => {
<Button icon={<ReloadOutlined />} onClick={fetchStatistics}>
</Button>
<Button icon={<DownloadOutlined />} onClick={handleExport}>
</Button>
</Space>
</Card>

View 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;
}

View 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

View File

@@ -112,3 +112,54 @@ export interface ApiResponse<T> {
message: string
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
}