由于本次代码变更内容为空,无法生成有效的提交信息。请提供具体的代码变更内容以便生成合适的提交信息。
This commit is contained in:
@@ -53,6 +53,18 @@ const mockPermissions = [
|
||||
{ id: 14, name: '系统写入', code: 'system:write', description: '创建/编辑系统信息', resource_type: 'system', created_at: '2024-01-01', updated_at: '2024-01-01' }
|
||||
]
|
||||
|
||||
// 模拟系统日志数据
|
||||
const mockSystemLogs = [
|
||||
{ id: '1', level: 'info', message: '用户登录成功 - admin', timestamp: '2024-03-15T14:30:22Z', module: 'auth', userId: '1', ip: '192.168.1.100' },
|
||||
{ id: '2', level: 'info', message: '数据库备份完成 - 备份文件: backup_20240315.sql', timestamp: '2024-03-15T14:00:00Z', module: 'database', userId: '1', ip: '192.168.1.100' },
|
||||
{ id: '3', level: 'warn', message: '系统警告 - 内存使用率超过80%', timestamp: '2024-03-15T13:45:18Z', module: 'system', userId: '1', ip: '192.168.1.100' },
|
||||
{ id: '4', level: 'info', message: '定时任务执行 - 清理过期日志', timestamp: '2024-03-15T13:30:00Z', module: 'task', userId: '1', ip: '192.168.1.100' },
|
||||
{ id: '5', level: 'error', message: '数据库连接失败 - 连接超时', timestamp: '2024-03-15T12:15:30Z', module: 'database', userId: '1', ip: '192.168.1.100' },
|
||||
{ id: '6', level: 'info', message: '用户注册成功 - user123', timestamp: '2024-03-15T11:20:45Z', module: 'auth', userId: '2', ip: '192.168.1.101' },
|
||||
{ id: '7', level: 'debug', message: 'API调用 - 获取用户列表', timestamp: '2024-03-15T10:30:15Z', module: 'api', userId: '1', ip: '192.168.1.100' },
|
||||
{ id: '8', level: 'info', message: '订单创建成功 - 订单号: ORD20240315001', timestamp: '2024-03-15T09:45:22Z', module: 'order', userId: '3', ip: '192.168.1.102' }
|
||||
]
|
||||
|
||||
// 模拟API响应格式
|
||||
const createSuccessResponse = (data: any) => ({
|
||||
success: true,
|
||||
@@ -175,7 +187,7 @@ export const mockOrderAPI = {
|
||||
}
|
||||
}
|
||||
|
||||
// 模拟系统统计API
|
||||
// 模拟系统API
|
||||
export const mockSystemAPI = {
|
||||
getSystemStats: async () => {
|
||||
await delay(600)
|
||||
@@ -190,6 +202,32 @@ export const mockSystemAPI = {
|
||||
})
|
||||
},
|
||||
|
||||
getSystemLogs: async (params: any = {}) => {
|
||||
await delay(800)
|
||||
const { page = 1, limit = 10, level, module, startDate, endDate } = params
|
||||
|
||||
// 根据查询参数过滤日志
|
||||
let filteredLogs = mockSystemLogs
|
||||
if (level) {
|
||||
filteredLogs = mockSystemLogs.filter(log => log.level === level)
|
||||
}
|
||||
if (module) {
|
||||
filteredLogs = filteredLogs.filter(log => log.module === module)
|
||||
}
|
||||
if (startDate) {
|
||||
filteredLogs = filteredLogs.filter(log => new Date(log.timestamp) >= new Date(startDate))
|
||||
}
|
||||
if (endDate) {
|
||||
filteredLogs = filteredLogs.filter(log => new Date(log.timestamp) <= new Date(endDate))
|
||||
}
|
||||
|
||||
const start = (page - 1) * limit
|
||||
const end = start + limit
|
||||
const paginatedData = filteredLogs.slice(start, end)
|
||||
|
||||
return createPaginatedResponse(paginatedData, page, limit, filteredLogs.length)
|
||||
},
|
||||
|
||||
getSystemMonitorData: async () => {
|
||||
await delay(500)
|
||||
return createSuccessResponse({
|
||||
|
||||
@@ -83,6 +83,14 @@
|
||||
<span>系统设置</span>
|
||||
<router-link to="/system" />
|
||||
</a-menu-item>
|
||||
|
||||
<a-menu-item v-if="hasPermission('system:read')" key="system-logs">
|
||||
<template #icon>
|
||||
<FileTextOutlined />
|
||||
</template>
|
||||
<span>系统日志</span>
|
||||
<router-link to="/system/logs" />
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</a-layout-sider>
|
||||
|
||||
@@ -179,7 +187,8 @@ import {
|
||||
MenuFoldOutlined,
|
||||
BellOutlined,
|
||||
QuestionCircleOutlined,
|
||||
LogoutOutlined
|
||||
LogoutOutlined,
|
||||
FileTextOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
@@ -52,6 +52,24 @@
|
||||
</a-form>
|
||||
</div>
|
||||
|
||||
<!-- 批量操作区域 -->
|
||||
<div class="batch-actions" style="margin-bottom: 16px;" v-if="hasPermission('system:write')">
|
||||
<a-space>
|
||||
<a-button
|
||||
type="primary"
|
||||
danger
|
||||
:disabled="selectedRowKeys.length === 0"
|
||||
@click="handleBatchDelete"
|
||||
>
|
||||
<template #icon>
|
||||
<DeleteOutlined />
|
||||
</template>
|
||||
批量删除
|
||||
</a-button>
|
||||
<span>已选择 {{ selectedRowKeys.length }} 项</span>
|
||||
</a-space>
|
||||
</div>
|
||||
|
||||
<!-- 权限表格 -->
|
||||
<a-table
|
||||
:columns="columns"
|
||||
@@ -60,6 +78,7 @@
|
||||
:pagination="pagination"
|
||||
:row-key="record => record.id"
|
||||
@change="handleTableChange"
|
||||
:row-selection="rowSelection"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'actions'">
|
||||
@@ -152,7 +171,8 @@ import {
|
||||
getPermissions,
|
||||
createPermission,
|
||||
updatePermission,
|
||||
deletePermission
|
||||
deletePermission,
|
||||
batchDeletePermissions
|
||||
} from '@/api/permission'
|
||||
import type { Permission } from '@/api/permission'
|
||||
|
||||
@@ -395,6 +415,44 @@ const handleDelete = (record: Permission) => {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 批量操作相关状态
|
||||
const selectedRowKeys = ref<number[]>([])
|
||||
const rowSelection = {
|
||||
selectedRowKeys: selectedRowKeys.value,
|
||||
onChange: (selectedKeys: number[]) => {
|
||||
selectedRowKeys.value = selectedKeys
|
||||
}
|
||||
}
|
||||
|
||||
const handleBatchDelete = () => {
|
||||
if (selectedRowKeys.value.length === 0) {
|
||||
message.warning('请先选择要删除的权限')
|
||||
return
|
||||
}
|
||||
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: `确定要删除选中的 ${selectedRowKeys.value.length} 个权限吗?`,
|
||||
okText: '确定',
|
||||
okType: 'danger',
|
||||
onOk: async () => {
|
||||
try {
|
||||
const response = await batchDeletePermissions(selectedRowKeys.value)
|
||||
|
||||
if (response.success) {
|
||||
message.success(response.data.message || '权限已删除')
|
||||
selectedRowKeys.value = []
|
||||
loadPermissions()
|
||||
} else {
|
||||
message.error('删除失败')
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('删除失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
|
||||
330
admin-system/src/pages/system/logs.vue
Normal file
330
admin-system/src/pages/system/logs.vue
Normal file
@@ -0,0 +1,330 @@
|
||||
<template>
|
||||
<div class="system-logs">
|
||||
<a-page-header
|
||||
title="系统日志"
|
||||
sub-title="查看系统操作日志"
|
||||
>
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<a-button @click="handleRefresh">
|
||||
<template #icon>
|
||||
<ReloadOutlined />
|
||||
</template>
|
||||
刷新
|
||||
</a-button>
|
||||
<a-button @click="handleExport">
|
||||
<template #icon>
|
||||
<DownloadOutlined />
|
||||
</template>
|
||||
导出
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-page-header>
|
||||
|
||||
<!-- 搜索区域 -->
|
||||
<a-card>
|
||||
<div class="search-container">
|
||||
<a-form layout="inline" :model="searchForm">
|
||||
<a-form-item label="日志级别">
|
||||
<a-select v-model:value="searchForm.level" style="width: 120px" allow-clear>
|
||||
<a-select-option value="info">信息</a-select-option>
|
||||
<a-select-option value="warn">警告</a-select-option>
|
||||
<a-select-option value="error">错误</a-select-option>
|
||||
<a-select-option value="debug">调试</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="模块">
|
||||
<a-select v-model:value="searchForm.module" style="width: 120px" allow-clear>
|
||||
<a-select-option value="auth">认证</a-select-option>
|
||||
<a-select-option value="database">数据库</a-select-option>
|
||||
<a-select-option value="system">系统</a-select-option>
|
||||
<a-select-option value="task">任务</a-select-option>
|
||||
<a-select-option value="api">API</a-select-option>
|
||||
<a-select-option value="order">订单</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="时间范围">
|
||||
<a-range-picker v-model:value="searchForm.timeRange" :placeholder="['开始时间', '结束时间']" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<a-button type="primary" @click="handleSearch">
|
||||
<template #icon>
|
||||
<SearchOutlined />
|
||||
</template>
|
||||
搜索
|
||||
</a-button>
|
||||
<a-button style="margin-left: 8px" @click="handleReset">
|
||||
重置
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
|
||||
<!-- 日志表格 -->
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="logList"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
:row-key="record => record.id"
|
||||
@change="handleTableChange"
|
||||
:scroll="{ x: 1200 }"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'level'">
|
||||
<a-tag :color="getLevelColor(record.level)">{{ getLevelText(record.level) }}</a-tag>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'timestamp'">
|
||||
{{ formatTime(record.timestamp) }}
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'actions'">
|
||||
<a-button size="small" @click="handleViewDetail(record)">
|
||||
<EyeOutlined />
|
||||
详情
|
||||
</a-button>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
|
||||
<!-- 日志详情模态框 -->
|
||||
<a-modal
|
||||
v-model:open="detailModalVisible"
|
||||
title="日志详情"
|
||||
width="600px"
|
||||
:footer="null"
|
||||
>
|
||||
<a-descriptions bordered size="small" v-if="currentLog">
|
||||
<a-descriptions-item label="ID" :span="3">{{ currentLog.id }}</a-descriptions-item>
|
||||
<a-descriptions-item label="级别" :span="1">
|
||||
<a-tag :color="getLevelColor(currentLog.level)">{{ getLevelText(currentLog.level) }}</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="模块" :span="1">{{ currentLog.module }}</a-descriptions-item>
|
||||
<a-descriptions-item label="时间" :span="1">{{ formatTime(currentLog.timestamp) }}</a-descriptions-item>
|
||||
<a-descriptions-item label="用户ID" :span="1">{{ currentLog.userId || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="IP地址" :span="1">{{ currentLog.ip || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="消息" :span="3">{{ currentLog.message }}</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { message, type FormInstance } from 'ant-design-vue'
|
||||
import type { TableProps } from 'ant-design-vue'
|
||||
import dayjs from 'dayjs'
|
||||
import {
|
||||
SearchOutlined,
|
||||
ReloadOutlined,
|
||||
DownloadOutlined,
|
||||
EyeOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import { getSystemLogs } from '@/api/system'
|
||||
import type { SystemLog, SystemLogQueryParams } from '@/api/system'
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
interface SearchForm {
|
||||
level: string
|
||||
module: string
|
||||
timeRange: any[]
|
||||
}
|
||||
|
||||
const loading = ref(false)
|
||||
const searchForm = reactive<SearchForm>({
|
||||
level: '',
|
||||
module: '',
|
||||
timeRange: []
|
||||
})
|
||||
|
||||
const logList = ref<SystemLog[]>([])
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 20,
|
||||
total: 0,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total: number) => `共 ${total} 条记录`
|
||||
})
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '级别',
|
||||
key: 'level',
|
||||
width: 100,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '模块',
|
||||
dataIndex: 'module',
|
||||
key: 'module',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '消息',
|
||||
dataIndex: 'message',
|
||||
key: 'message',
|
||||
width: 300
|
||||
},
|
||||
{
|
||||
title: '用户ID',
|
||||
dataIndex: 'userId',
|
||||
key: 'userId',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: 'IP地址',
|
||||
dataIndex: 'ip',
|
||||
key: 'ip',
|
||||
width: 150
|
||||
},
|
||||
{
|
||||
title: '时间',
|
||||
key: 'timestamp',
|
||||
width: 200
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 100,
|
||||
align: 'center'
|
||||
}
|
||||
]
|
||||
|
||||
// 详情模态框
|
||||
const detailModalVisible = ref(false)
|
||||
const currentLog = ref<SystemLog | null>(null)
|
||||
|
||||
// 获取级别颜色
|
||||
const getLevelColor = (level: string) => {
|
||||
const colors: Record<string, string> = {
|
||||
info: 'blue',
|
||||
warn: 'orange',
|
||||
error: 'red',
|
||||
debug: 'purple'
|
||||
}
|
||||
return colors[level] || 'default'
|
||||
}
|
||||
|
||||
// 获取级别文本
|
||||
const getLevelText = (level: string) => {
|
||||
const texts: Record<string, string> = {
|
||||
info: '信息',
|
||||
warn: '警告',
|
||||
error: '错误',
|
||||
debug: '调试'
|
||||
}
|
||||
return texts[level] || level
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (timestamp: string) => {
|
||||
return dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss')
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
loadLogs()
|
||||
})
|
||||
|
||||
// 方法
|
||||
const loadLogs = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
// 构造查询参数
|
||||
const params: SystemLogQueryParams = {
|
||||
page: pagination.current,
|
||||
limit: pagination.pageSize
|
||||
}
|
||||
|
||||
if (searchForm.level) {
|
||||
params.level = searchForm.level
|
||||
}
|
||||
|
||||
if (searchForm.module) {
|
||||
params.module = searchForm.module
|
||||
}
|
||||
|
||||
if (searchForm.timeRange && searchForm.timeRange.length === 2) {
|
||||
params.startDate = searchForm.timeRange[0].toISOString()
|
||||
params.endDate = searchForm.timeRange[1].toISOString()
|
||||
}
|
||||
|
||||
const response = await getSystemLogs(params)
|
||||
|
||||
if (response.success) {
|
||||
logList.value = response.data.logs
|
||||
pagination.total = response.data.pagination?.total || 0
|
||||
} else {
|
||||
message.error('加载日志列表失败')
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('加载日志列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
pagination.current = 1
|
||||
loadLogs()
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
Object.assign(searchForm, {
|
||||
level: '',
|
||||
module: '',
|
||||
timeRange: []
|
||||
})
|
||||
pagination.current = 1
|
||||
loadLogs()
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
loadLogs()
|
||||
message.success('数据已刷新')
|
||||
}
|
||||
|
||||
const handleExport = () => {
|
||||
message.info('导出功能开发中')
|
||||
}
|
||||
|
||||
const handleTableChange: TableProps['onChange'] = (pag) => {
|
||||
pagination.current = pag.current!
|
||||
pagination.pageSize = pag.pageSize!
|
||||
loadLogs()
|
||||
}
|
||||
|
||||
const handleViewDetail = (record: SystemLog) => {
|
||||
currentLog.value = record
|
||||
detailModalVisible.value = true
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.system-logs {
|
||||
.search-container {
|
||||
margin-bottom: 16px;
|
||||
padding: 16px;
|
||||
background: #fafafa;
|
||||
border-radius: 6px;
|
||||
|
||||
:deep(.ant-form-item) {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-table-thead > tr > th) {
|
||||
background: #fafafa;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
@@ -119,7 +119,19 @@ const routes: RouteRecordRaw[] = [
|
||||
title: '系统设置',
|
||||
icon: 'SettingOutlined',
|
||||
permissions: ['system:read'],
|
||||
layout: 'main' // 添加布局信息
|
||||
layout: 'main'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/system/logs',
|
||||
name: 'SystemLogs',
|
||||
component: () => import('@/pages/system/logs.vue'),
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
title: '系统日志',
|
||||
icon: 'FileTextOutlined',
|
||||
permissions: ['system:read'],
|
||||
layout: 'main'
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user