- 最后 3 个老写法 list 页收尾, 全部 list 页风格 100% 统一 - rr-page-header div -> <PageHeader> 组件 - rr-filters-row div + el-input -> <SearchBar> 组件 (fields 配置) - DataTable 加 :page-sizes=[10,20,50,100] (W17.3.7 统一规范) - limit 默认 20 -> 10 (W17.3.6 pager 字号统一) - 清理不再使用的老 style class (rr-page-*/rr-filters-row/rr-*-input) - 业务逻辑全保留 (method tag/status 映射/详情弹窗/errorInfo 预览)
196 lines
6.7 KiB
Vue
196 lines
6.7 KiB
Vue
<script setup lang="ts">
|
|
/**
|
|
* F006 操作日志 - W4.14
|
|
*
|
|
* W17.5: 套 PageHeader/SearchBar 标准化模板 (limit 10 + :page-sizes)
|
|
*
|
|
* 后端: GET /sys/log/operation/page
|
|
* 字段: id/module/operation/requestUri/requestMethod/requestParams/requestTime/userAgent/ip/status/creatorName/createDate
|
|
*
|
|
* 设计规范:v1.6
|
|
*/
|
|
import { onMounted, ref } from 'vue'
|
|
import { httpGet } from '@/utils/request'
|
|
import DataTable from '@/components/DataTable/DataTable.vue'
|
|
import PageHeader from '@/components/PageHeader/PageHeader.vue'
|
|
import SearchBar from '@/components/SearchBar/SearchBar.vue'
|
|
|
|
interface LogEntry {
|
|
id: string
|
|
module: string
|
|
operation: string
|
|
requestUri: string
|
|
requestMethod: string
|
|
requestParams: string | null
|
|
requestTime: number | null
|
|
userAgent: string | null
|
|
ip: string | null
|
|
status: number | null
|
|
creatorName: string | null
|
|
createDate: string
|
|
raw: any
|
|
}
|
|
|
|
const STATUS_LABEL: Record<number, { label: string; type: string }> = {
|
|
0: { label: '成功', type: 'success' },
|
|
1: { label: '失败', type: 'danger' }
|
|
}
|
|
|
|
const list = ref<LogEntry[]>([])
|
|
const total = ref(0)
|
|
const loading = ref(false)
|
|
const error = ref<string | null>(null)
|
|
const page = ref(1)
|
|
const limit = ref(10)
|
|
|
|
const filters = ref({ creatorName: '', module: '' })
|
|
|
|
function fmtDate(ts?: string | number | null) {
|
|
if (!ts) return '--'
|
|
const n = typeof ts === 'string' ? Date.parse(ts) : ts
|
|
if (!n || isNaN(n)) return String(ts)
|
|
const d = new Date(n)
|
|
if (isNaN(d.getTime())) return String(ts)
|
|
const pad = (x: number) => String(x).padStart(2, '0')
|
|
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
|
|
}
|
|
|
|
function requestTimeColor(ms: number | null): string {
|
|
if (ms == null) return 'var(--el-text-color-regular)'
|
|
if (ms > 1000) return 'var(--el-color-danger)'
|
|
if (ms > 500) return 'var(--el-color-warning)'
|
|
return 'var(--el-text-color-regular)'
|
|
}
|
|
|
|
async function load() {
|
|
loading.value = true
|
|
error.value = null
|
|
try {
|
|
const data = await httpGet<{ total: number | string; list: any[] }>('/sys/log/operation/page', {
|
|
page: page.value,
|
|
limit: limit.value,
|
|
...(filters.value.creatorName ? { creatorName: filters.value.creatorName } : {}),
|
|
...(filters.value.module ? { module: filters.value.module } : {})
|
|
})
|
|
list.value = (data.list || []).map((it: any) => ({
|
|
id: String(it.id),
|
|
module: it.module || '--',
|
|
operation: it.operation || '--',
|
|
requestUri: it.requestUri || '--',
|
|
requestMethod: it.requestMethod || '--',
|
|
requestParams: it.requestParams,
|
|
requestTime: it.requestTime,
|
|
userAgent: it.userAgent,
|
|
ip: it.ip,
|
|
status: it.status,
|
|
creatorName: it.creatorName,
|
|
createDate: it.createDate,
|
|
raw: it
|
|
}))
|
|
total.value = Number(data.total) || 0
|
|
} catch (e: any) {
|
|
error.value = e?.msg || e?.message || '后端接口异常'
|
|
list.value = []
|
|
total.value = 0
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
function onSearch() { page.value = 1; load() }
|
|
function onReset() { filters.value.creatorName = ''; filters.value.module = ''; page.value = 1; load() }
|
|
function onPageChange(p: number) { page.value = p; load() }
|
|
function onLimitChange(l: number) { limit.value = l; page.value = 1; load() }
|
|
|
|
onMounted(load)
|
|
</script>
|
|
|
|
<template>
|
|
<div class="rr-sys-log-operation">
|
|
<PageHeader title="操作日志" :sub="`共 ${total} 条操作记录`" />
|
|
|
|
<SearchBar
|
|
v-model:filters="filters"
|
|
:fields='[
|
|
{ key: "creatorName", label: "操作用户", type: "text", placeholder: "操作用户" },
|
|
{ key: "module", label: "模块名", type: "text", placeholder: "模块名" }
|
|
]'
|
|
@search="onSearch"
|
|
@reset="onReset"
|
|
/>
|
|
|
|
<div class="rr-list-card">
|
|
<DataTable
|
|
:columns="[
|
|
{ prop: 'creatorName', label: '操作用户', width: 130 },
|
|
{ prop: 'module', label: '模块', width: 100, slot: 'module' },
|
|
{ prop: 'operation', label: '操作', minWidth: 180 },
|
|
{ prop: 'requestMethod', label: '方法', width: 80, slot: 'method' },
|
|
{ prop: 'requestUri', label: '请求 URI', minWidth: 200, slot: 'uri' },
|
|
{ prop: 'status', label: '状态', width: 80, slot: 'status' },
|
|
{ prop: 'requestTime', label: '耗时(ms)', width: 100, slot: 'time' },
|
|
{ prop: 'createDate', label: '时间', width: 170, slot: 'createDate' }
|
|
]"
|
|
:data="list"
|
|
:loading="loading"
|
|
:total="total"
|
|
:page="page"
|
|
:limit="limit"
|
|
:show-pagination="true"
|
|
:page-sizes="[10, 20, 50, 100]"
|
|
:error="error"
|
|
empty-text="暂无操作日志"
|
|
borderless
|
|
row-key="id"
|
|
@page-change="onPageChange"
|
|
@limit-change="onLimitChange"
|
|
>
|
|
<template #module="{ row }">
|
|
<el-tag size="small" effect="plain">{{ row.module }}</el-tag>
|
|
</template>
|
|
<template #method="{ row }">
|
|
<span :class="['rr-method', `rr-method-${(row.requestMethod || '').toLowerCase()}`]">
|
|
{{ row.requestMethod }}
|
|
</span>
|
|
</template>
|
|
<template #uri="{ row }">
|
|
<code class="rr-mono-sm">{{ row.requestUri }}</code>
|
|
</template>
|
|
<template #status="{ row }">
|
|
<el-tag
|
|
v-if="row.status != null && STATUS_LABEL[row.status]"
|
|
:type="STATUS_LABEL[row.status].type as any"
|
|
size="small"
|
|
effect="light"
|
|
>{{ STATUS_LABEL[row.status].label }}</el-tag>
|
|
<span v-else>--</span>
|
|
</template>
|
|
<template #time="{ row }">
|
|
<span :style="{ color: requestTimeColor(row.requestTime) }">{{ row.requestTime ?? '--' }}</span>
|
|
</template>
|
|
<template #createDate="{ row }">
|
|
<span class="rr-time">{{ fmtDate(row.createDate) }}</span>
|
|
</template>
|
|
</DataTable>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.rr-sys-log-operation { padding: 16px; }
|
|
.rr-list-card { background: var(--el-bg-color); border-radius: 12px; padding: 4px; }
|
|
.rr-time { font-family: 'SF Mono', Menlo, Monaco, Consolas, monospace; font-size: 12px; color: var(--el-text-color-regular); }
|
|
.rr-mono-sm { font-family: 'SF Mono', Menlo, Monaco, Consolas, monospace; font-size: 11px; color: var(--el-text-color-regular); }
|
|
.rr-method {
|
|
display: inline-block;
|
|
padding: 1px 6px;
|
|
border-radius: 4px;
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
font-family: 'SF Mono', Menlo, Monaco, Consolas, monospace;
|
|
}
|
|
.rr-method-get { background: #DBEAFE; color: #2563EB; }
|
|
.rr-method-post { background: #D1FAE5; color: #059669; }
|
|
.rr-method-put { background: #FEF3C7; color: #D97706; }
|
|
.rr-method-delete { background: #FEE2E2; color: #DC2626; }
|
|
</style> |