164 lines
10 KiB
JavaScript
164 lines
10 KiB
JavaScript
(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 {'&':'&','<':'<','>':'>','"':'"',"'":'''}[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'); }
|
||
});
|
||
})();
|