feat: 添加导出功能、系统日志和管理员管理页面
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
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,
|
||||
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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
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
|
||||
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