ERP/frontend/js/components/shipments.js
2026-05-11 13:41:05 +08:00

434 lines
16 KiB
JavaScript
Executable File
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.

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>
`;
});