由于本次代码变更内容为空,无法生成有效的提交信息。请提供具体的代码变更内容以便生成合适的提交信息。

This commit is contained in:
2025-09-10 21:03:19 +08:00
parent 0197a903f1
commit 8f677fffee
5 changed files with 451 additions and 4 deletions

View File

@@ -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({

View File

@@ -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()

View File

@@ -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">

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

View File

@@ -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'
}
},
{