feat: 增加日志模块

This commit is contained in:
chansee97 2025-09-16 17:38:22 +08:00
parent d6c70bad0a
commit d400ebedc0
14 changed files with 955 additions and 3 deletions

View File

@ -55,7 +55,7 @@
"md-editor-v3": "^5.6.1",
"pinia": "^3.0.3",
"pinia-plugin-persistedstate": "^4.5.0",
"pro-naive-ui": "^3.0.3",
"pro-naive-ui": "^3.1.1",
"quill": "^2.0.3",
"radash": "^12.1.1",
"vue": "^3.5.20",

View File

@ -20,6 +20,10 @@ const propOverrides = {
preset: 'card',
},
ProDataTable: {
tableCardProps: {
size: 'small',
},
size: 'small',
paginateSinglePage: false,
},
}

View File

@ -0,0 +1,31 @@
import { request } from '@/utils/alova'
export type LoginLogSearchQuery = Partial<Entity.LoginLog> & Api.PageParams
/**
*
*/
export function getLoginLogPage(params: LoginLogSearchQuery) {
return request.Get<Api.ListResponse<Entity.LoginLog>>('/login-log', { params })
}
/**
*
*/
export function getLoginLogDetail(id: number) {
return request.Get<Entity.LoginLog>(`/login-log/${id}`)
}
/**
*
*/
export function deleteLoginLog(ids: string) {
return request.Delete(`/login-log/${ids}`)
}
/**
*
*/
export function clearLoginLog() {
return request.Delete('/login-log/clean')
}

View File

@ -0,0 +1,31 @@
import { request } from '@/utils/alova'
export interface OperLogQueryParams extends Api.PageParams {
}
/**
*
*/
export function getOperLogPage(params: OperLogQueryParams) {
return request.Get<Api.ListResponse<Entity.OperLog>>('/oper-log', { params })
}
/**
*
*/
export function getOperLogDetail(id: number) {
return request.Get<Entity.OperLog>(`/oper-log/${id}`)
}
/**
*
*/
export function deleteOperLog(ids: string) {
return request.Delete(`/oper-log/${ids}`)
}
/**
*
*/
export function clearOperLog() {
return request.Delete('/oper-log/clean')
}

View File

@ -1,12 +1,14 @@
import {
create,
ProDateRange,
ProDateTimeRange,
ProInput,
ProSelect,
} from 'pro-naive-ui'
import type { App } from 'vue'
const proNaive = create({
components: [ProInput, ProSelect],
components: [ProInput, ProSelect, ProDateRange, ProDateTimeRange],
})
export function install(app: App) {

View File

@ -127,7 +127,7 @@ export function setupRouterGuard(router: Router) {
router.beforeResolve((to) => {
// 设置菜单高亮
routeStore.setActiveMenu(to.meta.activePath ?? to.fullPath)
routeStore.setActiveMenu(to.meta.activePath || to.fullPath)
// 添加tabs
tabStore.addTab(to)
// 设置高亮标签

43
src/typings/entities/login-log.d.ts vendored Normal file
View File

@ -0,0 +1,43 @@
declare namespace Entity {
/**
*
*/
interface LoginLog {
/**
*
*/
id: number
/**
*
*/
username: string
/**
* IP地址
*/
ipaddr: string
/**
*
*/
loginLocation: string
/**
*
*/
browser: string
/**
*
*/
os: string
/**
* 0 1
*/
status: number
/**
*
*/
msg: string
/**
* 访
*/
loginTime: string
}
}

75
src/typings/entities/oper-log.d.ts vendored Normal file
View File

@ -0,0 +1,75 @@
declare namespace Entity {
/**
*
*/
interface OperLog {
/**
*
*/
id: number
/**
*
*/
title: string
/**
*
*/
method: string
/**
*
*/
requestMethod: string
/**
*
*/
operName: string
/**
*
*/
deptName: string
/**
* URL
*/
operUrl: string
/**
*
*/
operIp: string
/**
*
*/
operLocation: string
/**
*
*/
browser: string
/**
*
*/
os: string
/**
*
*/
operParam: string
/**
*
*/
jsonResult: string
/**
* 0 1
*/
status: number
/**
*
*/
errorMsg: string
/**
*
*/
costTime: string
/**
*
*/
operTime: Date
}
}

View File

@ -9,6 +9,11 @@ declare namespace Entity {
/* 各类接口返回的数据类型, 具体内容在 ./api */
declare namespace Api {
interface PageParams {
pageNum: number
pageSize: number
}
interface Response<T> {
/** 业务状态码 */
code: number

View File

@ -0,0 +1,155 @@
import type { DataTableColumns } from 'naive-ui'
import { NButton, NSpace, NTag } from 'naive-ui'
import type { ProSearchFormColumns } from 'pro-naive-ui'
import { renderProCopyableText } from 'pro-naive-ui'
// 登录日志搜索表单数据类型
export interface LoginLogSearchFormData {
ipaddr?: string
userName?: string
status?: number
loginTime?: string
}
// 登录日志搜索表单列配置
export const searchColumns: ProSearchFormColumns<LoginLogSearchFormData> = [
{
title: '登录地址',
path: 'ipaddr',
field: 'input',
fieldProps: {
clearable: true,
},
},
{
title: '用户名称',
path: 'username',
field: 'input',
fieldProps: {
clearable: true,
},
},
{
title: '状态',
path: 'status',
field: 'select',
fieldProps: {
clearable: true,
options: [
{ label: '成功', value: '0' },
{ label: '失败', value: '1' },
],
},
},
{
title: '登录时间',
path: 'loginTime',
field: 'date-time-range',
fieldProps: {
clearable: true,
format: 'yyyy-MM-dd HH:mm:ss',
valueFormat: 'yyyy-MM-dd HH:mm:ss',
defaultTime: ['00:00:00', '23:59:59'],
},
},
]
// 表格列配置
export function createTableColumns(options: {
onDelete: (infoId: number) => void
}): DataTableColumns<Entity.LoginLog> {
const { onDelete } = options
return [
{
type: 'selection',
},
{
title: '用户名称',
key: 'username',
align: 'center',
ellipsis: {
tooltip: true,
},
},
{
title: '登录地址',
key: 'ipaddr',
width: 130,
align: 'center',
render: row => renderProCopyableText(row.ipaddr),
},
{
title: '登录地点',
key: 'loginLocation',
width: 150,
align: 'center',
ellipsis: {
tooltip: true,
},
},
{
title: '浏览器',
key: 'browser',
align: 'center',
ellipsis: {
tooltip: true,
},
},
{
title: '操作系统',
key: 'os',
align: 'center',
ellipsis: {
tooltip: true,
},
},
{
title: '登录状态',
key: 'status',
width: 80,
align: 'center',
render: (row) => {
return (
<NTag type={row.status === 0 ? 'success' : 'error'} bordered={false}>
{row.status === 0 ? '成功' : '失败'}
</NTag>
)
},
},
{
title: '操作信息',
key: 'msg',
width: 200,
align: 'center',
ellipsis: {
tooltip: true,
},
},
{
title: '登录日期',
key: 'loginTime',
align: 'center',
},
{
title: '操作',
key: 'actions',
width: 150,
align: 'center',
fixed: 'right',
render: (row) => {
return (
<NSpace justify="center">
<NButton
text
type="error"
onClick={() => onDelete(row.id)}
>
</NButton>
</NSpace>
)
},
},
]
}

View File

@ -0,0 +1,149 @@
<script setup lang="tsx">
import { createProSearchForm, useNDataTable } from 'pro-naive-ui'
import {
clearLoginLog,
deleteLoginLog,
getLoginLogPage,
} from '@/api/monitor/login-log'
import type { LoginLogSearchQuery } from '@/api/monitor/login-log'
import { createTableColumns, searchColumns } from './columns'
import type { LoginLogSearchFormData } from './columns'
//
const searchForm = createProSearchForm<LoginLogSearchFormData>({
defaultCollapsed: true,
initialValues: {
},
})
// 使useNDataTable
const {
table: {
tableProps,
},
search: {
proSearchFormProps,
searchLoading,
},
refresh,
} = useNDataTable(getList, {
form: searchForm,
})
//
const checkedRowKeys = ref<number[]>([])
/** 查询登录日志列表 */
async function getList({ current, pageSize }: any, formData: LoginLogSearchFormData) {
try {
const params: LoginLogSearchQuery = {
pageNum: current,
pageSize,
...formData,
}
return getLoginLogPage(params).then(res => res.data)
}
catch (error) {
console.error('获取登录日志列表失败:', error)
return {
total: 0,
list: [],
}
}
}
/** 删除登录日志 */
async function handleDelete(infoId: number | number[]) {
const isBatch = Array.isArray(infoId)
const ids = isBatch ? infoId.join(',') : infoId.toString()
const count = isBatch ? infoId.length : 1
window.$dialog.warning({
title: '确认删除',
content: isBatch
? `是否确认删除选中的 ${count} 条登录日志?`
: '是否确认删除该条登录日志?',
positiveText: '确定删除',
negativeText: '取消',
onPositiveClick: async () => {
try {
await deleteLoginLog(ids)
window.$message.success('删除成功')
if (isBatch) {
//
checkedRowKeys.value = []
}
refresh()
}
catch (error) {
console.error('删除登录日志失败:', error)
}
},
})
}
/** 清空登录日志 */
async function handleClean() {
window.$dialog.warning({
title: '确认清空',
content: '是否确认清空所有登录日志数据?此操作不可恢复!',
positiveText: '确定清空',
negativeText: '取消',
onPositiveClick: async () => {
try {
await clearLoginLog()
window.$message.success('清空成功')
refresh()
}
catch (error) {
console.error('清空登录日志失败:', error)
}
},
})
}
//
const columns = createTableColumns({
onDelete: handleDelete,
})
</script>
<template>
<n-space vertical>
<n-card>
<pro-search-form
v-bind="proSearchFormProps"
:form="searchForm"
:columns="searchColumns"
/>
</n-card>
<!-- 数据表格 -->
<pro-data-table
v-bind="tableProps"
v-model:checked-row-keys="checkedRowKeys"
:columns="columns"
row-key="id"
:loading="searchLoading"
title=" "
>
<template #toolbar>
<n-flex>
<n-button type="error" :disabled="checkedRowKeys.length === 0" @click="handleDelete(checkedRowKeys)">
<template #icon>
<icon-park-outline-delete />
</template>
删除
</n-button>
<n-button type="warning" @click="handleClean">
<template #icon>
<icon-park-outline-clear />
</template>
清空
</n-button>
</n-flex>
</template>
</pro-data-table>
</n-space>
</template>

View File

@ -0,0 +1,168 @@
import type { DataTableColumns } from 'naive-ui'
import { NButton, NSpace, NTag } from 'naive-ui'
import type { ProSearchFormColumns } from 'pro-naive-ui'
import { renderProCopyableText } from 'pro-naive-ui'
// 操作日志搜索表单数据类型
export interface OperationLogSearchFormData {
operUrl?: string
title?: string
operName?: string
businessType?: number
status?: 0 | 1
operTime?: [string, string]
}
// 操作日志搜索表单列配置
export const searchColumns: ProSearchFormColumns<OperationLogSearchFormData> = [
{
title: '操作地址',
path: 'operUrl',
field: 'input',
fieldProps: {
clearable: true,
},
},
{
title: '系统模块',
path: 'title',
field: 'input',
fieldProps: {
clearable: true,
},
},
{
title: '操作人员',
path: 'operName',
field: 'input',
fieldProps: {
clearable: true,
},
},
{
title: '状态',
path: 'status',
field: 'select',
fieldProps: {
clearable: true,
options: [
{ label: '正常', value: 0 },
{ label: '异常', value: 1 },
],
},
},
{
title: '操作时间',
path: 'operTime',
field: 'date-time-range',
fieldProps: {
clearable: true,
format: 'yyyy-MM-dd',
valueFormat: 'yyyy-MM-dd HH:mm:ss',
defaultTime: ['00:00:00', '23:59:59'],
},
},
]
// 表格列配置
export function createTableColumns(options: {
onView: (row: Entity.OperLog) => void
onDelete: (operId: number) => void
}): DataTableColumns<Entity.OperLog> {
const { onView, onDelete } = options
return [
{
type: 'selection',
width: 55,
align: 'center',
},
{
title: '系统模块',
key: 'title',
width: 150,
align: 'center',
ellipsis: {
tooltip: true,
},
},
{
title: '操作人员',
key: 'operName',
width: 120,
align: 'center',
ellipsis: {
tooltip: true,
},
},
{
title: '主机地址',
key: 'operIp',
width: 130,
align: 'center',
render: row => renderProCopyableText(row.operIp),
},
{
title: '操作地点',
key: 'operLocation',
width: 150,
align: 'center',
ellipsis: {
tooltip: true,
},
},
{
title: '操作状态',
key: 'status',
width: 100,
align: 'center',
render: (row) => {
return (
<NTag type={row.status === 0 ? 'success' : 'error'} bordered={false}>
{row.status === 0 ? '正常' : '异常'}
</NTag>
)
},
},
{
title: '操作日期',
key: 'operTime',
width: 180,
align: 'center',
},
{
title: '消耗时间',
key: 'costTime',
width: 100,
align: 'center',
render: row => `${row.costTime}`,
},
{
title: '操作',
key: 'actions',
width: 150,
align: 'center',
fixed: 'right',
render: (row) => {
return (
<NSpace justify="center">
<NButton
text
type="primary"
onClick={() => onView(row)}
>
</NButton>
<NButton
text
type="error"
onClick={() => onDelete(row.id)}
>
</NButton>
</NSpace>
)
},
},
]
}

View File

@ -0,0 +1,119 @@
<script setup lang="ts">
import { useBoolean } from '@/hooks'
const { bool: visible, setTrue: showModal, setFalse: hideModal } = useBoolean(false)
const operationLog = ref<Entity.OperLog | null>(null)
//
function openModal(row: Entity.OperLog) {
operationLog.value = row
showModal()
}
//
function closeModal() {
hideModal()
operationLog.value = null
}
defineExpose({
openModal,
})
</script>
<template>
<n-modal
v-model:show="visible"
preset="card"
title="操作日志详细"
class="w-4/5 max-w-4xl"
:mask-closable="false"
>
<div v-if="operationLog" class="space-y-4">
<!-- 基本信息 -->
<n-card title="基本信息" size="small">
<n-descriptions :column="3" label-placement="left" size="small">
<n-descriptions-item label="日志编号">
{{ operationLog.id }}
</n-descriptions-item>
<n-descriptions-item label="系统模块">
{{ operationLog.title }}
</n-descriptions-item>
<n-descriptions-item label="部门名称">
{{ operationLog.deptName || '-' }}
</n-descriptions-item>
<n-descriptions-item label="操作状态">
<n-tag size="small">
{{ operationLog.status === 0 ? '正常' : '异常' }}
</n-tag>
</n-descriptions-item>
<n-descriptions-item label="消耗时间">
{{ `${operationLog.costTime}ms` }}
</n-descriptions-item>
<n-descriptions-item label="操作时间">
{{ operationLog.operTime }}
</n-descriptions-item>
</n-descriptions>
</n-card>
<!-- 请求信息 -->
<n-card title="请求信息" size="small">
<n-descriptions :column="2" label-placement="left" size="small">
<n-descriptions-item label="调用方法" :span="2">
<n-text code>
{{ operationLog.method }}
</n-text>
</n-descriptions-item>
<n-descriptions-item label="请求地址">
<n-text code>
{{ operationLog.operUrl }}
</n-text>
</n-descriptions-item>
<n-descriptions-item label="请求方式">
<n-tag size="small">
{{ operationLog.requestMethod }}
</n-tag>
</n-descriptions-item>
<n-descriptions-item label="主机地址">
{{ operationLog.operIp }}
</n-descriptions-item>
<n-descriptions-item label="操作地点">
{{ operationLog.operLocation }}
</n-descriptions-item>
</n-descriptions>
</n-card>
<!-- 请求参数 -->
<pro-card title="请求参数" size="small" :show="false">
<n-scrollbar style="max-height: 200px">
<pre>{{ JSON.parse(operationLog.operParam) }}</pre>
</n-scrollbar>
</pro-card>
<!-- 返回结果 -->
<pro-card title="返回结果" size="small">
<n-scrollbar style="max-height: 200px">
<pre>{{ operationLog.jsonResult }}</pre>
</n-scrollbar>
</pro-card>
<!-- 异常信息 -->
<n-card v-if="operationLog.status === 1" title="异常信息" size="small">
<n-scrollbar style="max-height: 200px">
<n-text type="error">
{{ operationLog.errorMsg || '无异常信息' }}
</n-text>
</n-scrollbar>
</n-card>
</div>
<template #footer>
<div class="flex justify-end">
<n-button @click="closeModal">
关闭
</n-button>
</div>
</template>
</n-modal>
</template>

View File

@ -0,0 +1,170 @@
<script setup lang="tsx">
import { createProSearchForm, useNDataTable } from 'pro-naive-ui'
import {
clearOperLog,
deleteOperLog,
getOperLogPage,
} from '@/api/monitor/oper-log'
import type { OperLogQueryParams } from '@/api/monitor/oper-log'
import { createTableColumns, searchColumns } from './columns'
import type { OperationLogSearchFormData } from './columns'
//
import DetailModal from './components/DetailModal.vue'
//
const detailModalRef = ref()
//
const searchForm = createProSearchForm<OperationLogSearchFormData>({
defaultCollapsed: true,
initialValues: {
},
})
// 使useNDataTable
const {
table: {
tableProps,
},
search: {
proSearchFormProps,
searchLoading,
},
refresh,
} = useNDataTable(getList, {
form: searchForm,
})
//
const checkedRowKeys = ref<(number)[]>([])
/** 查询操作日志列表 */
interface Result {
total: number
list: Entity.OperLog[]
}
async function getList({ current, pageSize }: any, formData: OperationLogSearchFormData): Promise<Result> {
try {
const params: OperLogQueryParams = {
pageNum: current,
pageSize,
...formData,
}
return getOperLogPage(params).then(res => res.data)
}
catch (error) {
console.error('获取操作日志列表失败:', error)
return {
total: 0,
list: [],
}
}
}
/** 查看详情 */
function handleView(row: Entity.OperLog) {
detailModalRef.value?.openModal(row)
}
/** 删除操作日志 */
async function handleDelete(operId: number | number[]) {
const isBatch = Array.isArray(operId)
const ids = isBatch ? operId.join(',') : operId.toString()
window.$dialog.warning({
title: '确认删除',
content: isBatch
? `是否确认删除选中的 ${operId.length} 条操作日志?`
: '是否确认删除该条操作日志?',
positiveText: '确定删除',
negativeText: '取消',
onPositiveClick: async () => {
try {
await deleteOperLog(ids)
window.$message.success('删除成功')
if (isBatch) {
//
checkedRowKeys.value = []
}
refresh()
}
catch (error) {
console.error('删除操作日志失败:', error)
}
},
})
}
/** 清空操作日志 */
async function handleClean() {
window.$dialog.warning({
title: '确认清空',
content: '是否确认清空所有操作日志数据?此操作不可恢复!',
positiveText: '确定清空',
negativeText: '取消',
onPositiveClick: async () => {
try {
await clearOperLog()
window.$message.success('清空成功')
refresh()
}
catch (error) {
console.error('清空操作日志失败:', error)
}
},
})
}
//
const columns = createTableColumns({
onView: handleView,
onDelete: handleDelete,
})
</script>
<template>
<n-space vertical>
<n-card>
<pro-search-form
v-bind="proSearchFormProps"
:form="searchForm"
:columns="searchColumns"
/>
</n-card>
<!-- 数据表格 -->
<pro-data-table
v-bind="tableProps"
v-model:checked-row-keys="checkedRowKeys"
:columns="columns"
row-key="id"
:loading="searchLoading"
title=" "
>
<template #toolbar>
<n-flex>
<n-button
type="error"
:disabled="checkedRowKeys.length === 0"
@click="handleDelete(checkedRowKeys.map(id => Number(id)))"
>
<template #icon>
<icon-park-outline-delete />
</template>
删除
</n-button>
<n-button type="warning" @click="handleClean">
<template #icon>
<icon-park-outline-clear />
</template>
清空
</n-button>
</n-flex>
</template>
</pro-data-table>
<!-- 详情弹窗 -->
<DetailModal ref="detailModalRef" />
</n-space>
</template>