Files
baiyue-logistics-chat/static/js/admin_orders.js
T

164 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(function(){
"use strict";
const csrf = document.querySelector('meta[name="staff-csrf-token"]').content;
const fileInput = document.getElementById('order-file');
const fileName = document.getElementById('file-name');
const form = document.getElementById('order-import-form');
const status = document.getElementById('import-status');
const results = document.getElementById('import-results');
const confirmButton = document.getElementById('confirm-import');
let selectedFile = null;
let canConfirm = false;
let orderBefore = null;
function setStatus(message, isError){
status.textContent = message || '';
status.classList.toggle('is-error', Boolean(isError));
}
function escapeText(value){
return String(value == null ? '' : value).replace(/[&<>"']/g, function(character){
return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[character];
});
}
function showResponse(data){
results.hidden = false;
const summary = data.summary || {};
document.getElementById('import-summary').innerHTML = [
['文件数据行', summary.total_rows], ['有效行', summary.valid_rows],
['重复保留', summary.duplicate_rows], ['错误行', summary.error_rows]
].map(function(item){
return '<div class="summary-item"><span>'+escapeText(item[0])+'</span><strong>'+escapeText(item[1] || 0)+'</strong></div>';
}).join('');
document.getElementById('preview-rows').innerHTML = (data.preview || []).map(function(row){
return '<tr><td>'+escapeText(row.row)+'</td><td>'+escapeText(row.tenant_id)+'</td><td>'+escapeText(row.order_no)+'</td><td>'+escapeText(row.status)+'</td><td>'+escapeText(row.inbound_code)+'</td><td class="'+(row.duplicate ? 'row-duplicate' : 'row-new')+'">'+(row.duplicate ? '已存在,保留原数据' : '可新增')+'</td></tr>';
}).join('') || '<tr><td colspan="6">没有可预览的有效数据</td></tr>';
document.getElementById('preview-limit-note').hidden = !data.preview_limited;
const errors = document.getElementById('import-errors');
errors.innerHTML = (data.errors || []).map(function(item){
return '<p>第 '+escapeText(item.row)+' 行:'+escapeText(item.message)+'</p>';
}).join('');
errors.hidden = !data.errors || !data.errors.length;
document.getElementById('results-caption').textContent = errors.hidden
? '请确认租户、订单号及重复记录处理方式。'
: '发现错误行;请修正文件后重新上传,当前不会写入数据。';
canConfirm = Boolean(data.ok && summary.valid_rows);
confirmButton.hidden = !canConfirm;
}
async function sendFile(path){
const body = new FormData();
body.append('file', selectedFile);
const response = await fetch(path, {method:'POST', body:body, credentials:'same-origin',
headers:{'X-CSRF-Token':csrf, 'Accept':'application/json'}, signal:AbortSignal.timeout(30000)});
const data = await response.json();
if(!response.ok && !data.summary) throw new Error(data.error || '请求失败,请稍后重试。');
return data;
}
async function requestJson(path, options){
options = options || {};
options.credentials = 'same-origin';
options.headers = Object.assign({'Accept':'application/json'}, options.headers || {});
const response = await fetch(path, options);
const data = await response.json();
if(!response.ok) throw new Error(data.error || '请求失败,请稍后重试。');
return data;
}
function renderOrders(data, append){
const body = document.getElementById('order-list-rows');
const rows = (data.orders || []).map(function(row){
return '<tr><td>'+escapeText(row.tenant_id)+'</td><td><strong>'+escapeText(row.order_no)+'</strong></td><td>'+escapeText(row.status)+'</td><td>'+escapeText(row.inbound_code)+'</td><td>'+escapeText(row.shipment_id)+'</td><td>'+escapeText(row.country_code)+'</td><td>'+escapeText(row.source_updated_at)+'</td></tr>';
}).join('');
if(!append) body.innerHTML = '';
body.insertAdjacentHTML('beforeend', rows || (!append ? '<tr><td colspan="7">没有找到订单</td></tr>' : ''));
orderBefore = data.next_before || null;
document.getElementById('load-more-orders').hidden = !orderBefore;
document.getElementById('order-list-status').textContent = '共 '+escapeText(data.total || 0)+' 条订单';
}
async function loadOrders(append){
const params = new URLSearchParams({q:document.getElementById('order-query').value.trim(), tenant:document.getElementById('order-tenant').value.trim(), status:document.getElementById('order-status').value, limit:'50'});
if(append && orderBefore) params.set('before', orderBefore);
const data = await requestJson('/api/admin/orders/list?'+params.toString());
renderOrders(data, append);
}
async function loadBatches(){
const data = await requestJson('/api/admin/orders/import/batches');
document.getElementById('import-batch-rows').innerHTML = (data.batches || []).map(function(batch){
const active = batch.state === 'active';
const action = active ? '<button class="button-secondary batch-undo" data-batch-id="'+escapeText(batch.id)+'" type="button">撤销</button>' : '已撤销(删 '+escapeText(batch.deleted_rows)+',留 '+escapeText(batch.preserved_on_undo)+')';
return '<tr><td>'+escapeText(batch.imported_at)+'</td><td>'+escapeText(batch.filename)+'</td><td>'+escapeText(batch.imported_by)+'</td><td>'+escapeText(batch.imported_rows)+'</td><td>'+escapeText(batch.preserved_rows)+'</td><td>'+escapeText(active ? '可撤销' : '已撤销')+'</td><td>'+action+'</td></tr>';
}).join('') || '<tr><td colspan="7">暂无导入记录</td></tr>';
document.querySelectorAll('.batch-undo').forEach(function(button){
button.addEventListener('click', async function(){
if(!await SupportUI.confirm('撤销后只会删除本批次中仍未被修改的新增订单,确定继续吗?',{title:'撤销导入'})) return;
button.disabled = true;
try{
const result = await requestJson('/api/admin/orders/import/batches/'+encodeURIComponent(button.dataset.batchId)+'/undo',{method:'POST',headers:{'X-CSRF-Token':csrf}});
setStatus('批次已处理:删除 '+result.deleted+' 条,保留 '+result.preserved+' 条。');
await Promise.all([loadBatches(), loadOrders(false)]);
}catch(error){ setStatus(error.message, true); button.disabled = false; }
});
});
}
fileInput.addEventListener('change', function(){
selectedFile = fileInput.files[0] || null;
fileName.textContent = selectedFile ? selectedFile.name : '选择订单文件';
results.hidden = true;
canConfirm = false;
confirmButton.hidden = true;
setStatus('');
});
form.addEventListener('submit', async function(event){
event.preventDefault();
if(!selectedFile) return;
const button = document.getElementById('preview-import');
button.disabled = true;
setStatus('正在读取并校验文件…');
try{
const data = await sendFile('/api/admin/orders/import/preview');
showResponse(data);
setStatus(data.ok ? '校验完成,请检查结果。' : '发现错误行,请先修正文件。', !data.ok);
}catch(error){ setStatus(error.message || '校验失败,请检查网络后重试。', true); }
finally{ button.disabled = false; }
});
confirmButton.addEventListener('click', async function(){
if(!canConfirm || !selectedFile) return;
confirmButton.disabled = true;
setStatus('正在重新校验并导入…');
try{
const data = await sendFile('/api/admin/orders/import');
canConfirm = false;
confirmButton.hidden = true;
fileInput.value = '';
selectedFile = null;
fileName.textContent = '选择订单文件';
setStatus('导入完成:新增 '+data.imported+' 条,重复保留 '+data.preserved+' 条。');
const refreshed = await Promise.allSettled([loadBatches(), loadOrders(false)]);
if(refreshed[0].status === 'rejected'){
document.getElementById('import-batch-rows').innerHTML = '<tr><td colspan="7">导入已完成,但记录刷新失败,请刷新页面重试。</td></tr>';
}
if(refreshed[1].status === 'rejected'){
document.getElementById('order-list-status').textContent = '导入已完成,但订单列表刷新失败:'+refreshed[1].reason.message;
}
if(refreshed.some(function(result){ return result.status === 'rejected'; })){
setStatus('导入已完成,但部分列表刷新失败,请刷新页面查看。', true);
}
}catch(error){ setStatus(error.message || '导入失败,请检查网络后重试。', true); }
finally{ confirmButton.disabled = false; }
});
document.getElementById('order-search-form').addEventListener('submit', async function(event){
event.preventDefault();
try { await loadOrders(false); } catch(error) { document.getElementById('order-list-status').textContent = error.message; }
});
document.getElementById('load-more-orders').addEventListener('click', async function(){
this.disabled = true;
try { await loadOrders(true); } catch(error) { document.getElementById('order-list-status').textContent = error.message; }
finally { this.disabled = false; }
});
Promise.all([loadOrders(false), loadBatches()]).catch(function(error){ document.getElementById('order-list-status').textContent = error.message; });
document.getElementById('staff-logout').addEventListener('click', async function(){
this.disabled = true;
try{
await fetch('/admin/logout', {method:'POST', credentials:'same-origin', headers:{'X-CSRF-Token':csrf}});
}finally{ location.assign('/admin'); }
});
})();