material_manage_system/frontend/src/views/Logs.vue

216 lines
6.4 KiB
Vue

<template>
<div class="logs-page">
<div class="page-header">
<h2>日志记录</h2>
</div>
<div class="filter-bar">
<el-select v-model="filters.operationType" placeholder="操作类型" style="width: 150px">
<el-option label="全部" value="" />
<el-option label="新增" value="CREATE" />
<el-option label="更新" value="UPDATE" />
<el-option label="删除" value="DELETE" />
</el-select>
<el-date-picker
v-model="filters.dateRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
style="margin-left: 10px"
/>
<el-select
v-model="filters.materialId"
placeholder="输入物料编码或名称搜索"
filterable
:filter-method="logsMaterialFilter"
clearable
style="margin-left: 10px; width: 260px"
@change="(id) => onMaterialSelect(id)"
>
<el-option
v-for="m in getFilteredMaterialsForLogs()"
:key="m.id"
:label="logsMaterialLabel(m)"
:value="m.id"
/>
</el-select>
<el-button @click="handleSearch" style="margin-left: 10px">查询</el-button>
<el-button @click="handleReset">重置</el-button>
</div>
<BaseTable
:data="logsStore.logs"
:loading="logsStore.loading"
:current-page="logsStore.page.currentPage"
:page-size="logsStore.page.pageSize"
:total="logsStore.page.total"
@page-change="handlePageChange"
>
<el-table-column prop="operation_type" label="操作类型">
<template #default="{ row }">
<StatusBadge :status="row.operation_type" />
</template>
</el-table-column>
<el-table-column prop="material_id" label="物料ID" width="80" />
<el-table-column prop="operator" label="操作人" />
<el-table-column prop="description" label="详情" show-overflow-tooltip>
<template #default="{ row }">
<el-button type="primary" link @click="showDetail(row)">查看详情</el-button>
</template>
</el-table-column>
<el-table-column prop="created_at" label="时间" width="160">
<template #default="{ row }">
{{ formatDate(row.created_at) }}
</template>
</el-table-column>
</BaseTable>
<el-dialog v-model="detailVisible" title="操作详情" width="700px">
<el-descriptions :column="2" border v-if="currentRow">
<el-descriptions-item label="物料ID">{{ currentRow.material_id }}</el-descriptions-item>
<el-descriptions-item label="操作类型" :span="2"><StatusBadge :status="currentRow.operation_type" /></el-descriptions-item>
<el-descriptions-item label="操作人">{{ currentRow.operator }}</el-descriptions-item>
<el-descriptions-item label="时间">{{ formatDate(currentRow.created_at) }}</el-descriptions-item>
<el-descriptions-item label="详情" :span="2">
<span class="log-description" style="white-space: pre-wrap; overflow-wrap: anywhere">{{ currentRow.description }}</span>
</el-descriptions-item>
</el-descriptions>
<div v-if="currentRow?.old_value || currentRow?.new_value" class="value-compare">
<h4>变更对比</h4>
<div class="value-block">
<div class="value-label">旧值 (old_value)</div>
<pre>{{ formatJson(currentRow?.old_value) }}</pre>
</div>
<div class="value-block">
<div class="value-label">新值 (new_value)</div>
<pre>{{ formatJson(currentRow?.new_value) }}</pre>
</div>
</div>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, watch, onMounted } from 'vue'
import { useLogsStore } from '../stores/logs'
import { useMaterialsStore } from '../stores/materials'
import BaseTable from '../components/business/BaseTable.vue'
import StatusBadge from '../components/business/StatusBadge.vue'
import { formatDate, toDateParam } from '../utils/datetime'
import { searchMaterialOptions } from '../utils/materialSearch'
const logsStore = useLogsStore()
const materialsStore = useMaterialsStore()
const detailVisible = ref(false)
const currentRow = ref(null)
const logsMaterialSearchText = ref('')
function logsMaterialFilter(val) {
logsMaterialSearchText.value = val
}
function getFilteredMaterialsForLogs() {
return searchMaterialOptions(materialsStore.historicalMaterialOptions, logsMaterialSearchText.value)
}
// 检索框里会出现已作废的物料,标注出来免得和有效物料混淆。
function logsMaterialLabel(material) {
const label = `${material.material_code} - ${material.name}`
return material.status === 'obsolete' ? `${label}(已作废)` : label
}
function onMaterialSelect(id) {
filters.materialId = id || ''
}
function showDetail(row) {
currentRow.value = row
detailVisible.value = true
}
function formatJson(value) {
if (!value) return ''
if (typeof value === 'string') {
try {
return JSON.stringify(JSON.parse(value), null, 2)
} catch {
return value
}
}
return JSON.stringify(value, null, 2)
}
const filters = reactive({
operationType: '',
dateRange: null,
materialId: ''
})
onMounted(() => {
logsStore.fetchLogs()
// 日志是历史记录,检索框要能选中已作废物料,所以用含作废的那份选项,
// 而不是只装了当前页的分页列表 materials。
materialsStore.fetchHistoricalMaterialOptions()
})
function handleSearch() {
logsStore.setFilters({
operationType: filters.operationType,
startDate: toDateParam(filters.dateRange?.[0]),
endDate: toDateParam(filters.dateRange?.[1]),
materialId: filters.materialId
})
logsStore.fetchLogs()
}
function handleReset() {
filters.operationType = ''
filters.dateRange = null
filters.materialId = ''
logsMaterialSearchText.value = ''
logsStore.resetFilters()
logsStore.fetchLogs()
}
function handlePageChange(page) {
logsStore.fetchLogs(page)
}
</script>
<style scoped>
.logs-page {
padding: 20px;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.filter-bar {
display: flex;
margin-bottom: 15px;
}
.value-compare {
margin-top: 20px;
}
.value-compare h4 {
margin-bottom: 10px;
}
.value-block {
margin-bottom: 15px;
}
.value-label {
font-weight: bold;
margin-bottom: 5px;
}
.value-block pre {
background: #f5f7fa;
padding: 10px;
border-radius: 4px;
overflow-x: auto;
}
</style>