bug fix
This commit is contained in:
parent
2e2b166e08
commit
573cf9feb5
@ -171,6 +171,9 @@ const API = (() => {
|
|||||||
listRepairs: () => request('/list/repairs'),
|
listRepairs: () => request('/list/repairs'),
|
||||||
listDefects: () => request('/list/defects'),
|
listDefects: () => request('/list/defects'),
|
||||||
listShipments: () => request('/list/shipments'),
|
listShipments: () => request('/list/shipments'),
|
||||||
|
listShipmentPlatforms: () => request('/shipments/platforms'),
|
||||||
|
addShipmentPlatform: (name) => request('/shipments/platforms', { method: 'POST', body: JSON.stringify({ name }) }),
|
||||||
|
deleteShipmentPlatform: (code) => request(`/shipments/platforms/${encodeURIComponent(code)}`, { method: 'DELETE' }),
|
||||||
auditPdd: (params={}) => request('/audit/pdd' + buildQuery(params)),
|
auditPdd: (params={}) => request('/audit/pdd' + buildQuery(params)),
|
||||||
auditYt: (params={}) => request('/audit/yt' + buildQuery(params)),
|
auditYt: (params={}) => request('/audit/yt' + buildQuery(params)),
|
||||||
auditTx: (params={}) => request('/audit/tx' + buildQuery(params)),
|
auditTx: (params={}) => request('/audit/tx' + buildQuery(params)),
|
||||||
|
|||||||
@ -90,6 +90,14 @@
|
|||||||
<div class="page-actions" style="margin-top: 16px;">
|
<div class="page-actions" style="margin-top: 16px;">
|
||||||
<button id="add-stock-btn" class="btn btn-primary" style="margin-right: 10px;">新增库存</button>
|
<button id="add-stock-btn" class="btn btn-primary" style="margin-right: 10px;">新增库存</button>
|
||||||
<button id="import-stock-btn" class="btn btn-secondary" style="margin-right: 10px;">导入 Excel</button>
|
<button id="import-stock-btn" class="btn btn-secondary" style="margin-right: 10px;">导入 Excel</button>
|
||||||
|
<div style="display: inline-block; position: relative; margin-right: 10px;">
|
||||||
|
<button id="export-btn" class="btn btn-secondary" onclick="InitialStock.toggleExportMenu(event)">导出 Excel ▼</button>
|
||||||
|
<div id="export-menu" style="display: none; position: absolute; top: 100%; left: 0; background: white; border: 1px solid var(--border); border-radius: 4px; box-shadow: 0 2px 8px rgba(0,0,0,0.15); z-index: 1000; min-width: 150px; margin-top: 4px;">
|
||||||
|
<button onclick="InitialStock.exportAll()" style="display: block; width: 100%; padding: 8px 16px; border: none; background: none; text-align: left; cursor: pointer; font-size: 14px;" onmouseover="this.style.background='var(--hover)'" onmouseout="this.style.background='none'">导出全部</button>
|
||||||
|
<button onclick="InitialStock.exportSelected()" style="display: block; width: 100%; padding: 8px 16px; border: none; background: none; text-align: left; cursor: pointer; font-size: 14px;" onmouseover="this.style.background='var(--hover)'" onmouseout="this.style.background='none'">导出选中</button>
|
||||||
|
<button onclick="InitialStock.exportByFactory()" style="display: block; width: 100%; padding: 8px 16px; border: none; background: none; text-align: left; cursor: pointer; font-size: 14px;" onmouseover="this.style.background='var(--hover)'" onmouseout="this.style.background='none'">按工厂导出</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<button id="download-template-btn" class="btn btn-secondary" style="margin-right: 10px;">下载模板</button>
|
<button id="download-template-btn" class="btn btn-secondary" style="margin-right: 10px;">下载模板</button>
|
||||||
<button id="restore-backup-btn" class="btn btn-secondary" style="margin-right: 10px;">📦 数据恢复</button>
|
<button id="restore-backup-btn" class="btn btn-secondary" style="margin-right: 10px;">📦 数据恢复</button>
|
||||||
<button id="batch-delete-btn" class="btn btn-danger">批量删除</button>
|
<button id="batch-delete-btn" class="btn btn-danger">批量删除</button>
|
||||||
@ -325,6 +333,15 @@
|
|||||||
document.getElementById('search-keyword')?.addEventListener('keypress', (e) => {
|
document.getElementById('search-keyword')?.addEventListener('keypress', (e) => {
|
||||||
if (e.key === 'Enter') InitialStock.search();
|
if (e.key === 'Enter') InitialStock.search();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
document.addEventListener('click', (e) => {
|
||||||
|
const exportMenu = document.getElementById('export-menu');
|
||||||
|
const exportBtn = document.getElementById('export-btn');
|
||||||
|
if (exportMenu && !exportMenu.contains(e.target) && e.target !== exportBtn) {
|
||||||
|
exportMenu.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
loadList();
|
loadList();
|
||||||
loadFactories();
|
loadFactories();
|
||||||
}, 100);
|
}, 100);
|
||||||
@ -656,6 +673,208 @@
|
|||||||
return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleExportMenu(event) {
|
||||||
|
event.stopPropagation();
|
||||||
|
const menu = document.getElementById('export-menu');
|
||||||
|
menu.style.display = menu.style.display === 'none' ? 'block' : 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ensureXLSXLoaded() {
|
||||||
|
if (typeof XLSX === 'undefined') {
|
||||||
|
try {
|
||||||
|
await loadXLSXLibrary();
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error('无法加载 Excel 库,请刷新页面重试');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadXLSXLibrary() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (typeof XLSX !== 'undefined') {
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const script = document.createElement('script');
|
||||||
|
script.src = 'https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js';
|
||||||
|
script.onload = () => resolve();
|
||||||
|
script.onerror = () => reject(new Error('加载 XLSX 库失败'));
|
||||||
|
document.head.appendChild(script);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function exportAll() {
|
||||||
|
try {
|
||||||
|
document.getElementById('export-menu').style.display = 'none';
|
||||||
|
|
||||||
|
if (stockList.length === 0) {
|
||||||
|
alert('没有数据可导出');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await ensureXLSXLoaded();
|
||||||
|
|
||||||
|
const exportData = stockList.map(item => ({
|
||||||
|
'物料编码': item.material_code || '',
|
||||||
|
'物料名称': item.material_name || '',
|
||||||
|
'库存数量': item.stock_qty || 0,
|
||||||
|
'单位': item.unit || 'pcs',
|
||||||
|
'最小包装': item.min_package || 1,
|
||||||
|
'供应商': item.supplier || '',
|
||||||
|
'工厂': item.factory || '',
|
||||||
|
'备注': item.remark || '',
|
||||||
|
'更新时间': formatTime(item.updated_at)
|
||||||
|
}));
|
||||||
|
|
||||||
|
const ws = XLSX.utils.json_to_sheet(exportData);
|
||||||
|
const wb = XLSX.utils.book_new();
|
||||||
|
XLSX.utils.book_append_sheet(wb, ws, '期初库存');
|
||||||
|
|
||||||
|
const colWidths = [
|
||||||
|
{ wch: 15 },
|
||||||
|
{ wch: 25 },
|
||||||
|
{ wch: 10 },
|
||||||
|
{ wch: 8 },
|
||||||
|
{ wch: 10 },
|
||||||
|
{ wch: 20 },
|
||||||
|
{ wch: 15 },
|
||||||
|
{ wch: 30 },
|
||||||
|
{ wch: 18 }
|
||||||
|
];
|
||||||
|
ws['!cols'] = colWidths;
|
||||||
|
|
||||||
|
const timestamp = new Date().toISOString().slice(0, 19).replace(/:/g, '-');
|
||||||
|
const filename = `期初库存_全部_${timestamp}.xlsx`;
|
||||||
|
|
||||||
|
XLSX.writeFile(wb, filename);
|
||||||
|
|
||||||
|
console.log(`成功导出 ${exportData.length} 条数据到 ${filename}`);
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error('导出失败:', err);
|
||||||
|
alert('导出失败: ' + err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function exportSelected() {
|
||||||
|
try {
|
||||||
|
document.getElementById('export-menu').style.display = 'none';
|
||||||
|
|
||||||
|
const checked = document.querySelectorAll('.row-checkbox:checked');
|
||||||
|
if (checked.length === 0) {
|
||||||
|
alert('请先选择要导出的数据');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedIds = Array.from(checked).map(cb => parseInt(cb.dataset.id));
|
||||||
|
const selectedData = stockList.filter(item => selectedIds.includes(item.id));
|
||||||
|
|
||||||
|
await ensureXLSXLoaded();
|
||||||
|
|
||||||
|
const exportData = selectedData.map(item => ({
|
||||||
|
'物料编码': item.material_code || '',
|
||||||
|
'物料名称': item.material_name || '',
|
||||||
|
'库存数量': item.stock_qty || 0,
|
||||||
|
'单位': item.unit || 'pcs',
|
||||||
|
'最小包装': item.min_package || 1,
|
||||||
|
'供应商': item.supplier || '',
|
||||||
|
'工厂': item.factory || '',
|
||||||
|
'备注': item.remark || '',
|
||||||
|
'更新时间': formatTime(item.updated_at)
|
||||||
|
}));
|
||||||
|
|
||||||
|
const ws = XLSX.utils.json_to_sheet(exportData);
|
||||||
|
const wb = XLSX.utils.book_new();
|
||||||
|
XLSX.utils.book_append_sheet(wb, ws, '期初库存');
|
||||||
|
|
||||||
|
const colWidths = [
|
||||||
|
{ wch: 15 },
|
||||||
|
{ wch: 25 },
|
||||||
|
{ wch: 10 },
|
||||||
|
{ wch: 8 },
|
||||||
|
{ wch: 10 },
|
||||||
|
{ wch: 20 },
|
||||||
|
{ wch: 15 },
|
||||||
|
{ wch: 30 },
|
||||||
|
{ wch: 18 }
|
||||||
|
];
|
||||||
|
ws['!cols'] = colWidths;
|
||||||
|
|
||||||
|
const timestamp = new Date().toISOString().slice(0, 19).replace(/:/g, '-');
|
||||||
|
const filename = `期初库存_选中${exportData.length}条_${timestamp}.xlsx`;
|
||||||
|
|
||||||
|
XLSX.writeFile(wb, filename);
|
||||||
|
|
||||||
|
console.log(`成功导出 ${exportData.length} 条数据到 ${filename}`);
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error('导出失败:', err);
|
||||||
|
alert('导出失败: ' + err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function exportByFactory() {
|
||||||
|
try {
|
||||||
|
document.getElementById('export-menu').style.display = 'none';
|
||||||
|
|
||||||
|
const factory = document.getElementById('factory-filter')?.value || '';
|
||||||
|
|
||||||
|
if (!factory) {
|
||||||
|
alert('请先选择要导出的工厂');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const factoryData = stockList.filter(item => item.factory === factory);
|
||||||
|
|
||||||
|
if (factoryData.length === 0) {
|
||||||
|
alert(`工厂 "${factory}" 没有数据可导出`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await ensureXLSXLoaded();
|
||||||
|
|
||||||
|
const exportData = factoryData.map(item => ({
|
||||||
|
'物料编码': item.material_code || '',
|
||||||
|
'物料名称': item.material_name || '',
|
||||||
|
'库存数量': item.stock_qty || 0,
|
||||||
|
'单位': item.unit || 'pcs',
|
||||||
|
'最小包装': item.min_package || 1,
|
||||||
|
'供应商': item.supplier || '',
|
||||||
|
'工厂': item.factory || '',
|
||||||
|
'备注': item.remark || '',
|
||||||
|
'更新时间': formatTime(item.updated_at)
|
||||||
|
}));
|
||||||
|
|
||||||
|
const ws = XLSX.utils.json_to_sheet(exportData);
|
||||||
|
const wb = XLSX.utils.book_new();
|
||||||
|
XLSX.utils.book_append_sheet(wb, ws, '期初库存');
|
||||||
|
|
||||||
|
const colWidths = [
|
||||||
|
{ wch: 15 },
|
||||||
|
{ wch: 25 },
|
||||||
|
{ wch: 10 },
|
||||||
|
{ wch: 8 },
|
||||||
|
{ wch: 10 },
|
||||||
|
{ wch: 20 },
|
||||||
|
{ wch: 15 },
|
||||||
|
{ wch: 30 },
|
||||||
|
{ wch: 18 }
|
||||||
|
];
|
||||||
|
ws['!cols'] = colWidths;
|
||||||
|
|
||||||
|
const timestamp = new Date().toISOString().slice(0, 19).replace(/:/g, '-');
|
||||||
|
const filename = `期初库存_${factory}_${timestamp}.xlsx`;
|
||||||
|
|
||||||
|
XLSX.writeFile(wb, filename);
|
||||||
|
|
||||||
|
console.log(`成功导出 ${exportData.length} 条数据到 ${filename}`);
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error('导出失败:', err);
|
||||||
|
alert('导出失败: ' + err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function showRestoreDialog() {
|
async function showRestoreDialog() {
|
||||||
document.getElementById('restore-modal').style.display = 'flex';
|
document.getElementById('restore-modal').style.display = 'flex';
|
||||||
await loadBackupList();
|
await loadBackupList();
|
||||||
@ -777,6 +996,10 @@
|
|||||||
importExcel,
|
importExcel,
|
||||||
closeRestoreModal,
|
closeRestoreModal,
|
||||||
restoreBackup,
|
restoreBackup,
|
||||||
deleteBackup
|
deleteBackup,
|
||||||
|
toggleExportMenu,
|
||||||
|
exportAll,
|
||||||
|
exportSelected,
|
||||||
|
exportByFactory
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|||||||
@ -91,6 +91,8 @@ Router.register('/shipments/query', async () => {
|
|||||||
<div class="result-item"><span class="label">机种:</span> <span class="badge">${data.platform_name || '未知'}</span></div>
|
<div class="result-item"><span class="label">机种:</span> <span class="badge">${data.platform_name || '未知'}</span></div>
|
||||||
<div class="result-item"><span class="label">出货日期:</span> ${data.date}</div>
|
<div class="result-item"><span class="label">出货日期:</span> ${data.date}</div>
|
||||||
<div class="result-item"><span class="label">箱号:</span> ${data.box}</div>
|
<div class="result-item"><span class="label">箱号:</span> ${data.box}</div>
|
||||||
|
${data.emmc ? `<div class="result-item"><span class="label">EMMC:</span> <span style="font-weight:600;color:var(--primary)">${data.emmc}</span></div>` : ''}
|
||||||
|
${data.ddr ? `<div class="result-item"><span class="label">DDR:</span> <span style="font-weight:600;color:var(--primary)">${data.ddr}</span></div>` : ''}
|
||||||
<div class="result-item"><span class="label">记录时间:</span> ${data.ts}</div>
|
<div class="result-item"><span class="label">记录时间:</span> ${data.ts}</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@ -117,6 +119,8 @@ Router.register('/shipments/query', async () => {
|
|||||||
<div class="result-item"><span class="label">SN/MAC:</span> ${record.sn}</div>
|
<div class="result-item"><span class="label">SN/MAC:</span> ${record.sn}</div>
|
||||||
<div class="result-item"><span class="label">机种:</span> <span class="badge">${record.platform_name || '未知'}</span></div>
|
<div class="result-item"><span class="label">机种:</span> <span class="badge">${record.platform_name || '未知'}</span></div>
|
||||||
<div class="result-item"><span class="label">出货日期:</span> ${record.date}</div>
|
<div class="result-item"><span class="label">出货日期:</span> ${record.date}</div>
|
||||||
|
${record.emmc ? `<div class="result-item"><span class="label">EMMC:</span> <span style="font-weight:600;color:var(--primary)">${record.emmc}</span></div>` : ''}
|
||||||
|
${record.ddr ? `<div class="result-item"><span class="label">DDR:</span> <span style="font-weight:600;color:var(--primary)">${record.ddr}</span></div>` : ''}
|
||||||
<div class="result-item"><span class="label">记录时间:</span> ${record.ts}</div>
|
<div class="result-item"><span class="label">记录时间:</span> ${record.ts}</div>
|
||||||
</div>
|
</div>
|
||||||
`).join('');
|
`).join('');
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
Router.register('/shipments/summary', async () => {
|
Router.register('/shipments/summary', async () => {
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
const queryBtn = document.getElementById('summary-query-btn');
|
const queryBtn = document.getElementById('summary-query-btn');
|
||||||
|
const exportBtn = document.getElementById('summary-export-btn');
|
||||||
const startDateInput = document.getElementById('start-date');
|
const startDateInput = document.getElementById('start-date');
|
||||||
const endDateInput = document.getElementById('end-date');
|
const endDateInput = document.getElementById('end-date');
|
||||||
const resultDiv = document.getElementById('summary-result');
|
const resultDiv = document.getElementById('summary-result');
|
||||||
@ -89,7 +90,63 @@ Router.register('/shipments/summary', async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const performExport = async () => {
|
||||||
|
const startDate = startDateInput?.value;
|
||||||
|
const endDate = endDateInput?.value;
|
||||||
|
|
||||||
|
if (!startDate || !endDate) {
|
||||||
|
alert('请选择开始和结束日期');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
exportBtn.disabled = true;
|
||||||
|
exportBtn.textContent = '导出中...';
|
||||||
|
|
||||||
|
const url = `/api/shipments/export?start=${encodeURIComponent(startDate)}&end=${encodeURIComponent(endDate)}`;
|
||||||
|
|
||||||
|
// 使用 fetch 下载文件
|
||||||
|
const res = await fetch(url, {
|
||||||
|
credentials: 'include'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const error = await res.json();
|
||||||
|
throw new Error(error.error || '导出失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取文件名
|
||||||
|
const contentDisposition = res.headers.get('Content-Disposition');
|
||||||
|
let filename = `发货记录_${startDate}_至_${endDate}.xlsx`;
|
||||||
|
if (contentDisposition) {
|
||||||
|
const matches = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/.exec(contentDisposition);
|
||||||
|
if (matches && matches[1]) {
|
||||||
|
filename = matches[1].replace(/['"]/g, '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 下载文件
|
||||||
|
const blob = await res.blob();
|
||||||
|
const downloadUrl = window.URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = downloadUrl;
|
||||||
|
a.download = filename;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
window.URL.revokeObjectURL(downloadUrl);
|
||||||
|
document.body.removeChild(a);
|
||||||
|
|
||||||
|
alert('导出成功!');
|
||||||
|
} catch (e) {
|
||||||
|
alert('导出失败:' + e.message);
|
||||||
|
} finally {
|
||||||
|
exportBtn.disabled = false;
|
||||||
|
exportBtn.textContent = '导出Excel';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
queryBtn?.addEventListener('click', performQuery);
|
queryBtn?.addEventListener('click', performQuery);
|
||||||
|
exportBtn?.addEventListener('click', performExport);
|
||||||
|
|
||||||
// 自动执行一次查询
|
// 自动执行一次查询
|
||||||
performQuery();
|
performQuery();
|
||||||
@ -163,8 +220,9 @@ Router.register('/shipments/summary', async () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="actions">
|
<div class="actions" style="display:flex;gap:12px">
|
||||||
<button class="btn btn-primary" id="summary-query-btn">查询</button>
|
<button class="btn btn-primary" id="summary-query-btn">查询</button>
|
||||||
|
<button class="btn" id="summary-export-btn" style="background:#10b981;color:white">导出Excel</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,108 @@
|
|||||||
Router.register('/upload/shipments', async () => {
|
Router.register('/upload/shipments', async () => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const manualStatus = document.getElementById('ship-manual-status');
|
const manualStatus = document.getElementById('ship-manual-status');
|
||||||
|
const defaultPlatformNameMap = {pdd: '拼多多', yt: '圆通', tx: '兔喜', mt: '美团', drf: '大润发', std: '标准版'};
|
||||||
|
|
||||||
|
const renderPlatformOptions = (selectEl, list, selectedValue = '') => {
|
||||||
|
if (!selectEl) return;
|
||||||
|
const optsHtml = [
|
||||||
|
'<option value="">请选择机种</option>',
|
||||||
|
...(list || []).map(o => `<option value="${o.value}">${o.label}</option>`)
|
||||||
|
].join('');
|
||||||
|
selectEl.innerHTML = optsHtml;
|
||||||
|
if (selectedValue) selectEl.value = selectedValue;
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadPlatforms = async (selectedManual = '', selectedImport = '') => {
|
||||||
|
try {
|
||||||
|
const res = await API.listShipmentPlatforms();
|
||||||
|
const list = (res && res.list) ? res.list : [];
|
||||||
|
renderPlatformOptions(document.getElementById('ship-manual-platform'), list, selectedManual);
|
||||||
|
renderPlatformOptions(document.getElementById('ship-platform'), list, selectedImport);
|
||||||
|
} catch (e) {
|
||||||
|
// 失败时不阻塞页面,保持当前 options(可能是旧的静态内容)
|
||||||
|
console.warn('加载机种列表失败:', e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const showManageModal = async () => {
|
||||||
|
let list = [];
|
||||||
|
try {
|
||||||
|
const res = await API.listShipmentPlatforms();
|
||||||
|
list = (res && res.list) ? res.list : [];
|
||||||
|
} catch (e) {
|
||||||
|
API.toast('加载机种失败');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const custom = list.filter(x => !x.is_default);
|
||||||
|
const modal = document.createElement('div');
|
||||||
|
modal.id = 'ship-platform-manage-modal';
|
||||||
|
modal.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.5);z-index:1000;display:flex;align-items:center;justify-content:center;padding:20px';
|
||||||
|
modal.innerHTML = `
|
||||||
|
<div style="background:var(--surface);border-radius:12px;width:90%;max-width:520px;max-height:80vh;display:flex;flex-direction:column;box-shadow:0 10px 30px rgba(0,0,0,0.2)">
|
||||||
|
<div style="padding:14px 18px;border-bottom:1px solid var(--border);display:flex;justify-content:space-between;align-items:center">
|
||||||
|
<div style="font-weight:700">机种管理(仅自定义可删除)</div>
|
||||||
|
<button id="ship-platform-manage-close" class="btn btn-secondary" style="padding:4px 10px">关闭</button>
|
||||||
|
</div>
|
||||||
|
<div style="padding:16px;overflow:auto;flex:1">
|
||||||
|
${custom.length ? `
|
||||||
|
<div style="font-size:12px;color:var(--text-2);margin-bottom:10px">共 ${custom.length} 个自定义机种</div>
|
||||||
|
<ul class="list" style="margin:0">
|
||||||
|
${custom.map(x => `
|
||||||
|
<li style="display:flex;justify-content:space-between;align-items:center;gap:10px">
|
||||||
|
<div style="min-width:0">
|
||||||
|
<div style="font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${x.label}</div>
|
||||||
|
<div style="font-size:11px;color:var(--text-2);font-family:monospace">${x.value}</div>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-secondary ship-platform-delete" data-code="${x.value}" style="background:var(--danger);color:white">删除</button>
|
||||||
|
</li>
|
||||||
|
`).join('')}
|
||||||
|
</ul>
|
||||||
|
` : `
|
||||||
|
<div style="text-align:center;padding:30px 0;color:var(--text-2)">暂无自定义机种</div>
|
||||||
|
`}
|
||||||
|
</div>
|
||||||
|
<div style="padding:12px 16px;border-top:1px solid var(--border);display:flex;justify-content:flex-end;gap:10px">
|
||||||
|
<button class="btn" id="ship-platform-manage-add">新增机种</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
document.body.appendChild(modal);
|
||||||
|
|
||||||
|
const close = () => modal.remove();
|
||||||
|
modal.addEventListener('click', (e) => { if (e.target === modal) close(); });
|
||||||
|
modal.querySelector('#ship-platform-manage-close')?.addEventListener('click', close);
|
||||||
|
|
||||||
|
modal.querySelector('#ship-platform-manage-add')?.addEventListener('click', async () => {
|
||||||
|
const name = (prompt('请输入自定义机种名称') || '').trim();
|
||||||
|
if (!name) return;
|
||||||
|
try {
|
||||||
|
const added = await API.addShipmentPlatform(name);
|
||||||
|
if (added && added.ok) {
|
||||||
|
API.toast('已添加机种:' + (added.label || name));
|
||||||
|
close();
|
||||||
|
await loadPlatforms(added.value, added.value);
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
});
|
||||||
|
|
||||||
|
modal.querySelectorAll('.ship-platform-delete').forEach(btn => {
|
||||||
|
btn.addEventListener('click', async () => {
|
||||||
|
const code = btn.dataset.code;
|
||||||
|
const label = btn.closest('li')?.querySelector('div div')?.textContent || code;
|
||||||
|
if (!confirm(`确定删除自定义机种:${label}?\n\n删除后下拉将不再显示该机种。`)) return;
|
||||||
|
try {
|
||||||
|
const r = await API.deleteShipmentPlatform(code);
|
||||||
|
if (r && r.ok) {
|
||||||
|
API.toast('删除成功');
|
||||||
|
close();
|
||||||
|
await loadPlatforms();
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// 设置默认日期为今天
|
// 设置默认日期为今天
|
||||||
const dateInput = document.getElementById('ship-date');
|
const dateInput = document.getElementById('ship-date');
|
||||||
@ -8,6 +110,11 @@ Router.register('/upload/shipments', async () => {
|
|||||||
const today = new Date().toISOString().split('T')[0];
|
const today = new Date().toISOString().split('T')[0];
|
||||||
dateInput.value = today;
|
dateInput.value = today;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 动态加载机种下拉,并支持自定义新增
|
||||||
|
loadPlatforms();
|
||||||
|
|
||||||
|
document.getElementById('ship-manage-platforms')?.addEventListener('click', showManageModal);
|
||||||
|
|
||||||
// 手动录入提交
|
// 手动录入提交
|
||||||
const btn = document.getElementById('ship-upload');
|
const btn = document.getElementById('ship-upload');
|
||||||
@ -54,7 +161,7 @@ Router.register('/upload/shipments', async () => {
|
|||||||
|
|
||||||
await API.uploadShipments(payload);
|
await API.uploadShipments(payload);
|
||||||
|
|
||||||
const platformName = {pdd: '拼多多', yt: '圆通', tx: '兔喜', mt: '美团', drf: '大润发', std: '标准版'}[platform] || platform;
|
const platformName = defaultPlatformNameMap[platform] || platform;
|
||||||
manualStatus.textContent = `✓ 录入成功!机种:${platformName},数量:${qty}`;
|
manualStatus.textContent = `✓ 录入成功!机种:${platformName},数量:${qty}`;
|
||||||
manualStatus.className = 'success';
|
manualStatus.className = 'success';
|
||||||
|
|
||||||
@ -155,7 +262,7 @@ Router.register('/upload/shipments', async () => {
|
|||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|
||||||
if (data.ok) {
|
if (data.ok) {
|
||||||
const platformName = {pdd: '拼多多', yt: '圆通', tx: '兔喜', mt: '美团', drf: '大润发', std: '标准版'}[platform] || platform;
|
const platformName = defaultPlatformNameMap[platform] || platform;
|
||||||
fileStatus.textContent = `✓ 上传成功!机种:${platformName},共导入${data.count}个箱次,${data.total_qty}个SN`;
|
fileStatus.textContent = `✓ 上传成功!机种:${platformName},共导入${data.count}个箱次,${data.total_qty}个SN`;
|
||||||
fileStatus.className = 'success';
|
fileStatus.className = 'success';
|
||||||
fileInput.value = '';
|
fileInput.value = '';
|
||||||
@ -234,6 +341,9 @@ Router.register('/upload/shipments', async () => {
|
|||||||
<div id="shipments-page">
|
<div id="shipments-page">
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<h1>发货记录</h1>
|
<h1>发货记录</h1>
|
||||||
|
<div class="page-actions">
|
||||||
|
<button class="btn btn-secondary" id="ship-manage-platforms" type="button">机种管理</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="content-area">
|
<div class="content-area">
|
||||||
|
|||||||
368
server/app.py
368
server/app.py
@ -19,6 +19,16 @@ BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
|||||||
DB_PATH = os.path.join(BASE_DIR, 'data.db')
|
DB_PATH = os.path.join(BASE_DIR, 'data.db')
|
||||||
FRONTEND_DIR = os.path.join(os.path.dirname(BASE_DIR), 'frontend')
|
FRONTEND_DIR = os.path.join(os.path.dirname(BASE_DIR), 'frontend')
|
||||||
|
|
||||||
|
# 发货机种(默认选项 + 自定义表)
|
||||||
|
DEFAULT_SHIPMENT_PLATFORMS = {
|
||||||
|
'pdd': '拼多多',
|
||||||
|
'yt': '圆通',
|
||||||
|
'tx': '兔喜',
|
||||||
|
'mt': '美团',
|
||||||
|
'drf': '大润发',
|
||||||
|
'std': '标准版',
|
||||||
|
}
|
||||||
|
|
||||||
app = Flask(__name__, static_folder=FRONTEND_DIR, static_url_path='')
|
app = Flask(__name__, static_folder=FRONTEND_DIR, static_url_path='')
|
||||||
app.config['SECRET_KEY'] = os.environ.get('APP_SECRET', 'change-me')
|
app.config['SECRET_KEY'] = os.environ.get('APP_SECRET', 'change-me')
|
||||||
app.config['MAX_CONTENT_LENGTH'] = 50 * 1024 * 1024 # 限制上传文件大小为50MB
|
app.config['MAX_CONTENT_LENGTH'] = 50 * 1024 * 1024 # 限制上传文件大小为50MB
|
||||||
@ -110,6 +120,25 @@ def init_db():
|
|||||||
date TEXT,
|
date TEXT,
|
||||||
qty INTEGER,
|
qty INTEGER,
|
||||||
receiver TEXT,
|
receiver TEXT,
|
||||||
|
ts TEXT,
|
||||||
|
emmc TEXT,
|
||||||
|
ddr TEXT
|
||||||
|
)''')
|
||||||
|
# 为已存在的表添加 emmc 和 ddr 列(如果不存在)
|
||||||
|
try:
|
||||||
|
c.execute('ALTER TABLE shipments ADD COLUMN emmc TEXT')
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
c.execute('ALTER TABLE shipments ADD COLUMN ddr TEXT')
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 发货机种类型(自定义项)
|
||||||
|
c.execute('''CREATE TABLE IF NOT EXISTS shipment_platforms(
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
code TEXT UNIQUE NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
ts TEXT
|
ts TEXT
|
||||||
)''')
|
)''')
|
||||||
c.execute('''CREATE TABLE IF NOT EXISTS devices(
|
c.execute('''CREATE TABLE IF NOT EXISTS devices(
|
||||||
@ -917,6 +946,49 @@ def upload_avatar():
|
|||||||
c.execute('UPDATE users SET avatar=? WHERE id=?', (avatar_url, uid))
|
c.execute('UPDATE users SET avatar=? WHERE id=?', (avatar_url, uid))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def _make_platform_code():
|
||||||
|
"""生成一个短且稳定可用的机种 code(避免与默认 code 冲突)"""
|
||||||
|
# 12位随机 hex 足够用且不会太长
|
||||||
|
import secrets
|
||||||
|
return 'cust_' + secrets.token_hex(6)
|
||||||
|
|
||||||
|
|
||||||
|
def list_shipment_platforms():
|
||||||
|
"""返回发货机种选项(包含默认 + 自定义)"""
|
||||||
|
opts = [{'code': k, 'name': v, 'is_default': True} for k, v in DEFAULT_SHIPMENT_PLATFORMS.items()]
|
||||||
|
try:
|
||||||
|
conn = get_db()
|
||||||
|
c = conn.cursor()
|
||||||
|
c.execute('SELECT code, name FROM shipment_platforms ORDER BY id DESC')
|
||||||
|
rows = [dict(r) for r in c.fetchall()]
|
||||||
|
conn.close()
|
||||||
|
# 自定义排在后面,防止覆盖默认同 code(理论上不会发生)
|
||||||
|
for r in rows:
|
||||||
|
opts.append({'code': r['code'], 'name': r['name'], 'is_default': False})
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return opts
|
||||||
|
|
||||||
|
|
||||||
|
def shipment_platform_display_name(code):
|
||||||
|
"""将 platform code 映射为展示名称(默认映射 + 自定义表)"""
|
||||||
|
if not code:
|
||||||
|
return ''
|
||||||
|
if code in DEFAULT_SHIPMENT_PLATFORMS:
|
||||||
|
return DEFAULT_SHIPMENT_PLATFORMS[code]
|
||||||
|
try:
|
||||||
|
conn = get_db()
|
||||||
|
c = conn.cursor()
|
||||||
|
c.execute('SELECT name FROM shipment_platforms WHERE code=?', (code,))
|
||||||
|
row = c.fetchone()
|
||||||
|
conn.close()
|
||||||
|
if row and row['name']:
|
||||||
|
return row['name']
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return code
|
||||||
|
|
||||||
log('upload_avatar', f'上传头像: {new_filename}')
|
log('upload_avatar', f'上传头像: {new_filename}')
|
||||||
return jsonify({'ok': True, 'avatar_url': avatar_url})
|
return jsonify({'ok': True, 'avatar_url': avatar_url})
|
||||||
@ -2688,7 +2760,7 @@ def upload_shipments():
|
|||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
platform_name = {'pdd': '拼多多', 'yt': '圆通', 'tx': '兔喜', 'mt': '美团', 'drf': '大润发', 'std': '标准版'}.get(platform, platform)
|
platform_name = shipment_platform_display_name(platform)
|
||||||
log_data = {'date': date, 'qty': qty, 'to': to, 'platform': platform}
|
log_data = {'date': date, 'qty': qty, 'to': to, 'platform': platform}
|
||||||
if box_no:
|
if box_no:
|
||||||
log_data['box_no'] = box_no
|
log_data['box_no'] = box_no
|
||||||
@ -2698,6 +2770,91 @@ def upload_shipments():
|
|||||||
return jsonify({'ok': True})
|
return jsonify({'ok': True})
|
||||||
|
|
||||||
|
|
||||||
|
@app.get('/api/shipments/platforms')
|
||||||
|
@require_login
|
||||||
|
def get_shipment_platforms():
|
||||||
|
"""获取发货机种选项(默认 + 自定义)"""
|
||||||
|
opts = list_shipment_platforms()
|
||||||
|
# 前端需要统一格式
|
||||||
|
return jsonify({'list': [{'value': o['code'], 'label': o['name'], 'is_default': o['is_default']} for o in opts]})
|
||||||
|
|
||||||
|
|
||||||
|
@app.post('/api/shipments/platforms')
|
||||||
|
@require_login
|
||||||
|
@require_any_role('admin','superadmin')
|
||||||
|
def add_shipment_platform():
|
||||||
|
"""添加自定义发货机种"""
|
||||||
|
data = request.get_json() or {}
|
||||||
|
name = (data.get('name') or '').strip()
|
||||||
|
if not name:
|
||||||
|
return jsonify({'error': 'invalid payload'}), 400
|
||||||
|
if len(name) > 50:
|
||||||
|
return jsonify({'error': '机种名称过长'}), 400
|
||||||
|
|
||||||
|
# 若与默认名称完全一致,直接返回默认项
|
||||||
|
for code, label in DEFAULT_SHIPMENT_PLATFORMS.items():
|
||||||
|
if label == name:
|
||||||
|
return jsonify({'ok': True, 'value': code, 'label': label, 'is_default': True})
|
||||||
|
|
||||||
|
conn = get_db()
|
||||||
|
c = conn.cursor()
|
||||||
|
try:
|
||||||
|
# 若已存在同名自定义项,直接返回已有 code
|
||||||
|
c.execute('SELECT code, name FROM shipment_platforms WHERE name=?', (name,))
|
||||||
|
row = c.fetchone()
|
||||||
|
if row:
|
||||||
|
conn.close()
|
||||||
|
return jsonify({'ok': True, 'value': row['code'], 'label': row['name'], 'is_default': False})
|
||||||
|
|
||||||
|
code = _make_platform_code()
|
||||||
|
c.execute('INSERT INTO shipment_platforms(code, name, ts) VALUES(?,?,?)', (code, name, get_beijing_time()))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
log('add_shipment_platform', json.dumps({'code': code, 'name': name}))
|
||||||
|
notify_superadmin('新增发货机种', f"机种: {name}({code})")
|
||||||
|
return jsonify({'ok': True, 'value': code, 'label': name, 'is_default': False})
|
||||||
|
except Exception as e:
|
||||||
|
try:
|
||||||
|
conn.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@app.delete('/api/shipments/platforms/<code>')
|
||||||
|
@require_login
|
||||||
|
@require_any_role('admin','superadmin')
|
||||||
|
def delete_shipment_platform(code):
|
||||||
|
"""删除自定义发货机种(默认机种不可删除)"""
|
||||||
|
code = (code or '').strip()
|
||||||
|
if not code:
|
||||||
|
return jsonify({'error': 'invalid payload'}), 400
|
||||||
|
if code in DEFAULT_SHIPMENT_PLATFORMS:
|
||||||
|
return jsonify({'error': '默认机种不可删除'}), 400
|
||||||
|
|
||||||
|
conn = get_db()
|
||||||
|
c = conn.cursor()
|
||||||
|
try:
|
||||||
|
c.execute('SELECT id, name FROM shipment_platforms WHERE code=?', (code,))
|
||||||
|
row = c.fetchone()
|
||||||
|
if not row:
|
||||||
|
conn.close()
|
||||||
|
return jsonify({'ok': True, 'deleted': 0})
|
||||||
|
|
||||||
|
c.execute('DELETE FROM shipment_platforms WHERE code=?', (code,))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
log('delete_shipment_platform', json.dumps({'code': code, 'name': row['name']}))
|
||||||
|
notify_superadmin('删除发货机种', f"机种: {row['name']}({code})")
|
||||||
|
return jsonify({'ok': True, 'deleted': 1})
|
||||||
|
except Exception as e:
|
||||||
|
try:
|
||||||
|
conn.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
# collect
|
# collect
|
||||||
@app.get('/api/collect/devices')
|
@app.get('/api/collect/devices')
|
||||||
@require_login
|
@require_login
|
||||||
@ -3764,7 +3921,7 @@ def query_shipment_by_sn():
|
|||||||
# 解析 JSON 数据
|
# 解析 JSON 数据
|
||||||
shipment_info = json.loads(result)
|
shipment_info = json.loads(result)
|
||||||
platform = shipment_info.get('platform', 'pdd') # 默认拼多多
|
platform = shipment_info.get('platform', 'pdd') # 默认拼多多
|
||||||
platform_name = {'pdd': '拼多多', 'yt': '圆通', 'tx': '兔喜', 'mt': '美团', 'drf': '大润发', 'std': '标准版'}.get(platform, platform)
|
platform_name = shipment_platform_display_name(platform)
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'found': True,
|
'found': True,
|
||||||
'sn': sn,
|
'sn': sn,
|
||||||
@ -3772,7 +3929,9 @@ def query_shipment_by_sn():
|
|||||||
'box': shipment_info.get('box'),
|
'box': shipment_info.get('box'),
|
||||||
'platform': platform,
|
'platform': platform,
|
||||||
'platform_name': platform_name,
|
'platform_name': platform_name,
|
||||||
'ts': shipment_info.get('ts')
|
'ts': shipment_info.get('ts'),
|
||||||
|
'emmc': shipment_info.get('emmc', ''),
|
||||||
|
'ddr': shipment_info.get('ddr', '')
|
||||||
})
|
})
|
||||||
else:
|
else:
|
||||||
return jsonify({
|
return jsonify({
|
||||||
@ -3809,14 +3968,16 @@ def query_shipment_by_box():
|
|||||||
shipment_info = json.loads(data)
|
shipment_info = json.loads(data)
|
||||||
if shipment_info.get('box') == box_no:
|
if shipment_info.get('box') == box_no:
|
||||||
platform = shipment_info.get('platform', 'pdd')
|
platform = shipment_info.get('platform', 'pdd')
|
||||||
platform_name = {'pdd': '拼多多', 'yt': '圆通', 'tx': '兔喜', 'mt': '美团', 'drf': '大润发', 'std': '标准版'}.get(platform, platform)
|
platform_name = shipment_platform_display_name(platform)
|
||||||
matched_records.append({
|
matched_records.append({
|
||||||
'sn': sn.decode('utf-8') if isinstance(sn, bytes) else sn,
|
'sn': sn.decode('utf-8') if isinstance(sn, bytes) else sn,
|
||||||
'date': shipment_info.get('date'),
|
'date': shipment_info.get('date'),
|
||||||
'box': shipment_info.get('box'),
|
'box': shipment_info.get('box'),
|
||||||
'platform': platform,
|
'platform': platform,
|
||||||
'platform_name': platform_name,
|
'platform_name': platform_name,
|
||||||
'ts': shipment_info.get('ts')
|
'ts': shipment_info.get('ts'),
|
||||||
|
'emmc': shipment_info.get('emmc', ''),
|
||||||
|
'ddr': shipment_info.get('ddr', '')
|
||||||
})
|
})
|
||||||
except:
|
except:
|
||||||
continue
|
continue
|
||||||
@ -4022,6 +4183,130 @@ def clear_shipments_redis():
|
|||||||
return jsonify({'error': f'清空失败:{str(e)}'}), 500
|
return jsonify({'error': f'清空失败:{str(e)}'}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@app.get('/api/shipments/export')
|
||||||
|
@require_login
|
||||||
|
def export_shipments():
|
||||||
|
"""导出发货记录为Excel文件"""
|
||||||
|
try:
|
||||||
|
start_date = request.args.get('start')
|
||||||
|
end_date = request.args.get('end')
|
||||||
|
|
||||||
|
if not start_date or not end_date:
|
||||||
|
return jsonify({'error': '请提供开始和结束日期'}), 400
|
||||||
|
|
||||||
|
import openpyxl
|
||||||
|
from openpyxl.styles import Font, Alignment, PatternFill
|
||||||
|
import json
|
||||||
|
|
||||||
|
conn = get_db()
|
||||||
|
c = conn.cursor()
|
||||||
|
|
||||||
|
# 查询指定日期范围内的发货记录(按箱号分组)
|
||||||
|
c.execute('''
|
||||||
|
SELECT id, date, receiver, emmc, ddr, ts
|
||||||
|
FROM shipments
|
||||||
|
WHERE date >= ? AND date <= ?
|
||||||
|
ORDER BY date, id
|
||||||
|
''', (start_date, end_date))
|
||||||
|
|
||||||
|
records = [dict(r) for r in c.fetchall()]
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
if not records:
|
||||||
|
return jsonify({'error': '该时间段内没有发货记录'}), 404
|
||||||
|
|
||||||
|
# 从 Redis 获取 SN 数据
|
||||||
|
try:
|
||||||
|
r = get_redis()
|
||||||
|
redis_key = 'shipment_sn_mapping'
|
||||||
|
all_sn_data = r.hgetall(redis_key)
|
||||||
|
except:
|
||||||
|
all_sn_data = {}
|
||||||
|
|
||||||
|
# 创建 Excel 工作簿
|
||||||
|
wb = openpyxl.Workbook()
|
||||||
|
ws = wb.active
|
||||||
|
ws.title = '发货记录'
|
||||||
|
|
||||||
|
# 设置表头样式
|
||||||
|
header_fill = PatternFill(start_color='4472C4', end_color='4472C4', fill_type='solid')
|
||||||
|
header_font = Font(bold=True, color='FFFFFF')
|
||||||
|
header_alignment = Alignment(horizontal='center', vertical='center')
|
||||||
|
|
||||||
|
# 创建表头
|
||||||
|
headers = ['出货日期', '箱号'] + [f'SN{i}' for i in range(1, 21)] + ['EMMC', 'DDR']
|
||||||
|
for col_idx, header in enumerate(headers, 1):
|
||||||
|
cell = ws.cell(row=1, column=col_idx, value=header)
|
||||||
|
cell.fill = header_fill
|
||||||
|
cell.font = header_font
|
||||||
|
cell.alignment = header_alignment
|
||||||
|
|
||||||
|
# 填充数据
|
||||||
|
row_idx = 2
|
||||||
|
for record in records:
|
||||||
|
# 提取箱号
|
||||||
|
receiver = record['receiver'] or ''
|
||||||
|
box_no = receiver.replace('箱号:', '').strip()
|
||||||
|
|
||||||
|
# 从 Redis 获取该箱号的所有 SN
|
||||||
|
sns = []
|
||||||
|
for sn, data in all_sn_data.items():
|
||||||
|
try:
|
||||||
|
sn_str = sn.decode('utf-8') if isinstance(sn, bytes) else sn
|
||||||
|
data_str = data.decode('utf-8') if isinstance(data, bytes) else data
|
||||||
|
info = json.loads(data_str)
|
||||||
|
if info.get('box') == box_no:
|
||||||
|
sns.append(sn_str)
|
||||||
|
except:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 写入数据行
|
||||||
|
ws.cell(row=row_idx, column=1, value=record['date'])
|
||||||
|
ws.cell(row=row_idx, column=2, value=box_no)
|
||||||
|
|
||||||
|
# 写入 SN(最多20个)
|
||||||
|
for i, sn in enumerate(sns[:20], 3):
|
||||||
|
ws.cell(row=row_idx, column=i, value=sn)
|
||||||
|
|
||||||
|
# 写入 EMMC 和 DDR
|
||||||
|
ws.cell(row=row_idx, column=23, value=record.get('emmc', ''))
|
||||||
|
ws.cell(row=row_idx, column=24, value=record.get('ddr', ''))
|
||||||
|
|
||||||
|
row_idx += 1
|
||||||
|
|
||||||
|
# 调整列宽
|
||||||
|
ws.column_dimensions['A'].width = 12 # 出货日期
|
||||||
|
ws.column_dimensions['B'].width = 20 # 箱号
|
||||||
|
for i in range(3, 23): # SN1-SN20
|
||||||
|
ws.column_dimensions[openpyxl.utils.get_column_letter(i)].width = 25
|
||||||
|
ws.column_dimensions['W'].width = 10 # EMMC
|
||||||
|
ws.column_dimensions['X'].width = 10 # DDR
|
||||||
|
|
||||||
|
# 保存到内存
|
||||||
|
from io import BytesIO
|
||||||
|
output = BytesIO()
|
||||||
|
wb.save(output)
|
||||||
|
output.seek(0)
|
||||||
|
|
||||||
|
# 生成文件名
|
||||||
|
filename = f'发货记录_{start_date}_至_{end_date}.xlsx'
|
||||||
|
|
||||||
|
log('export_shipments', f'start={start_date}, end={end_date}, records={len(records)}')
|
||||||
|
|
||||||
|
return send_file(
|
||||||
|
output,
|
||||||
|
mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||||
|
as_attachment=True,
|
||||||
|
download_name=filename
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
log('export_shipments_error', str(e))
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
return jsonify({'error': f'导出失败:{str(e)}'}), 500
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/upload/shipments-file', methods=['POST'])
|
@app.route('/api/upload/shipments-file', methods=['POST'])
|
||||||
@require_login
|
@require_login
|
||||||
@require_any_role('admin','superadmin')
|
@require_any_role('admin','superadmin')
|
||||||
@ -4033,8 +4318,9 @@ def upload_shipments_file():
|
|||||||
if not f:
|
if not f:
|
||||||
return jsonify({'error': '请选择文件'}), 400
|
return jsonify({'error': '请选择文件'}), 400
|
||||||
|
|
||||||
if not platform or platform not in ['pdd', 'yt', 'tx']:
|
allowed = set([o['code'] for o in list_shipment_platforms()])
|
||||||
return jsonify({'error': '请选择机种(拼多多/圆通/兔喜)'}), 400
|
if not platform or platform not in allowed:
|
||||||
|
return jsonify({'error': '请选择有效的机种类型(可在下拉中新增自定义机种)'}), 400
|
||||||
|
|
||||||
name = secure_filename(f.filename or '')
|
name = secure_filename(f.filename or '')
|
||||||
ext = (name.split('.')[-1] or '').lower()
|
ext = (name.split('.')[-1] or '').lower()
|
||||||
@ -4056,6 +4342,8 @@ def upload_shipments_file():
|
|||||||
# 找到列索引
|
# 找到列索引
|
||||||
date_idx = next((i for i, h in enumerate(header) if '出货日期' in h or '发货日期' in h or 'date' in header_lower[i]), None)
|
date_idx = next((i for i, h in enumerate(header) if '出货日期' in h or '发货日期' in h or 'date' in header_lower[i]), None)
|
||||||
box_idx = next((i for i, h in enumerate(header) if '箱号' in h or 'box' in header_lower[i]), None)
|
box_idx = next((i for i, h in enumerate(header) if '箱号' in h or 'box' in header_lower[i]), None)
|
||||||
|
emmc_idx = next((i for i, h in enumerate(header) if 'emmc' in h.lower()), None)
|
||||||
|
ddr_idx = next((i for i, h in enumerate(header) if 'ddr' in h.lower()), None)
|
||||||
|
|
||||||
if date_idx is None or box_idx is None:
|
if date_idx is None or box_idx is None:
|
||||||
return jsonify({'error': '缺少必需的列'}), 400
|
return jsonify({'error': '缺少必需的列'}), 400
|
||||||
@ -4064,8 +4352,10 @@ def upload_shipments_file():
|
|||||||
sn_cols = [(i, h) for i, h in enumerate(header) if h.startswith('SN') and h[2:].isdigit()]
|
sn_cols = [(i, h) for i, h in enumerate(header) if h.startswith('SN') and h[2:].isdigit()]
|
||||||
sn_indices = sorted(sn_cols, key=lambda x: int(x[1][2:])) # 按 SN 后面的数字排序
|
sn_indices = sorted(sn_cols, key=lambda x: int(x[1][2:])) # 按 SN 后面的数字排序
|
||||||
|
|
||||||
# 记录上一个有效的日期(用于处理合并单元格)
|
# 记录上一个有效的日期、EMMC、DDR(用于处理合并单元格)
|
||||||
last_valid_date = None
|
last_valid_date = None
|
||||||
|
last_valid_emmc = None
|
||||||
|
last_valid_ddr = None
|
||||||
|
|
||||||
for line in lines[1:]:
|
for line in lines[1:]:
|
||||||
parts = [p.strip() for p in line.split(',')]
|
parts = [p.strip() for p in line.split(',')]
|
||||||
@ -4085,6 +4375,22 @@ def upload_shipments_file():
|
|||||||
# 处理箱号
|
# 处理箱号
|
||||||
box = parts[box_idx] if box_idx < len(parts) and parts[box_idx] else ''
|
box = parts[box_idx] if box_idx < len(parts) and parts[box_idx] else ''
|
||||||
|
|
||||||
|
# 处理 EMMC(支持合并单元格)
|
||||||
|
current_emmc = parts[emmc_idx] if emmc_idx is not None and emmc_idx < len(parts) and parts[emmc_idx] else ''
|
||||||
|
if current_emmc:
|
||||||
|
last_valid_emmc = current_emmc
|
||||||
|
emmc = current_emmc
|
||||||
|
else:
|
||||||
|
emmc = last_valid_emmc or ''
|
||||||
|
|
||||||
|
# 处理 DDR(支持合并单元格)
|
||||||
|
current_ddr = parts[ddr_idx] if ddr_idx is not None and ddr_idx < len(parts) and parts[ddr_idx] else ''
|
||||||
|
if current_ddr:
|
||||||
|
last_valid_ddr = current_ddr
|
||||||
|
ddr = current_ddr
|
||||||
|
else:
|
||||||
|
ddr = last_valid_ddr or ''
|
||||||
|
|
||||||
# 如果没有日期或箱号,跳过这行
|
# 如果没有日期或箱号,跳过这行
|
||||||
if not date or not box:
|
if not date or not box:
|
||||||
continue
|
continue
|
||||||
@ -4101,7 +4407,9 @@ def upload_shipments_file():
|
|||||||
'date': date,
|
'date': date,
|
||||||
'box': box,
|
'box': box,
|
||||||
'sns': sns,
|
'sns': sns,
|
||||||
'qty': len(sns)
|
'qty': len(sns),
|
||||||
|
'emmc': emmc,
|
||||||
|
'ddr': ddr
|
||||||
})
|
})
|
||||||
else:
|
else:
|
||||||
import openpyxl
|
import openpyxl
|
||||||
@ -4120,6 +4428,8 @@ def upload_shipments_file():
|
|||||||
# 找到列索引
|
# 找到列索引
|
||||||
date_idx = next((i for i, h in enumerate(header) if '出货日期' in h or '发货日期' in h or 'date' in header_lower[i]), None)
|
date_idx = next((i for i, h in enumerate(header) if '出货日期' in h or '发货日期' in h or 'date' in header_lower[i]), None)
|
||||||
box_idx = next((i for i, h in enumerate(header) if '箱号' in h or 'box' in header_lower[i]), None)
|
box_idx = next((i for i, h in enumerate(header) if '箱号' in h or 'box' in header_lower[i]), None)
|
||||||
|
emmc_idx = next((i for i, h in enumerate(header) if 'emmc' in h.lower()), None)
|
||||||
|
ddr_idx = next((i for i, h in enumerate(header) if 'ddr' in h.lower()), None)
|
||||||
|
|
||||||
if date_idx is None or box_idx is None:
|
if date_idx is None or box_idx is None:
|
||||||
wb.close()
|
wb.close()
|
||||||
@ -4129,8 +4439,10 @@ def upload_shipments_file():
|
|||||||
sn_cols = [(i, h) for i, h in enumerate(header) if h.startswith('SN') and h[2:].isdigit()]
|
sn_cols = [(i, h) for i, h in enumerate(header) if h.startswith('SN') and h[2:].isdigit()]
|
||||||
sn_indices = sorted(sn_cols, key=lambda x: int(x[1][2:])) # 按 SN 后面的数字排序
|
sn_indices = sorted(sn_cols, key=lambda x: int(x[1][2:])) # 按 SN 后面的数字排序
|
||||||
|
|
||||||
# 记录上一个有效的日期(用于处理合并单元格)
|
# 记录上一个有效的日期、EMMC、DDR(用于处理合并单元格)
|
||||||
last_valid_date = None
|
last_valid_date = None
|
||||||
|
last_valid_emmc = None
|
||||||
|
last_valid_ddr = None
|
||||||
|
|
||||||
# 读取数据行
|
# 读取数据行
|
||||||
for row in ws.iter_rows(min_row=2, values_only=True):
|
for row in ws.iter_rows(min_row=2, values_only=True):
|
||||||
@ -4168,6 +4480,24 @@ def upload_shipments_file():
|
|||||||
# 处理箱号
|
# 处理箱号
|
||||||
box = str(row[box_idx]).strip() if box_idx < len(row) and row[box_idx] else ''
|
box = str(row[box_idx]).strip() if box_idx < len(row) and row[box_idx] else ''
|
||||||
|
|
||||||
|
# 处理 EMMC(支持合并单元格)
|
||||||
|
emmc_value = row[emmc_idx] if emmc_idx is not None and emmc_idx < len(row) else None
|
||||||
|
current_emmc = str(emmc_value).strip() if emmc_value and str(emmc_value) != 'None' else ''
|
||||||
|
if current_emmc:
|
||||||
|
last_valid_emmc = current_emmc
|
||||||
|
emmc = current_emmc
|
||||||
|
else:
|
||||||
|
emmc = last_valid_emmc or ''
|
||||||
|
|
||||||
|
# 处理 DDR(支持合并单元格)
|
||||||
|
ddr_value = row[ddr_idx] if ddr_idx is not None and ddr_idx < len(row) else None
|
||||||
|
current_ddr = str(ddr_value).strip() if ddr_value and str(ddr_value) != 'None' else ''
|
||||||
|
if current_ddr:
|
||||||
|
last_valid_ddr = current_ddr
|
||||||
|
ddr = current_ddr
|
||||||
|
else:
|
||||||
|
ddr = last_valid_ddr or ''
|
||||||
|
|
||||||
# 如果没有日期或箱号,跳过这行
|
# 如果没有日期或箱号,跳过这行
|
||||||
if not date or not box or box == 'None':
|
if not date or not box or box == 'None':
|
||||||
continue
|
continue
|
||||||
@ -4186,7 +4516,9 @@ def upload_shipments_file():
|
|||||||
'date': date,
|
'date': date,
|
||||||
'box': box,
|
'box': box,
|
||||||
'sns': sns,
|
'sns': sns,
|
||||||
'qty': len(sns)
|
'qty': len(sns),
|
||||||
|
'emmc': emmc,
|
||||||
|
'ddr': ddr
|
||||||
})
|
})
|
||||||
|
|
||||||
wb.close()
|
wb.close()
|
||||||
@ -4202,8 +4534,10 @@ def upload_shipments_file():
|
|||||||
total_qty = 0
|
total_qty = 0
|
||||||
for r in rows:
|
for r in rows:
|
||||||
receiver_info = f"箱号:{r['box']}"
|
receiver_info = f"箱号:{r['box']}"
|
||||||
c.execute('INSERT INTO shipments(date, qty, receiver, ts) VALUES(?,?,?,?)',
|
emmc_value = r.get('emmc', '')
|
||||||
(r['date'], r['qty'], receiver_info, now))
|
ddr_value = r.get('ddr', '')
|
||||||
|
c.execute('INSERT INTO shipments(date, qty, receiver, ts, emmc, ddr) VALUES(?,?,?,?,?,?)',
|
||||||
|
(r['date'], r['qty'], receiver_info, now, emmc_value, ddr_value))
|
||||||
total_qty += r['qty']
|
total_qty += r['qty']
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
@ -4224,14 +4558,18 @@ def upload_shipments_file():
|
|||||||
sns = row_data['sns']
|
sns = row_data['sns']
|
||||||
|
|
||||||
# 为每个 SN/MAC 创建映射记录
|
# 为每个 SN/MAC 创建映射记录
|
||||||
|
emmc = row_data.get('emmc', '')
|
||||||
|
ddr = row_data.get('ddr', '')
|
||||||
for sn in sns:
|
for sn in sns:
|
||||||
if sn: # 确保 SN 不为空
|
if sn: # 确保 SN 不为空
|
||||||
# 存储格式: MAC -> JSON(date, box, platform, timestamp)
|
# 存储格式: MAC -> JSON(date, box, platform, timestamp, emmc, ddr)
|
||||||
shipment_info = json.dumps({
|
shipment_info = json.dumps({
|
||||||
'date': date,
|
'date': date,
|
||||||
'box': box,
|
'box': box,
|
||||||
'platform': platform,
|
'platform': platform,
|
||||||
'ts': now
|
'ts': now,
|
||||||
|
'emmc': emmc,
|
||||||
|
'ddr': ddr
|
||||||
}, ensure_ascii=False)
|
}, ensure_ascii=False)
|
||||||
pipe.hset(redis_key, sn, shipment_info)
|
pipe.hset(redis_key, sn, shipment_info)
|
||||||
redis_count += 1
|
redis_count += 1
|
||||||
|
|||||||
BIN
server/data.db
BIN
server/data.db
Binary file not shown.
Loading…
Reference in New Issue
Block a user