<script setup lang="ts">
|
import { computed, onMounted, reactive, ref } from 'vue'
|
import { ElMessage } from 'element-plus'
|
import { listOperationLogs, type OperationLogRecord } from '@/api/operationLogs'
|
|
interface FilterState {
|
id: string
|
operator: string
|
dateRange: [string, string] | null
|
}
|
|
const filters = reactive<FilterState>({ id: '', operator: '全部', dateRange: null })
|
const appliedFilters = ref<FilterState>({ id: '', operator: '全部', dateRange: null })
|
const currentPage = ref(1)
|
const pageSize = ref(10)
|
const operationLogRecords = ref<OperationLogRecord[]>([])
|
const loading = ref(false)
|
const loadError = ref('')
|
async function loadLogs() {
|
loadError.value = ''
|
loading.value = true
|
try { operationLogRecords.value = await listOperationLogs() } catch (error) {
|
operationLogRecords.value = []
|
loadError.value = error instanceof Error ? error.message : '操作日志加载失败'
|
} finally { loading.value = false }
|
}
|
onMounted(loadLogs)
|
const operatorOptions = computed(() => [...new Set(operationLogRecords.value.map(item => item.operator))].sort((a, b) => a.localeCompare(b, 'zh-CN')))
|
|
const filteredRecords = computed(() => {
|
const query = appliedFilters.value
|
return operationLogRecords.value
|
.filter(item => !query.id.trim() || String(item.id).includes(query.id.trim()))
|
.filter(item => query.operator === '全部' || item.operator === query.operator)
|
.filter(item => !query.dateRange || (item.operatedAt.slice(0, 10) >= query.dateRange[0] && item.operatedAt.slice(0, 10) <= query.dateRange[1]))
|
.sort((a, b) => b.operatedAt.localeCompare(a.operatedAt))
|
})
|
|
const pagedRecords = computed(() => {
|
const start = (currentPage.value - 1) * pageSize.value
|
return filteredRecords.value.slice(start, start + pageSize.value)
|
})
|
|
function applySearch() {
|
appliedFilters.value = {
|
id: filters.id,
|
operator: filters.operator,
|
dateRange: filters.dateRange ? [...filters.dateRange] as [string, string] : null,
|
}
|
currentPage.value = 1
|
ElMessage.success(`已找到 ${filteredRecords.value.length} 条操作日志`)
|
}
|
|
function resetSearch() {
|
Object.assign(filters, { id: '', operator: '全部', dateRange: null })
|
appliedFilters.value = { id: '', operator: '全部', dateRange: null }
|
currentPage.value = 1
|
}
|
</script>
|
|
<template>
|
<div class="operation-log-page">
|
<el-alert v-if="loadError" :title="loadError" type="error" :closable="false" show-icon /><el-button v-if="loadError" @click="loadLogs">重试</el-button>
|
<section class="filter-panel" aria-label="操作日志查询条件">
|
<header class="section-heading"><div><i></i><strong>查询条件</strong><span>OPERATION LOG QUERY</span></div><small>共 {{ operationLogRecords.length }} 条日志</small></header>
|
<div class="filter-grid">
|
<label><span>日志ID</span><el-input v-model="filters.id" clearable placeholder="输入日志ID" @keyup.enter="applySearch" /></label>
|
<label><span>操作人</span><el-select v-model="filters.operator"><el-option label="全部" value="全部" /><el-option v-for="item in operatorOptions" :key="item" :label="item" :value="item" /></el-select></label>
|
<label><span>操作时间</span><el-date-picker v-model="filters.dateRange" type="daterange" value-format="YYYY-MM-DD" start-placeholder="开始日期" end-placeholder="结束日期" range-separator="至" unlink-panels /></label>
|
<div class="filter-actions"><button class="primary-button" type="button" @click="applySearch"><el-icon><Search /></el-icon>查询</button><button class="secondary-button" type="button" @click="resetSearch"><el-icon><RefreshLeft /></el-icon>重置</button></div>
|
</div>
|
</section>
|
|
<section class="result-panel">
|
<div class="result-toolbar"><div><i></i><strong>操作日志</strong><span>{{ filteredRecords.length }} RESULTS · 按操作时间倒序</span></div><small>只读审计记录</small></div>
|
<div class="table-wrap">
|
<table>
|
<thead><tr><th>日志ID</th><th>操作人</th><th>操作内容</th><th>操作时间</th></tr></thead>
|
<tbody>
|
<tr v-for="item in pagedRecords" :key="item.id"><td class="id-cell">#{{ item.id }}</td><td :title="item.operator"><span class="operator-avatar">{{ item.operator.slice(0, 1) }}</span><strong>{{ item.operator }}</strong></td><td :title="item.content">{{ item.content }}</td><td class="time-cell">{{ item.operatedAt }}</td></tr>
|
<tr v-if="!pagedRecords.length"><td colspan="4" class="empty-cell">{{ loading ? '正在加载操作日志' : loadError ? '操作日志加载失败' : '暂无符合条件的操作日志' }}</td></tr>
|
</tbody>
|
</table>
|
</div>
|
<footer class="pagination-bar"><span>显示 {{ pagedRecords.length }} 条,共 {{ filteredRecords.length }} 条</span><el-pagination v-model:current-page="currentPage" v-model:page-size="pageSize" :page-sizes="[10, 20, 50]" :total="filteredRecords.length" layout="sizes, prev, pager, next" background /></footer>
|
</section>
|
</div>
|
</template>
|
|
<style scoped>
|
.operation-log-page{width:100%}.primary-button,.secondary-button{height:34px;display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:0 13px;border:1px solid transparent;border-radius:3px;font-size:13px;font-weight:500;cursor:pointer;white-space:nowrap}.primary-button{color:#fff;background:var(--blue);border-color:var(--blue)}.primary-button:hover{background:#176fcb;border-color:#176fcb}.secondary-button{color:#52606d;background:#fff;border-color:var(--line-strong)}.secondary-button:hover{color:var(--blue);border-color:#9dc8f5}.filter-panel,.result-panel{background:#fff;border:1px solid #d8e1e9}.filter-panel{margin-bottom:16px}.section-heading,.result-toolbar{display:flex;align-items:center;justify-content:space-between;background:var(--surface-soft);border-bottom:1px solid var(--line)}.section-heading{height:44px;padding:0 16px}.section-heading>div,.result-toolbar>div{display:flex;align-items:center;gap:8px}.section-heading>div>i,.result-toolbar>div>i{width:3px;height:16px;background:#2b78a3}.section-heading strong,.result-toolbar strong{color:var(--ink);font-size:15px;font-weight:600}.section-heading span,.result-toolbar span{color:#81909d;font-size:10px}.section-heading small,.result-toolbar>small{color:#647380;font-size:11px}.filter-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:14px 18px;padding:16px}.filter-grid label{width:100%;min-width:0;display:grid;grid-template-columns:72px minmax(0,1fr);align-items:center;gap:8px}.filter-grid label>span{color:#52606d;font-size:12px;font-weight:500}.filter-grid :deep(.el-select),.filter-grid :deep(.el-date-editor){width:100%}.filter-grid :deep(.el-input__wrapper),.filter-grid :deep(.el-select__wrapper){min-height:34px;border-radius:3px;box-shadow:0 0 0 1px #d8e1e9 inset;background:#f8fafc}.filter-actions{display:flex;align-items:center;justify-content:flex-end;gap:8px}.result-toolbar{height:50px;padding:0 16px}.table-wrap{overflow-x:auto}table{width:100%;min-width:900px;table-layout:fixed;border-collapse:collapse;color:#52606d;font-size:13px}th,td{height:50px;padding:0 14px;border-right:1px solid #dde7f0;border-bottom:1px solid #dde7f0;text-align:left;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}th{height:40px;color:#52606d;background:#eaf4ff;border-color:#c7e0ff;font-size:12px;font-weight:600}th:nth-child(1){width:130px}th:nth-child(2){width:160px}th:nth-child(3){width:auto}th:nth-child(4){width:190px}th:last-child,td:last-child{border-right:0}tbody tr:hover{background:#f7fbff}tbody tr:last-child td{border-bottom:0}.id-cell,.time-cell{color:#647380;font-family:'Cascadia Mono',Consolas,monospace;font-size:12px}.operator-avatar{width:24px;height:24px;display:inline-grid;place-items:center;margin-right:8px;color:#2b78a3;background:#e6f2fd;font-size:11px;font-weight:700;vertical-align:middle}td strong{color:#31404d;font-weight:600}.empty-cell{height:180px;color:#8795a3;text-align:center}.pagination-bar{min-height:58px;display:flex;align-items:center;justify-content:space-between;gap:16px;padding:8px 14px 8px 16px;border-top:1px solid var(--line)}.pagination-bar>span{color:#7b8794;font-size:11px}.pagination-bar :deep(.el-pagination){--el-pagination-button-bg-color:#edf4fa;--el-pagination-hover-color:var(--blue)}@media(max-width:1320px){.filter-grid{grid-template-columns:repeat(3,minmax(0,1fr))}.filter-actions{grid-column:-2/-1}}@media(max-width:980px){.filter-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:680px){.filter-grid{grid-template-columns:1fr;gap:12px;padding:14px 12px}.filter-actions{grid-column:auto;justify-content:stretch}.filter-actions button{flex:1}.section-heading,.result-toolbar{padding-left:12px}.section-heading span,.result-toolbar span{display:none}.pagination-bar{align-items:flex-start;flex-direction:column;overflow-x:auto}}
|
</style>
|