235 lines
7.6 KiB
JavaScript
235 lines
7.6 KiB
JavaScript
|
|
// 操作日志管理
|
|||
|
|
(() => {
|
|||
|
|
let currentPage = 1;
|
|||
|
|
const pageSize = 50;
|
|||
|
|
|
|||
|
|
Router.register('/system/operations-log', async () => {
|
|||
|
|
const html = `
|
|||
|
|
<div class="page-header">
|
|||
|
|
<h1>操作日志</h1>
|
|||
|
|
<div class="page-actions">
|
|||
|
|
<input type="text" id="keyword-filter" class="input" placeholder="搜索操作详情..." style="width: 200px; margin-right: 10px;" />
|
|||
|
|
<button id="search-btn" class="btn btn-secondary">搜索</button>
|
|||
|
|
<button id="cleanup-btn" class="btn btn-danger" style="margin-left: 10px;">清理旧日志</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="card">
|
|||
|
|
<div class="card-body">
|
|||
|
|
<div class="table-container">
|
|||
|
|
<table class="data-table">
|
|||
|
|
<thead>
|
|||
|
|
<tr>
|
|||
|
|
<th style="width: 60px;">ID</th>
|
|||
|
|
<th style="width: 100px;">用户</th>
|
|||
|
|
<th style="width: 150px;">操作类型</th>
|
|||
|
|
<th>操作详情</th>
|
|||
|
|
<th style="width: 180px;">操作时间</th>
|
|||
|
|
</tr>
|
|||
|
|
</thead>
|
|||
|
|
<tbody id="log-list">
|
|||
|
|
<tr>
|
|||
|
|
<td colspan="5" class="text-center">加载中...</td>
|
|||
|
|
</tr>
|
|||
|
|
</tbody>
|
|||
|
|
</table>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div id="pagination" style="margin-top: 20px; display: flex; justify-content: space-between; align-items: center;">
|
|||
|
|
<div id="page-info"></div>
|
|||
|
|
<div>
|
|||
|
|
<button id="prev-btn" class="btn btn-secondary" disabled>上一页</button>
|
|||
|
|
<button id="next-btn" class="btn btn-secondary" style="margin-left: 10px;">下一页</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
`;
|
|||
|
|
|
|||
|
|
setTimeout(() => {
|
|||
|
|
loadLogs();
|
|||
|
|
|
|||
|
|
document.getElementById('search-btn')?.addEventListener('click', () => {
|
|||
|
|
currentPage = 1;
|
|||
|
|
loadLogs();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
document.getElementById('keyword-filter')?.addEventListener('keypress', (e) => {
|
|||
|
|
if (e.key === 'Enter') {
|
|||
|
|
currentPage = 1;
|
|||
|
|
loadLogs();
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
document.getElementById('cleanup-btn')?.addEventListener('click', cleanupLogs);
|
|||
|
|
document.getElementById('prev-btn')?.addEventListener('click', () => {
|
|||
|
|
if (currentPage > 1) {
|
|||
|
|
currentPage--;
|
|||
|
|
loadLogs();
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
document.getElementById('next-btn')?.addEventListener('click', () => {
|
|||
|
|
currentPage++;
|
|||
|
|
loadLogs();
|
|||
|
|
});
|
|||
|
|
}, 100);
|
|||
|
|
|
|||
|
|
return html;
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 操作类型中文映射
|
|||
|
|
const actionLabels = {
|
|||
|
|
'login': '登录',
|
|||
|
|
'logout': '登出',
|
|||
|
|
'create_customer_order': '创建客户订单',
|
|||
|
|
'update_customer_order': '更新客户订单',
|
|||
|
|
'delete_customer_order': '删除客户订单',
|
|||
|
|
'create_reconciliation': '创建对账单',
|
|||
|
|
'update_reconciliation': '更新对账单',
|
|||
|
|
'delete_reconciliation': '删除对账单',
|
|||
|
|
'batch_delete_reconciliation': '批量删除对账单',
|
|||
|
|
'batch_delete_reconciliations': '批量删除对账单',
|
|||
|
|
'create_work_order': '创建工单',
|
|||
|
|
'update_work_order': '更新工单',
|
|||
|
|
'delete_work_order': '删除工单',
|
|||
|
|
'confirm_work_order': '确认工单',
|
|||
|
|
'create_bom': '创建BOM',
|
|||
|
|
'update_bom': '更新BOM',
|
|||
|
|
'delete_bom': '删除BOM',
|
|||
|
|
'import_bom': '导入BOM',
|
|||
|
|
'create_initial_stock': '创建期初库存',
|
|||
|
|
'update_initial_stock': '更新期初库存',
|
|||
|
|
'delete_initial_stock': '删除期初库存',
|
|||
|
|
'import_initial_stock': '导入期初库存',
|
|||
|
|
'create_purchase_demand': '创建采购需求',
|
|||
|
|
'update_purchase_demand': '更新采购需求',
|
|||
|
|
'delete_purchase_demand': '删除采购需求',
|
|||
|
|
'create_material_purchase': '创建物料采购',
|
|||
|
|
'update_material_purchase': '更新物料采购',
|
|||
|
|
'delete_material_purchase': '删除物料采购',
|
|||
|
|
'batch_delete_material_purchase': '批量删除物料采购',
|
|||
|
|
'import_material_purchase': '导入物料采购',
|
|||
|
|
'cleanup_operations_log': '清理操作日志',
|
|||
|
|
'add_personnel': '添加人员',
|
|||
|
|
'upload_sop': '上传SOP',
|
|||
|
|
'delete_sop': '删除SOP',
|
|||
|
|
'upload_mac': '上传MAC',
|
|||
|
|
'upload_stats': '上传统计',
|
|||
|
|
'upload_repairs': '上传返修',
|
|||
|
|
'upload_shipments': '上传发货',
|
|||
|
|
'create_shipment': '创建发货',
|
|||
|
|
'delete_shipment': '删除发货',
|
|||
|
|
'clear_redis': '清空缓存',
|
|||
|
|
'audit_yt_cost': '审核易泰勒成本',
|
|||
|
|
'audit_yt_probe': '审核易泰勒探针',
|
|||
|
|
'audit_pdd_cost': '审核拼多多成本',
|
|||
|
|
'audit_pdd_probe': '审核拼多多探针',
|
|||
|
|
'upload_shipment': '上传发货记录'
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
function getActionLabel(action) {
|
|||
|
|
return actionLabels[action] || action;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function loadLogs() {
|
|||
|
|
try {
|
|||
|
|
const keyword = document.getElementById('keyword-filter')?.value || '';
|
|||
|
|
const res = await fetch(`/api/operations-log?page=${currentPage}&page_size=${pageSize}&keyword=${encodeURIComponent(keyword)}`);
|
|||
|
|
const data = await res.json();
|
|||
|
|
|
|||
|
|
const tbody = document.getElementById('log-list');
|
|||
|
|
if (!tbody) return;
|
|||
|
|
|
|||
|
|
if (!data.list || data.list.length === 0) {
|
|||
|
|
tbody.innerHTML = '<tr><td colspan="5" class="text-center">暂无数据</td></tr>';
|
|||
|
|
updatePagination(0);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
tbody.innerHTML = data.list.map(log => `
|
|||
|
|
<tr>
|
|||
|
|
<td>${log.id}</td>
|
|||
|
|
<td>${log.username || '—'}</td>
|
|||
|
|
<td><span class="badge">${getActionLabel(log.action)}</span></td>
|
|||
|
|
<td style="max-width: 400px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" title="${escapeHtml(log.detail || '')}">${log.detail || '—'}</td>
|
|||
|
|
<td>${formatTime(log.ts)}</td>
|
|||
|
|
</tr>
|
|||
|
|
`).join('');
|
|||
|
|
|
|||
|
|
updatePagination(data.total);
|
|||
|
|
} catch (err) {
|
|||
|
|
console.error('加载操作日志失败:', err);
|
|||
|
|
API.toast('加载失败', 'error');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function updatePagination(total) {
|
|||
|
|
const pageInfo = document.getElementById('page-info');
|
|||
|
|
const prevBtn = document.getElementById('prev-btn');
|
|||
|
|
const nextBtn = document.getElementById('next-btn');
|
|||
|
|
|
|||
|
|
const totalPages = Math.ceil(total / pageSize);
|
|||
|
|
|
|||
|
|
if (pageInfo) {
|
|||
|
|
pageInfo.textContent = `共 ${total} 条记录,第 ${currentPage}/${totalPages || 1} 页`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (prevBtn) {
|
|||
|
|
prevBtn.disabled = currentPage <= 1;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (nextBtn) {
|
|||
|
|
nextBtn.disabled = currentPage >= totalPages;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function formatTime(ts) {
|
|||
|
|
if (!ts) return '—';
|
|||
|
|
try {
|
|||
|
|
const date = new Date(ts);
|
|||
|
|
return date.toLocaleString('zh-CN', {
|
|||
|
|
year: 'numeric',
|
|||
|
|
month: '2-digit',
|
|||
|
|
day: '2-digit',
|
|||
|
|
hour: '2-digit',
|
|||
|
|
minute: '2-digit',
|
|||
|
|
second: '2-digit'
|
|||
|
|
});
|
|||
|
|
} catch {
|
|||
|
|
return ts;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function escapeHtml(str) {
|
|||
|
|
if (!str) return '';
|
|||
|
|
return str.replace(/&/g, '&')
|
|||
|
|
.replace(/</g, '<')
|
|||
|
|
.replace(/>/g, '>')
|
|||
|
|
.replace(/"/g, '"');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function cleanupLogs() {
|
|||
|
|
if (!confirm('确定要清理3个月前的旧日志吗?此操作不可恢复。')) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
const res = await fetch('/api/operations-log/cleanup', {
|
|||
|
|
method: 'DELETE'
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const data = await res.json();
|
|||
|
|
|
|||
|
|
if (res.ok && data.ok) {
|
|||
|
|
API.toast(data.message, 'success');
|
|||
|
|
loadLogs();
|
|||
|
|
} else {
|
|||
|
|
API.toast(data.error || '清理失败', 'error');
|
|||
|
|
}
|
|||
|
|
} catch (err) {
|
|||
|
|
console.error('清理日志失败:', err);
|
|||
|
|
API.toast('清理失败', 'error');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
})();
|