admin_new/src/modules/sys/LogOperationView.vue
Sisyphus 557db07231 refactor(F006-W17.5): 3 个日志页标准化 (LogError/LogLogin/LogOperation 套 PageHeader+SearchBar)
- 最后 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 预览)
2026-07-09 11:07:19 +08:00

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>