From 90640a31452ee3c0803de3c4cf8876a8af347da5 Mon Sep 17 00:00:00 2001 From: YONGYE Date: Sat, 11 Apr 2026 10:12:57 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=AF=BC=E5=87=BA?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=E3=80=81=E7=B3=BB=E7=BB=9F=E6=97=A5=E5=BF=97?= =?UTF-8?q?=E5=92=8C=E7=AE=A1=E7=90=86=E5=91=98=E7=AE=A1=E7=90=86=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.tsx | 8 +- src/api/admin.ts | 59 +++++ src/api/request.ts | 138 ++++++++++- src/components/Layout/index.tsx | 12 + src/pages/AdminManagement/index.css | 18 ++ src/pages/AdminManagement/index.tsx | 357 ++++++++++++++++++++++++++++ src/pages/OrderManagement/index.tsx | 31 ++- src/pages/Statistics/index.tsx | 27 ++- src/pages/SystemLogs/index.css | 20 ++ src/pages/SystemLogs/index.tsx | 218 +++++++++++++++++ src/types/api.ts | 51 ++++ 11 files changed, 927 insertions(+), 12 deletions(-) create mode 100644 src/pages/AdminManagement/index.css create mode 100644 src/pages/AdminManagement/index.tsx create mode 100644 src/pages/SystemLogs/index.css create mode 100644 src/pages/SystemLogs/index.tsx diff --git a/src/App.tsx b/src/App.tsx index 1d5c3419..0e263672 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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() { } /> } /> } /> + } /> + } /> diff --git a/src/api/admin.ts b/src/api/admin.ts index 631a3db6..8e47f2f2 100644 --- a/src/api/admin.ts +++ b/src/api/admin.ts @@ -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> => { return request.post('/cloudprint.AdminService/GetDailyStatistics', { date }) } + +// 导出订单列表 +export const exportOrderList = (params: { + status?: string + start_date?: string + end_date?: string +}): Promise => { + return request.post('/cloudprint.AdminService/ExportOrderList', params, { + responseType: 'blob', + }) +} + +// 导出统计数据 +export const exportStatistics = (params?: { + start_date?: string + end_date?: string +}): Promise => { + 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> => { + return request.post('/cloudprint.AdminService/GetSystemLogs', params) +} + +// 管理员账号管理 +export const getAdminList = (params?: { page?: number; page_size?: number }): Promise> => { + return request.post('/cloudprint.AdminService/GetAdminList', params || {}) +} + +export const createAdmin = (data: CreateAdminRequest): Promise> => { + return request.post('/cloudprint.AdminService/CreateAdmin', data) +} + +export const updateAdmin = (data: UpdateAdminRequest): Promise> => { + return request.post('/cloudprint.AdminService/UpdateAdmin', data) +} + +export const deleteAdmin = (adminId: string): Promise> => { + return request.post('/cloudprint.AdminService/DeleteAdmin', { id: adminId }) +} + +export const resetAdminPassword = (data: ResetAdminPasswordRequest): Promise> => { + return request.post('/cloudprint.AdminService/ResetAdminPassword', data) +} diff --git a/src/api/request.ts b/src/api/request.ts index 12b64a04..c2fb24cf 100644 --- a/src/api/request.ts +++ b/src/api/request.ts @@ -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 = { + '/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) } diff --git a/src/components/Layout/index.tsx b/src/components/Layout/index.tsx index 7eadb53f..cd2a814c 100644 --- a/src/components/Layout/index.tsx +++ b/src/components/Layout/index.tsx @@ -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: , label: '数据统计', }, + { + key: '/logs', + icon: , + label: '系统日志', + }, + { + key: '/admins', + icon: , + label: '管理员管理', + }, ] const handleMenuClick = (key: string) => { diff --git a/src/pages/AdminManagement/index.css b/src/pages/AdminManagement/index.css new file mode 100644 index 00000000..f6cfca87 --- /dev/null +++ b/src/pages/AdminManagement/index.css @@ -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; +} diff --git a/src/pages/AdminManagement/index.tsx b/src/pages/AdminManagement/index.tsx new file mode 100644 index 00000000..c1020167 --- /dev/null +++ b/src/pages/AdminManagement/index.tsx @@ -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([]) + 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(null) + const [resetAdminId, setResetAdminId] = useState('') + 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 = { + super: { color: 'red', icon: , text: '超级管理员' }, + admin: { color: 'blue', icon: , text: '管理员' }, + operator: { color: 'green', icon: , text: '操作员' }, + } + const config = roleMap[role] || { color: 'default', icon: null, text: role } + return ( + + {config.text} + + ) + } + + 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) => ( + + {status === 'active' ? '启用' : '禁用'} + + ), + }, + { + 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) => ( + + + + handleDelete(record.id)} + okText="确认" + cancelText="取消" + > + + + + ), + }, + ] + + return ( +
+
+ 管理员管理 + +
+ + + `共 ${total} 条记录`, + onChange: (page, size) => { + setCurrentPage(page) + setPageSize(size || 10) + fetchAdmins(page, size || 10) + }, + }} + /> + + + setModalVisible(false)} + destroyOnClose + > +
+ {!editingAdmin && ( + <> + + + + + + + + )} + + + + + + + {editingAdmin && ( + + + + )} + +
+ + setResetModalVisible(false)} + destroyOnClose + > +
+ + + + ({ + validator(_, value) { + if (!value || getFieldValue('new_password') === value) { + return Promise.resolve() + } + return Promise.reject(new Error('两次输入的密码不一致')) + }, + }), + ]} + > + + + +
+ + ) +} + +export default AdminManagement diff --git a/src/pages/OrderManagement/index.tsx b/src/pages/OrderManagement/index.tsx index a56eaa56..86955d7e 100644 --- a/src/pages/OrderManagement/index.tsx +++ b/src/pages/OrderManagement/index.tsx @@ -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 = { pending: { color: 'orange', text: '待支付', icon: }, @@ -264,6 +290,9 @@ const OrderManagement = () => { + diff --git a/src/pages/Statistics/index.tsx b/src/pages/Statistics/index.tsx index 4c8055ac..725b4748 100644 --- a/src/pages/Statistics/index.tsx +++ b/src/pages/Statistics/index.tsx @@ -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 = () => { + diff --git a/src/pages/SystemLogs/index.css b/src/pages/SystemLogs/index.css new file mode 100644 index 00000000..555bb872 --- /dev/null +++ b/src/pages/SystemLogs/index.css @@ -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; +} diff --git a/src/pages/SystemLogs/index.tsx b/src/pages/SystemLogs/index.tsx new file mode 100644 index 00000000..328f614e --- /dev/null +++ b/src/pages/SystemLogs/index.tsx @@ -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([]) + const [total, setTotal] = useState(0) + const [currentPage, setCurrentPage] = useState(1) + const [pageSize, setPageSize] = useState(20) + const [levelFilter, setLevelFilter] = useState('') + const [moduleFilter, setModuleFilter] = useState('') + 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 = { + info: { color: 'blue', icon: , text: '信息' }, + warning: { color: 'orange', icon: , text: '警告' }, + error: { color: 'red', icon: , text: '错误' }, + debug: { color: 'default', icon: , text: '调试' }, + } + const config = levelMap[level] || { color: 'default', icon: null, text: level } + return ( + + {config.text} + + ) + } + + 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) => {text}, + }, + { + title: '消息', + dataIndex: 'message', + key: 'message', + ellipsis: true, + }, + { + title: '用户ID', + dataIndex: 'user_id', + key: 'user_id', + width: 150, + render: (text: string) => text ? {text?.substring(0, 8)}... : '-', + }, + { + title: 'IP地址', + dataIndex: 'ip', + key: 'ip', + width: 140, + render: (text: string) => text || '-', + }, + ] + + return ( +
+
+ 系统日志 +
+ + + + + + { + if (dates) { + setDateRange([ + dates[0]?.format('YYYY-MM-DD HH:mm:ss') || '', + dates[1]?.format('YYYY-MM-DD HH:mm:ss') || '', + ]) + } else { + setDateRange(null) + } + }} + /> + + + + + + +
`共 ${total} 条记录`, + onChange: (page, size) => { + setCurrentPage(page) + setPageSize(size || 20) + fetchLogs(page, size || 20) + }, + }} + /> + + + ) +} + +export default SystemLogs diff --git a/src/types/api.ts b/src/types/api.ts index ed9532eb..a5d11ee0 100644 --- a/src/types/api.ts +++ b/src/types/api.ts @@ -112,3 +112,54 @@ export interface ApiResponse { 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 +}