216 lines
6.4 KiB
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>
|