434 lines
16 KiB
JavaScript
Executable File
434 lines
16 KiB
JavaScript
Executable File
Router.register('/upload/shipments', async () => {
|
||
setTimeout(() => {
|
||
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');
|
||
if (dateInput && !dateInput.value) {
|
||
const today = new Date().toISOString().split('T')[0];
|
||
dateInput.value = today;
|
||
}
|
||
|
||
// 动态加载机种下拉,并支持自定义新增
|
||
loadPlatforms();
|
||
|
||
document.getElementById('ship-manage-platforms')?.addEventListener('click', showManageModal);
|
||
|
||
// 手动录入提交
|
||
const btn = document.getElementById('ship-upload');
|
||
btn?.addEventListener('click', async () => {
|
||
const date = document.getElementById('ship-date').value;
|
||
const qty = parseInt(document.getElementById('ship-qty').value || '0', 10);
|
||
const to = document.getElementById('ship-to').value;
|
||
const platform = document.getElementById('ship-manual-platform').value;
|
||
const boxNo = document.getElementById('ship-box-no').value.trim();
|
||
|
||
// 验证必填字段
|
||
if (!date) {
|
||
manualStatus.textContent = '✗ 请选择发货日期';
|
||
manualStatus.className = 'error';
|
||
return;
|
||
}
|
||
|
||
if (!platform) {
|
||
manualStatus.textContent = '✗ 请选择机种类型';
|
||
manualStatus.className = 'error';
|
||
return;
|
||
}
|
||
|
||
if (!to) {
|
||
manualStatus.textContent = '✗ 请输入接收方';
|
||
manualStatus.className = 'error';
|
||
return;
|
||
}
|
||
|
||
if (qty <= 0) {
|
||
manualStatus.textContent = '✗ 数量必须大于0';
|
||
manualStatus.className = 'error';
|
||
return;
|
||
}
|
||
|
||
try {
|
||
manualStatus.textContent = '提交中...';
|
||
manualStatus.className = '';
|
||
|
||
const payload = { date, qty, to, platform };
|
||
if (boxNo) {
|
||
payload.box_no = boxNo;
|
||
}
|
||
|
||
await API.uploadShipments(payload);
|
||
|
||
const platformName = defaultPlatformNameMap[platform] || platform;
|
||
manualStatus.textContent = `✓ 录入成功!机种:${platformName},数量:${qty}`;
|
||
manualStatus.className = 'success';
|
||
|
||
// 清空表单(保留日期和机种)
|
||
document.getElementById('ship-qty').value = '';
|
||
document.getElementById('ship-to').value = '';
|
||
document.getElementById('ship-box-no').value = '';
|
||
|
||
} catch(e) {
|
||
manualStatus.textContent = '✗ 录入失败:' + (e.message || '未知错误');
|
||
manualStatus.className = 'error';
|
||
}
|
||
});
|
||
|
||
const fileInput = document.getElementById('ship-file');
|
||
const validateBtn = document.getElementById('ship-validate');
|
||
const uploadFileBtn = document.getElementById('ship-upload-file');
|
||
const fileStatus = document.getElementById('ship-file-status');
|
||
|
||
fileInput?.addEventListener('change', () => {
|
||
fileStatus.textContent = '';
|
||
fileStatus.className = '';
|
||
});
|
||
|
||
validateBtn?.addEventListener('click', async () => {
|
||
const file = fileInput?.files?.[0];
|
||
if (!file) {
|
||
fileStatus.textContent = '请先选择文件';
|
||
fileStatus.className = 'error';
|
||
return;
|
||
}
|
||
|
||
const formData = new FormData();
|
||
formData.append('file', file);
|
||
|
||
try {
|
||
const res = await fetch('/api/validate/shipments-file', {
|
||
method: 'POST',
|
||
body: formData,
|
||
credentials: 'include'
|
||
});
|
||
|
||
if (!res.ok) {
|
||
const text = await res.text();
|
||
throw new Error(`HTTP ${res.status}: ${text}`);
|
||
}
|
||
|
||
const data = await res.json();
|
||
|
||
if (data.valid) {
|
||
fileStatus.textContent = '✓ ' + data.message;
|
||
fileStatus.className = 'success';
|
||
} else {
|
||
fileStatus.textContent = '✗ ' + data.message;
|
||
fileStatus.className = 'error';
|
||
}
|
||
} catch (e) {
|
||
fileStatus.textContent = '验证失败:' + e.message;
|
||
fileStatus.className = 'error';
|
||
}
|
||
});
|
||
|
||
uploadFileBtn?.addEventListener('click', async () => {
|
||
const file = fileInput?.files?.[0];
|
||
const platform = document.getElementById('ship-platform')?.value;
|
||
|
||
if (!platform) {
|
||
fileStatus.textContent = '✗ 请选择机种类型';
|
||
fileStatus.className = 'error';
|
||
return;
|
||
}
|
||
|
||
if (!file) {
|
||
fileStatus.textContent = '✗ 请先选择文件';
|
||
fileStatus.className = 'error';
|
||
return;
|
||
}
|
||
|
||
const formData = new FormData();
|
||
formData.append('file', file);
|
||
formData.append('platform', platform);
|
||
|
||
try {
|
||
fileStatus.textContent = '上传中...';
|
||
fileStatus.className = '';
|
||
|
||
const res = await fetch('/api/upload/shipments-file', {
|
||
method: 'POST',
|
||
body: formData,
|
||
credentials: 'include'
|
||
});
|
||
|
||
if (!res.ok) {
|
||
const text = await res.text();
|
||
throw new Error(`HTTP ${res.status}: ${text}`);
|
||
}
|
||
|
||
const data = await res.json();
|
||
|
||
if (data.ok) {
|
||
const platformName = defaultPlatformNameMap[platform] || platform;
|
||
fileStatus.textContent = `✓ 上传成功!机种:${platformName},共导入${data.count}个箱次,${data.total_qty}个SN`;
|
||
fileStatus.className = 'success';
|
||
fileInput.value = '';
|
||
document.getElementById('ship-platform').value = '';
|
||
} else {
|
||
fileStatus.textContent = '✗ ' + (data.error || '上传失败');
|
||
fileStatus.className = 'error';
|
||
}
|
||
} catch (e) {
|
||
fileStatus.textContent = '上传失败:' + e.message;
|
||
fileStatus.className = 'error';
|
||
}
|
||
});
|
||
}, 0);
|
||
return `
|
||
<style>
|
||
#shipments-page {
|
||
padding: 20px;
|
||
background: var(--bg);
|
||
}
|
||
#shipments-page .page-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 20px;
|
||
}
|
||
#shipments-page h1 {
|
||
margin: 0;
|
||
font-size: 24px;
|
||
color: var(--text);
|
||
}
|
||
#shipments-page .content-area {
|
||
background: var(--surface);
|
||
border-radius: 8px;
|
||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||
}
|
||
#shipments-page .section {
|
||
padding: 20px;
|
||
}
|
||
#shipments-page .section + .section {
|
||
border-top: 1px solid var(--border);
|
||
}
|
||
#shipments-page .section-title {
|
||
font-weight: 600;
|
||
font-size: 16px;
|
||
margin-bottom: 12px;
|
||
color: var(--text);
|
||
}
|
||
#shipments-page .section-desc {
|
||
font-size: 13px;
|
||
color: var(--text-2);
|
||
margin-bottom: 16px;
|
||
}
|
||
#shipments-page .format-requirements {
|
||
background: rgba(79,140,255,0.08);
|
||
border: 1px solid rgba(79,140,255,0.2);
|
||
border-radius: 8px;
|
||
padding: 12px;
|
||
margin-bottom: 16px;
|
||
font-size: 13px;
|
||
line-height: 1.6;
|
||
}
|
||
#shipments-page .row {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, 1fr);
|
||
gap: 16px;
|
||
margin-bottom: 16px;
|
||
}
|
||
@media (max-width: 768px) {
|
||
#shipments-page .row {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
}
|
||
</style>
|
||
|
||
<div id="shipments-page">
|
||
<div class="page-header">
|
||
<h1>发货记录</h1>
|
||
<div class="page-actions">
|
||
<button class="btn btn-secondary" id="ship-manage-platforms" type="button">机种管理</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="content-area">
|
||
<div class="section">
|
||
<div class="section-title">手动录入</div>
|
||
<div class="section-desc">用于快速录入发货汇总信息(不含详细SN)</div>
|
||
|
||
<div class="row">
|
||
<div class="field">
|
||
<label>发货日期 <span style="color:var(--danger)">*</span></label>
|
||
<input id="ship-date" type="date" class="input" />
|
||
</div>
|
||
<div class="field">
|
||
<label>机种类型 <span style="color:var(--danger)">*</span></label>
|
||
<select id="ship-manual-platform" class="input">
|
||
<option value="">请选择机种</option>
|
||
<option value="pdd">拼多多</option>
|
||
<option value="yt">圆通</option>
|
||
<option value="tx">兔喜</option>
|
||
<option value="mt">美团</option>
|
||
<option value="drf">大润发</option>
|
||
<option value="std">标准版</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="row">
|
||
<div class="field">
|
||
<label>接收方 <span style="color:var(--danger)">*</span></label>
|
||
<input id="ship-to" class="input" placeholder="客户名称" />
|
||
</div>
|
||
<div class="field">
|
||
<label>数量 <span style="color:var(--danger)">*</span></label>
|
||
<input id="ship-qty" type="number" min="1" class="input" placeholder="发货数量" />
|
||
</div>
|
||
</div>
|
||
|
||
<div class="field" style="margin-bottom:16px">
|
||
<label>箱号(可选)</label>
|
||
<input id="ship-box-no" class="input" placeholder="例如:BOX001" />
|
||
</div>
|
||
|
||
<div id="ship-manual-status" style="margin:8px 0;font-size:13px"></div>
|
||
<div class="actions">
|
||
<button class="btn btn-primary" id="ship-upload">提交录入</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="section">
|
||
<div class="section-title">详细记录批量导入</div>
|
||
|
||
<div class="format-requirements">
|
||
<div style="font-weight:600;margin-bottom:6px;color:var(--primary)">📋 文件格式要求</div>
|
||
<div style="color:var(--text)">
|
||
<div>• 必需列:出货日期、箱号、SN1、SN2、...、SN20</div>
|
||
<div>• 出货日期列支持合并单元格</div>
|
||
<div>• 支持格式:Excel (.xlsx, .xls) 或 CSV</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="field" style="margin-bottom:16px">
|
||
<label>机种类型 <span style="color:var(--danger)">*</span></label>
|
||
<select id="ship-platform" class="input">
|
||
<option value="">请选择机种</option>
|
||
<option value="pdd">拼多多</option>
|
||
<option value="yt">圆通</option>
|
||
<option value="tx">兔喜</option>
|
||
<option value="mt">美团</option>
|
||
<option value="drf">大润发</option>
|
||
<option value="std">标准版</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="field" style="margin-bottom:16px">
|
||
<label>选择文件</label>
|
||
<input type="file" id="ship-file" accept=".xlsx,.xls,.csv" class="input" style="padding:6px" />
|
||
</div>
|
||
|
||
<div id="ship-file-status" style="margin:8px 0;font-size:13px"></div>
|
||
<div class="actions">
|
||
<button class="btn btn-secondary" id="ship-validate">验证文件</button>
|
||
<button class="btn btn-primary" id="ship-upload-file">导入数据</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}); |