在资产管理、SKU管理和分类管理中新增操作日志查看功能,支持查看各实体的操作历史记录,同时新增操作日志API接口定义包含查询参数和日志信息类型

This commit is contained in:
WUSIJIAN
2026-01-16 14:19:24 +08:00
parent 404f0b719d
commit 30da5e3c29
5 changed files with 208 additions and 3 deletions

View File

@@ -191,3 +191,32 @@ export function stockOperation(data: StockOperationParams) {
data,
});
}
// 操作日志查询参数
export interface LogQueryParams {
collection_id: string;
pageNum?: number;
pageSize?: number;
}
// 操作日志信息
export interface OperationLogInfo {
id: string;
service_name: string;
collection: string;
collection_id: string[];
operation: string;
creator: string;
createdAt: string;
data: { FieldName: string; FieldValue: any }[] | null;
ip_address: string;
}
// 查询操作日志
export function listLogs(params: LogQueryParams) {
return newService({
url: '/assets/log/listLogs',
method: 'get',
params,
});
}

View File

@@ -78,10 +78,11 @@
</el-tooltip>
</template>
</el-table-column>
<el-table-column label="操作" width="180" align="center">
<el-table-column label="操作" width="220" align="center">
<template #default="scope">
<el-button size="small" text type="primary" @click="onEditSku(scope.row)">编辑</el-button>
<el-button v-if="!scope.row.unlimitedStock" size="small" text type="success" @click="onGenerateStock(scope.row)">生成库存</el-button>
<el-button size="small" text type="info" @click="onViewLog(scope.row)">日志</el-button>
<el-button size="small" text type="danger" @click="onDeleteSku(scope.row)">删除</el-button>
</template>
</el-table-column>
@@ -209,6 +210,7 @@
</template>
</el-dialog>
</el-dialog>
<OperationLogDialog ref="operationLogRef" />
</template>
<script setup lang="ts">
@@ -217,6 +219,7 @@ import { ElMessage, type FormInstance, type FormRules } from 'element-plus';
import { ElMessageBox } from 'element-plus';
import { listAssetSkus, createAssetSku, updateAssetSku, deleteAssetSku, getAssetSku, getAsset, uploadAssetImage, getSpecsUnitOptions, getStockFormFields, stockOperation } from '/@/api/assets/asset';
import { createFormDiff } from '/@/utils/diffUtils';
import OperationLogDialog from '../../component/operationLogDialog.vue';
import type { UploadRequestOptions, UploadUserFile } from 'element-plus';
interface SpecValueItem {
@@ -249,6 +252,7 @@ const editLoading = ref(false);
const skuFormVisible = ref(false);
const isEditSku = ref(false);
const editSkuId = ref('');
const operationLogRef = ref();
// 库存弹窗相关
const stockFormVisible = ref(false);
@@ -529,6 +533,11 @@ const onEditSku = async (row: any) => {
}
};
// 查看日志
const onViewLog = (row: any) => {
operationLogRef.value?.openDialog(row.id);
};
// 删除 SKU
const onDeleteSku = (row: any) => {
ElMessageBox.confirm(`确定要删除SKU "${row.skuName}" 吗?`, '提示', {

View File

@@ -61,10 +61,11 @@
<el-table-column prop="offlineTime" label="下线时间" width="170" show-overflow-tooltip />
<el-table-column prop="createdAt" label="创建时间" width="170" show-overflow-tooltip />
<el-table-column prop="updatedAt" label="修改时间" width="170" show-overflow-tooltip />
<el-table-column label="操作" width="200" fixed="right" align="center">
<el-table-column label="操作" width="250" fixed="right" align="center">
<template #default="scope">
<el-button size="small" text type="primary" @click="onEdit(scope.row)">修改</el-button>
<el-button size="small" text type="success" @click="onAddSku(scope.row)">规格管理</el-button>
<el-button size="small" text type="info" @click="onViewLog(scope.row)">日志</el-button>
<el-button size="small" text type="danger" @click="onRowDel(scope.row)">删除</el-button>
</template>
</el-table-column>
@@ -85,6 +86,7 @@
</div>
<EditAsset ref="editAssetRef" @getAssetList="getAssetList" />
<SkuDialog ref="skuDialogRef" />
<OperationLogDialog ref="operationLogRef" />
</div>
</template>
@@ -100,6 +102,7 @@ import { ElMessageBox, ElMessage } from 'element-plus';
import { listAssets, updateAssetStatus, deleteAsset } from '/@/api/assets/asset';
import EditAsset from './component/editAsset.vue';
import SkuDialog from './component/skuDialog.vue';
import OperationLogDialog from '../component/operationLogDialog.vue';
interface AssetRow {
id: string;
@@ -119,6 +122,7 @@ interface AssetRow {
const editAssetRef = ref();
const skuDialogRef = ref();
const operationLogRef = ref();
const tableData = reactive({
data: [] as AssetRow[],
@@ -231,6 +235,11 @@ const onAddSku = (row: AssetRow) => {
});
};
// 查看日志
const onViewLog = (row: AssetRow) => {
operationLogRef.value?.openDialog(row.id);
};
// 分页大小改变
const onSizeChange = (size: number) => {
tableData.param.pageSize = size;

View File

@@ -44,16 +44,18 @@
/>
</template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<el-table-column label="操作" width="250" fixed="right">
<template #default="scope">
<el-button size="small" class="op-btn-add" text type="primary" @click="onOpenAddCategory(scope.row)">新增</el-button>
<el-button size="small" text type="primary" @click="onOpenEditCategory(scope.row)">修改</el-button>
<el-button size="small" text type="info" @click="onViewLog(scope.row)">日志</el-button>
<el-button size="small" text type="danger" @click="onRowDel(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<EditCategory ref="editCategoryRef" @getCategoryList="getCategoryList" />
<OperationLogDialog ref="operationLogRef" />
</div>
</template>
@@ -67,6 +69,7 @@ export default {
import { ref, reactive, onMounted } from 'vue';
import { ElMessageBox, ElMessage } from 'element-plus';
import EditCategory from './component/editCategory.vue';
import OperationLogDialog from '../component/operationLogDialog.vue';
import { getCategoryTree, deleteCategory, updateCategoryStatus,listCategories } from '/@/api/assets/category';
interface CategoryRow {
@@ -82,6 +85,7 @@ interface CategoryRow {
}
const editCategoryRef = ref();
const operationLogRef = ref();
const tableData = reactive({
data: [] as CategoryRow[],
loading: false,
@@ -169,6 +173,11 @@ const onStatusChange = (row: CategoryRow) => {
});
};
// 查看日志
const onViewLog = (row: CategoryRow) => {
operationLogRef.value?.openDialog(row.id);
};
onMounted(() => {
getCategoryList();
});

View File

@@ -0,0 +1,149 @@
<template>
<el-dialog v-model="dialogVisible" title="操作日志" width="800px" :close-on-click-modal="false" append-to-body>
<el-table :data="logList" style="width: 100%" v-loading="loading" border max-height="500">
<el-table-column prop="createdAt" label="操作时间" width="170" />
<el-table-column prop="operation" label="操作类型" width="100" align="center">
<template #default="scope">
<el-tag :type="getOperationTagType(scope.row.operation)">{{ getOperationLabel(scope.row.operation) }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="creator" label="操作人" width="120" />
<el-table-column prop="data" label="变更内容" min-width="200">
<template #default="scope">
<div v-if="scope.row.data && scope.row.data.length > 0">
<div v-for="(item, index) in scope.row.data" :key="index" class="change-item">
<span class="field-name">{{ item.FieldName }}:</span>
<span class="field-value">{{ formatFieldValue(item.FieldValue) }}</span>
</div>
</div>
<span v-else class="no-data">-</span>
</template>
</el-table-column>
<el-table-column prop="ip_address" label="IP地址" width="140" />
</el-table>
<!-- 分页 -->
<div class="mt15" style="text-align: right" v-if="total > 0">
<el-pagination
v-model:current-page="queryParams.pageNum"
v-model:page-size="queryParams.pageSize"
:page-sizes="[10, 20, 50]"
:total="total"
layout="total, sizes, prev, pager, next"
@size-change="onSizeChange"
@current-change="onCurrentChange"
/>
</div>
<template #footer>
<el-button @click="dialogVisible = false">关闭</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue';
import { listLogs, OperationLogInfo } from '/@/api/assets/asset';
const dialogVisible = ref(false);
const loading = ref(false);
const logList = ref<OperationLogInfo[]>([]);
const total = ref(0);
const queryParams = reactive({
collection_id: '',
pageNum: 1,
pageSize: 10,
});
// 打开弹窗
const openDialog = (collectionId: string) => {
queryParams.collection_id = collectionId;
queryParams.pageNum = 1;
dialogVisible.value = true;
fetchLogs();
};
// 获取日志列表
const fetchLogs = async () => {
loading.value = true;
try {
const res: any = await listLogs(queryParams);
if (res.code === 0 && res.data) {
logList.value = res.data.logs || [];
total.value = res.data.total || 0;
}
} catch (error) {
// 错误已由拦截器处理
} finally {
loading.value = false;
}
};
// 操作类型标签
const getOperationTagType = (operation: string) => {
switch (operation) {
case 'insert':
return 'success';
case 'update':
return 'warning';
case 'delete':
return 'danger';
default:
return 'info';
}
};
// 操作类型文本
const getOperationLabel = (operation: string) => {
switch (operation) {
case 'insert':
return '新增';
case 'update':
return '修改';
case 'delete':
return '删除';
default:
return operation;
}
};
// 格式化字段值
const formatFieldValue = (value: any) => {
if (value === null || value === undefined) return '-';
if (typeof value === 'boolean') return value ? '是' : '否';
if (typeof value === 'object') return JSON.stringify(value);
return String(value);
};
// 分页
const onSizeChange = () => {
queryParams.pageNum = 1;
fetchLogs();
};
const onCurrentChange = () => {
fetchLogs();
};
defineExpose({
openDialog,
});
</script>
<style scoped lang="scss">
.change-item {
margin-bottom: 4px;
&:last-child {
margin-bottom: 0;
}
.field-name {
color: #909399;
margin-right: 8px;
}
.field-value {
color: #303133;
}
}
.no-data {
color: #c0c4cc;
}
</style>