优化系统
This commit is contained in:
parent
8916fc56ec
commit
7c15f4156f
@ -137,7 +137,10 @@ const API = (() => {
|
||||
listRepairRecords: (params={}) => request('/list/repair-records' + (params.sn ? `?sn=${encodeURIComponent(params.sn)}` : params.limit ? `?limit=${params.limit}` : '')),
|
||||
getRepairRecord: id => request(`/repair-record/${id}`),
|
||||
updateRepairRecord: (id, data) => request(`/repair-record/${id}`, { method: 'PUT', body: JSON.stringify(data) }),
|
||||
updateRepairImages: (id, formData) => uploadFile(`/repair-record/${id}/images`, formData),
|
||||
deleteRepairRecord: id => request(`/repair-record/${id}`, { method: 'DELETE' }),
|
||||
batchUpdateTracking: (ids, tracking_number) => request('/repair-records/batch-tracking', { method: 'POST', body: JSON.stringify({ ids, tracking_number }) }),
|
||||
queryTracking: tracking_number => request(`/tracking/${encodeURIComponent(tracking_number)}`),
|
||||
exportRepairRecords: ids => {
|
||||
// 使用fetch直接下载文件
|
||||
return fetch('/api/export/repair-records', {
|
||||
|
||||
@ -2,7 +2,7 @@ const Dashboard = (() => {
|
||||
const truckIcon = `<div class="truck-loader"><div class="truck-wrapper"><div class="truck-body"><svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 198 93" class="trucksvg"><path stroke-width="3" stroke="#282828" fill="#F83D3D" d="M135 22.5H177.264C178.295 22.5 179.22 23.133 179.594 24.0939L192.33 56.8443C192.442 57.1332 192.5 57.4404 192.5 57.7504V89C192.5 90.3807 191.381 91.5 190 91.5H135C133.619 91.5 132.5 90.3807 132.5 89V25C132.5 23.6193 133.619 22.5 135 22.5Z"></path><path stroke-width="3" stroke="#282828" fill="#7D7C7C" d="M146 33.5H181.741C182.779 33.5 183.709 34.1415 184.078 35.112L190.538 52.112C191.16 53.748 189.951 55.5 188.201 55.5H146C144.619 55.5 143.5 54.3807 143.5 53V36C143.5 34.6193 144.619 33.5 146 33.5Z"></path><path stroke-width="2" stroke="#282828" fill="#282828" d="M150 65C150 65.39 149.763 65.8656 149.127 66.2893C148.499 66.7083 147.573 67 146.5 67C145.427 67 144.501 66.7083 143.873 66.2893C143.237 65.8656 143 65.39 143 65C143 64.61 143.237 64.1344 143.873 63.7107C144.501 63.2917 145.427 63 146.5 63C147.573 63 148.499 63.2917 149.127 63.7107C149.763 64.1344 150 64.61 150 65Z"></path><rect stroke-width="2" stroke="#282828" fill="#FFFCAB" rx="1" height="7" width="5" y="63" x="187"></rect><rect stroke-width="2" stroke="#282828" fill="#282828" rx="1" height="11" width="4" y="81" x="193"></rect><rect stroke-width="3" stroke="#282828" fill="#DFDFDF" rx="2.5" height="90" width="121" y="1.5" x="6.5"></rect><rect stroke-width="2" stroke="#282828" fill="#DFDFDF" rx="2" height="4" width="6" y="84" x="1"></rect></svg></div><div class="truck-tires"><svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 30 30" class="tiresvg"><circle stroke-width="3" stroke="#282828" fill="#282828" r="13.5" cy="15" cx="15"></circle><circle fill="#DFDFDF" r="7" cy="15" cx="15"></circle></svg><svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 30 30" class="tiresvg"><circle stroke-width="3" stroke="#282828" fill="#282828" r="13.5" cy="15" cx="15"></circle><circle fill="#DFDFDF" r="7" cy="15" cx="15"></circle></svg></div><div class="truck-road"></div><svg class="truck-lamp" viewBox="0 0 453.459 453.459" xmlns="http://www.w3.org/2000/svg"><path d="M252.882,0c-37.781,0-68.686,29.953-70.245,67.358h-6.917v8.954c-26.109,2.163-45.463,10.011-45.463,19.366h9.993c-1.65,5.146-2.507,10.54-2.507,16.017c0,28.956,23.558,52.514,52.514,52.514c28.956,0,52.514-23.558,52.514-52.514c0-5.478-0.856-10.872-2.506-16.017h9.992c0-9.354-19.352-17.204-45.463-19.366v-8.954h-6.149C200.189,38.779,223.924,16,252.882,16c29.952,0,54.32,24.368,54.32,54.32c0,28.774-11.078,37.009-25.105,47.437c-17.444,12.968-37.216,27.667-37.216,78.884v113.914h-0.797c-5.068,0-9.174,4.108-9.174,9.177c0,2.844,1.293,5.383,3.321,7.066c-3.432,27.933-26.851,95.744-8.226,115.459v11.202h45.75v-11.202c18.625-19.715-4.794-87.527-8.227-115.459c2.029-1.683,3.322-4.223,3.322-7.066c0-5.068-4.107-9.177-9.176-9.177h-0.795V196.641c0-43.174,14.942-54.283,30.762-66.043c14.793-10.997,31.559-23.461,31.559-60.277C323.202,31.545,291.656,0,252.882,0z M232.77,111.694c0,23.442-19.071,42.514-42.514,42.514c-23.442,0-42.514-19.072-42.514-42.514c0-5.531,1.078-10.957,3.141-16.017h78.747C231.693,100.736,232.77,106.162,232.77,111.694z"></path></svg></div></div>`;
|
||||
|
||||
const metricsIcons = {
|
||||
'直通良品率': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>',
|
||||
'直通良品数': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>',
|
||||
'良品率': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg>',
|
||||
'发货数量': truckIcon,
|
||||
'不良数量': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>',
|
||||
@ -886,7 +886,7 @@ const Dashboard = (() => {
|
||||
return `
|
||||
<div>
|
||||
<div class="dashboard-metrics-4col">
|
||||
${metricsCard('直通良品率', data.fpyRate || '—', 'success')}
|
||||
${metricsCard('直通良品数', data.fpyCount || 0, 'success')}
|
||||
${rateCard(data.goodRate || '—', data.badRate || '—')}
|
||||
${metricsCard('发货数量', data.shipments, 'warning')}
|
||||
${todayProductionCard(data.todayPdd || 0, data.todayYt || 0, data.activePlatform || 'pdd')}
|
||||
|
||||
@ -54,7 +54,7 @@ Router.register('/upload/shipments', async () => {
|
||||
|
||||
await API.uploadShipments(payload);
|
||||
|
||||
const platformName = {pdd: '拼多多', yt: '圆通', tx: '兔喜'}[platform] || platform;
|
||||
const platformName = {pdd: '拼多多', yt: '圆通', tx: '兔喜', mt: '美团', drf: '大润发', std: '标准版'}[platform] || platform;
|
||||
manualStatus.textContent = `✓ 录入成功!机种:${platformName},数量:${qty}`;
|
||||
manualStatus.className = 'success';
|
||||
|
||||
@ -155,7 +155,7 @@ Router.register('/upload/shipments', async () => {
|
||||
const data = await res.json();
|
||||
|
||||
if (data.ok) {
|
||||
const platformName = {pdd: '拼多多', yt: '圆通', tx: '兔喜'}[platform] || platform;
|
||||
const platformName = {pdd: '拼多多', yt: '圆通', tx: '兔喜', mt: '美团', drf: '大润发', std: '标准版'}[platform] || platform;
|
||||
fileStatus.textContent = `✓ 上传成功!机种:${platformName},共导入${data.count}个箱次,${data.total_qty}个SN`;
|
||||
fileStatus.className = 'success';
|
||||
fileInput.value = '';
|
||||
@ -193,6 +193,9 @@ Router.register('/upload/shipments', async () => {
|
||||
<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>
|
||||
@ -234,6 +237,9 @@ Router.register('/upload/shipments', async () => {
|
||||
<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">
|
||||
|
||||
@ -177,15 +177,19 @@ const Upload = (() => {
|
||||
<div id="repair-machine-type-error" style="color:#ef4444;font-size:12px;margin-top:4px;display:none"></div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" style="margin-bottom:16px">
|
||||
<label style="display:block;font-weight:600;margin-bottom:6px">
|
||||
不良现象 <span style="color:#ef4444">*</span>
|
||||
</label>
|
||||
<input type="text" id="repair-defect-symptom" class="input" placeholder="例:红灯不亮" style="width:100%" />
|
||||
<div id="repair-symptom-error" style="color:#ef4444;font-size:12px;margin-top:4px;display:none"></div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" style="margin-bottom:16px">
|
||||
<label style="display:block;font-weight:600;margin-bottom:6px">
|
||||
不良原因 <span style="color:#ef4444">*</span>
|
||||
</label>
|
||||
<select id="repair-defect-reason" class="input" style="width:100%">
|
||||
<option value="">请选择不良原因</option>
|
||||
${defectReasonOptions.map(opt => `<option value="${opt}">${opt}</option>`).join('')}
|
||||
</select>
|
||||
<input type="text" id="repair-defect-reason-other" class="input" placeholder="请输入其他原因" style="width:100%;margin-top:8px;display:none" />
|
||||
<input type="text" id="repair-defect-reason" class="input" placeholder="例:MMC损坏 | 填写[无不良]则维修方法可不填" style="width:100%" />
|
||||
<div id="repair-defect-error" style="color:#ef4444;font-size:12px;margin-top:4px;display:none"></div>
|
||||
</div>
|
||||
|
||||
@ -242,6 +246,7 @@ const Upload = (() => {
|
||||
<input type="text" id="repairs-search-sn" class="input" placeholder="输入SN搜索" style="width:150px;font-size:12px;padding:4px 8px" />
|
||||
<button class="btn btn-secondary" id="repairs-search-btn" style="font-size:12px;padding:4px 8px">搜索</button>
|
||||
<button class="btn btn-secondary" id="repairs-show-all" style="font-size:12px;padding:4px 8px">全部</button>
|
||||
<button class="btn btn-secondary" id="repairs-tracking-btn" style="font-size:12px;padding:4px 8px">添加快递单号</button>
|
||||
<button class="btn btn-secondary" id="repairs-export-btn" style="font-size:12px;padding:4px 8px">导出选中</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -810,8 +815,8 @@ const Upload = (() => {
|
||||
const showAllBtn = document.getElementById('repairs-show-all');
|
||||
const snInput = document.getElementById('repair-sn');
|
||||
const machineTypeSelect = document.getElementById('repair-machine-type');
|
||||
const defectSelect = document.getElementById('repair-defect-reason');
|
||||
const defectOtherInput = document.getElementById('repair-defect-reason-other');
|
||||
const symptomInput = document.getElementById('repair-defect-symptom');
|
||||
const defectInput = document.getElementById('repair-defect-reason');
|
||||
const methodInput = document.getElementById('repair-method');
|
||||
const methodGroup = document.getElementById('repair-method-group');
|
||||
const imageInput = document.getElementById('repair-images');
|
||||
@ -819,6 +824,7 @@ const Upload = (() => {
|
||||
const previewContainer = document.getElementById('repair-image-preview');
|
||||
const snError = document.getElementById('repair-sn-error');
|
||||
const machineTypeError = document.getElementById('repair-machine-type-error');
|
||||
const symptomError = document.getElementById('repair-symptom-error');
|
||||
const defectError = document.getElementById('repair-defect-error');
|
||||
const methodError = document.getElementById('repair-method-error');
|
||||
const methodRequired = document.getElementById('repair-method-required');
|
||||
@ -829,25 +835,19 @@ const Upload = (() => {
|
||||
// 存储待上传的图片文件
|
||||
let pendingImages = [];
|
||||
|
||||
// 显示/隐藏"其他"原因输入框,并根据不良原因决定维修方法是否必填
|
||||
addListener(defectSelect, 'change', () => {
|
||||
if(defectSelect.value === '其他') {
|
||||
defectOtherInput.style.display = 'block';
|
||||
defectOtherInput.focus();
|
||||
} else {
|
||||
defectOtherInput.style.display = 'none';
|
||||
defectOtherInput.value = '';
|
||||
}
|
||||
// 根据不良原因决定维修方法是否必填
|
||||
addListener(defectInput, 'input', () => {
|
||||
// 清除错误提示
|
||||
defectError.style.display = 'none';
|
||||
methodError.style.display = 'none';
|
||||
|
||||
const defectValue = defectInput.value.trim();
|
||||
// 条件性显示维修方法:无不良时可选,其他原因时必填
|
||||
if(defectSelect.value) {
|
||||
if(defectValue) {
|
||||
methodGroup.style.opacity = '1';
|
||||
methodInput.disabled = false;
|
||||
|
||||
if(defectSelect.value === '无不良') {
|
||||
if(defectValue === '无不良') {
|
||||
// 无不良时维修方法可选
|
||||
methodRequired.style.display = 'none';
|
||||
methodInput.placeholder = '请描述维修方法(可选)';
|
||||
@ -967,9 +967,7 @@ const Upload = (() => {
|
||||
addListener(clearBtn, 'click', () => {
|
||||
snInput.value = '';
|
||||
machineTypeSelect.value = '';
|
||||
defectSelect.value = '';
|
||||
defectOtherInput.value = '';
|
||||
defectOtherInput.style.display = 'none';
|
||||
defectInput.value = '';
|
||||
methodInput.value = '';
|
||||
methodGroup.style.opacity = '0.5';
|
||||
methodInput.disabled = true;
|
||||
@ -1027,10 +1025,14 @@ const Upload = (() => {
|
||||
<div style="flex:1;display:flex;flex-direction:column;gap:4px">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center">
|
||||
<span style="font-weight:600">SN: ${r.sn}</span>
|
||||
<span style="font-size:11px;color:var(--text-2)">${ts}</span>
|
||||
<div style="text-align:right">
|
||||
<div style="font-size:11px;color:var(--text-2)">${ts}</div>
|
||||
${r.tracking_number ? `<span class="badge tracking-link" data-tracking="${r.tracking_number}" style="background:#fef3c7;color:#92400e;cursor:pointer;font-size:10px;margin-top:2px;display:inline-block">📦 ${r.tracking_number}</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center">
|
||||
${r.machine_type ? `<span class="badge" style="background:var(--primary-bg);color:var(--primary)">${r.machine_type}</span>` : ''}
|
||||
${r.defect_symptom ? `<span class="badge" style="background:#e0e7ff;color:#4338ca">${r.defect_symptom}</span>` : ''}
|
||||
<span class="badge" style="background:${r.defect_reason === '无不良' ? 'var(--success-bg)' : 'var(--danger-bg)'};color:${r.defect_reason === '无不良' ? 'var(--success)' : 'var(--danger)'}">${r.defect_reason}</span>
|
||||
${imgCount > 0 ? `<span class="badge">${imgCount}张图片</span>` : ''}
|
||||
</div>
|
||||
@ -1098,6 +1100,86 @@ const Upload = (() => {
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// 快递单号点击查询物流
|
||||
document.querySelectorAll('.tracking-link').forEach(link => {
|
||||
link.onclick = async () => {
|
||||
const trackingNumber = link.dataset.tracking;
|
||||
if(!trackingNumber) return;
|
||||
|
||||
// 显示加载弹窗
|
||||
const modal = document.createElement('div');
|
||||
modal.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center;z-index:1000';
|
||||
modal.innerHTML = `
|
||||
<div style="background:var(--surface);border-radius:12px;width:90%;max-width:500px;max-height:80vh;box-shadow:0 4px 20px rgba(0,0,0,0.15);display:flex;flex-direction:column">
|
||||
<div style="padding:16px 20px;border-bottom:1px solid var(--border);display:flex;justify-content:space-between;align-items:center">
|
||||
<h3 style="margin:0;font-size:16px">📦 物流查询</h3>
|
||||
<button id="tracking-modal-close" style="background:none;border:none;font-size:20px;cursor:pointer;color:var(--text-2)">×</button>
|
||||
</div>
|
||||
<div id="tracking-content" style="padding:20px;overflow-y:auto;flex:1">
|
||||
<div style="text-align:center;padding:40px 0">
|
||||
<div style="display:inline-block;width:24px;height:24px;border:3px solid var(--primary);border-top-color:transparent;border-radius:50%;animation:spin 1s linear infinite"></div>
|
||||
<div style="margin-top:12px;color:var(--text-2)">正在查询物流信息...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(modal);
|
||||
document.getElementById('tracking-modal-close').onclick = () => modal.remove();
|
||||
modal.onclick = (e) => { if(e.target === modal) modal.remove(); };
|
||||
|
||||
try {
|
||||
const result = await API.queryTracking(trackingNumber);
|
||||
const contentEl = document.getElementById('tracking-content');
|
||||
|
||||
if(result.ok && result.data && result.data.length > 0) {
|
||||
// 快递公司名称映射
|
||||
const comNames = {
|
||||
'shunfeng': '顺丰速运', 'yuantong': '圆通速递', 'zhongtong': '中通快递',
|
||||
'yunda': '韵达快递', 'jd': '京东物流', 'ems': 'EMS', 'shentong': '申通快递',
|
||||
'debangkuaidi': '德邦快递', 'auto': '快递'
|
||||
};
|
||||
const comName = comNames[result.com] || result.com || '快递';
|
||||
const stateNames = {'0': '在途', '1': '揽收', '2': '疑难', '3': '签收', '4': '退签', '5': '派件', '6': '退回', '7': '转投'};
|
||||
const stateName = stateNames[result.state] || '未知';
|
||||
const stateColor = result.state === '3' ? 'var(--success)' : (result.state === '5' ? 'var(--primary)' : 'var(--text-2)');
|
||||
|
||||
contentEl.innerHTML = `
|
||||
<div style="margin-bottom:16px;display:flex;justify-content:space-between;align-items:center">
|
||||
<div>
|
||||
<div style="font-weight:600;font-size:14px">${comName}</div>
|
||||
<div style="font-size:12px;color:var(--text-2)">${trackingNumber}</div>
|
||||
</div>
|
||||
<span class="badge" style="background:${result.state === '3' ? 'var(--success-bg)' : 'var(--primary-bg)'};color:${stateColor}">${stateName}</span>
|
||||
</div>
|
||||
<div style="border-left:2px solid var(--border);padding-left:16px;margin-left:8px">
|
||||
${result.data.map((item, idx) => `
|
||||
<div style="position:relative;padding-bottom:16px;${idx === result.data.length - 1 ? '' : 'border-bottom:none'}">
|
||||
<div style="position:absolute;left:-22px;top:4px;width:12px;height:12px;border-radius:50%;background:${idx === 0 ? 'var(--primary)' : 'var(--border)'};border:2px solid var(--surface)"></div>
|
||||
<div style="font-size:12px;color:${idx === 0 ? 'var(--primary)' : 'var(--text-2)'};margin-bottom:4px">${item.time || item.ftime || ''}</div>
|
||||
<div style="font-size:13px;color:${idx === 0 ? 'var(--text)' : 'var(--text-2)'}">${item.context || ''}</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
contentEl.innerHTML = `
|
||||
<div style="text-align:center;padding:40px 0;color:var(--text-2)">
|
||||
<div style="font-size:48px;margin-bottom:12px">📭</div>
|
||||
<div>${result.error || '暂无物流信息'}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
} catch(e) {
|
||||
document.getElementById('tracking-content').innerHTML = `
|
||||
<div style="text-align:center;padding:40px 0;color:var(--danger)">
|
||||
<div style="font-size:48px;margin-bottom:12px">❌</div>
|
||||
<div>查询失败: ${e.message || '网络错误'}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
// 更新全选框状态
|
||||
@ -1163,7 +1245,8 @@ const Upload = (() => {
|
||||
let modal = document.getElementById(modalId);
|
||||
if(modal) modal.remove();
|
||||
|
||||
const imagesHtml = (record.images || []).length > 0
|
||||
const images = record.images || [];
|
||||
const imagesHtml = images.length > 0
|
||||
? `<div style="display:flex;flex-wrap:wrap;gap:8px;margin-top:8px">
|
||||
${record.images.map(img => `
|
||||
<a href="/api/repair-image/${img}" target="_blank" style="display:block;width:100px;height:100px;border-radius:6px;overflow:hidden;border:1px solid var(--border)">
|
||||
@ -1197,21 +1280,38 @@ const Upload = (() => {
|
||||
${machineTypeOptions.map(opt => `<option value="${opt}" ${opt === record.machine_type ? 'selected' : ''}>${opt}</option>`).join('')}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group" style="margin-bottom:16px">
|
||||
<label style="display:block;font-weight:600;margin-bottom:6px">不良现象 <span style="color:#ef4444">*</span></label>
|
||||
<input type="text" id="edit-repair-symptom" class="input" placeholder="例:红灯不亮" style="width:100%" value="${record.defect_symptom || ''}" />
|
||||
</div>
|
||||
<div class="form-group" style="margin-bottom:16px">
|
||||
<label style="display:block;font-weight:600;margin-bottom:6px">不良原因 <span style="color:#ef4444">*</span></label>
|
||||
<select id="edit-repair-defect" class="input" style="width:100%">
|
||||
${defectReasonOptions.map(opt => `<option value="${opt}" ${opt === record.defect_reason ? 'selected' : ''}>${opt}</option>`).join('')}
|
||||
</select>
|
||||
<input type="text" id="edit-repair-defect-other" class="input" placeholder="请输入其他原因" style="width:100%;margin-top:8px;display:${record.defect_reason === '其他' || !defectReasonOptions.includes(record.defect_reason) ? 'block' : 'none'}" value="${!defectReasonOptions.includes(record.defect_reason) ? record.defect_reason : ''}" />
|
||||
<input type="text" id="edit-repair-defect" class="input" placeholder="填写【无不良】则维修方法可不填" style="width:100%" value="${record.defect_reason || ''}" />
|
||||
</div>
|
||||
<div class="form-group" style="margin-bottom:16px">
|
||||
<label style="display:block;font-weight:600;margin-bottom:6px">维修方法 <span id="edit-method-required" style="color:#ef4444;display:${isNoDefect ? 'none' : 'inline'}">*</span></label>
|
||||
<textarea id="edit-repair-method" class="input" rows="3" style="width:100%;resize:vertical">${record.repair_method || ''}</textarea>
|
||||
</div>
|
||||
<div class="form-group" style="margin-bottom:16px">
|
||||
<label style="display:block;font-weight:600;margin-bottom:6px">图片</label>
|
||||
${imagesHtml}
|
||||
<div style="font-size:11px;color:var(--text-2);margin-top:4px">编辑模式暂不支持修改图片</div>
|
||||
<label style="display:block;font-weight:600;margin-bottom:6px">快递单号</label>
|
||||
<input type="text" id="edit-repair-tracking" class="input" placeholder="寄回客户的快递单号(可选)" style="width:100%" value="${record.tracking_number || ''}" />
|
||||
</div>
|
||||
<div class="form-group" style="margin-bottom:16px">
|
||||
<label style="display:block;font-weight:600;margin-bottom:6px">图片管理</label>
|
||||
<div id="edit-images-container" style="display:flex;flex-wrap:wrap;gap:8px;margin-bottom:8px">
|
||||
${images.map(img => `
|
||||
<div class="edit-image-item" data-filename="${img}" style="position:relative;width:80px;height:80px;border-radius:6px;overflow:hidden;border:1px solid var(--border)">
|
||||
<img src="/api/repair-image/${img}" style="width:100%;height:100%;object-fit:cover" />
|
||||
<button class="edit-image-delete" data-filename="${img}" style="position:absolute;top:2px;right:2px;width:20px;height:20px;border-radius:50%;background:rgba(239,68,68,0.9);color:white;border:none;cursor:pointer;font-size:12px;display:flex;align-items:center;justify-content:center">×</button>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
<div style="background:rgba(79,140,255,0.08);border:1px dashed rgba(79,140,255,0.4);border-radius:8px;padding:12px;text-align:center;cursor:pointer" id="edit-image-dropzone">
|
||||
<input type="file" id="edit-new-images" multiple accept="image/jpeg,image/png,image/gif" style="display:none" />
|
||||
<div style="color:var(--primary);font-size:12px">点击或拖拽添加新图片</div>
|
||||
</div>
|
||||
<div id="edit-new-images-preview" style="display:flex;flex-wrap:wrap;gap:8px;margin-top:8px"></div>
|
||||
<div style="font-size:11px;color:var(--text-2);margin-top:4px">最多10张图片,点击×删除</div>
|
||||
</div>
|
||||
<div style="display:flex;gap:12px">
|
||||
<button class="btn" id="edit-repair-save" style="flex:1">保存</button>
|
||||
@ -1226,6 +1326,10 @@ const Upload = (() => {
|
||||
<div style="font-weight:600;color:var(--text-2);font-size:12px;margin-bottom:4px">机种</div>
|
||||
<span class="badge" style="background:var(--primary-bg);color:var(--primary)">${record.machine_type || '未知'}</span>
|
||||
</div>
|
||||
<div style="margin-bottom:12px">
|
||||
<div style="font-weight:600;color:var(--text-2);font-size:12px;margin-bottom:4px">不良现象</div>
|
||||
<div style="font-size:14px">${record.defect_symptom || '未填写'}</div>
|
||||
</div>
|
||||
<div style="margin-bottom:12px">
|
||||
<div style="font-weight:600;color:var(--text-2);font-size:12px;margin-bottom:4px">不良原因</div>
|
||||
<span class="badge" style="background:${isNoDefect ? 'var(--success-bg)' : 'var(--danger-bg)'};color:${isNoDefect ? 'var(--success)' : 'var(--danger)'}">${record.defect_reason}</span>
|
||||
@ -1234,6 +1338,10 @@ const Upload = (() => {
|
||||
<div style="font-weight:600;color:var(--text-2);font-size:12px;margin-bottom:4px">维修方法</div>
|
||||
<div style="font-size:14px">${record.repair_method || '无'}</div>
|
||||
</div>
|
||||
<div style="margin-bottom:12px">
|
||||
<div style="font-weight:600;color:var(--text-2);font-size:12px;margin-bottom:4px">快递单号</div>
|
||||
<div style="font-size:14px">${record.tracking_number ? `<span class="badge" style="background:#fef3c7;color:#92400e">📦 ${record.tracking_number}</span>` : '未填写'}</div>
|
||||
</div>
|
||||
<div style="margin-bottom:12px">
|
||||
<div style="font-weight:600;color:var(--text-2);font-size:12px;margin-bottom:4px">上传时间</div>
|
||||
<div style="font-size:14px">${ts}</div>
|
||||
@ -1258,29 +1366,87 @@ const Upload = (() => {
|
||||
modal.querySelector('div').onclick = (e) => { if(e.target === modal.querySelector('div')) modal.remove(); };
|
||||
|
||||
if(isEdit) {
|
||||
const editDefectSelect = document.getElementById('edit-repair-defect');
|
||||
const editDefectOther = document.getElementById('edit-repair-defect-other');
|
||||
const editDefectInput = document.getElementById('edit-repair-defect');
|
||||
const editMethodRequired = document.getElementById('edit-method-required');
|
||||
|
||||
// 不良原因选择变化
|
||||
editDefectSelect.onchange = () => {
|
||||
if(editDefectSelect.value === '其他') {
|
||||
editDefectOther.style.display = 'block';
|
||||
} else {
|
||||
editDefectOther.style.display = 'none';
|
||||
// 图片管理相关
|
||||
let keepImages = [...images]; // 要保留的现有图片
|
||||
let newImageFiles = []; // 新添加的图片文件
|
||||
|
||||
// 删除现有图片
|
||||
document.querySelectorAll('.edit-image-delete').forEach(btn => {
|
||||
btn.onclick = (e) => {
|
||||
e.stopPropagation();
|
||||
const filename = btn.dataset.filename;
|
||||
keepImages = keepImages.filter(img => img !== filename);
|
||||
btn.closest('.edit-image-item').remove();
|
||||
};
|
||||
});
|
||||
|
||||
// 添加新图片
|
||||
const editDropzone = document.getElementById('edit-image-dropzone');
|
||||
const editNewImagesInput = document.getElementById('edit-new-images');
|
||||
const editNewImagesPreview = document.getElementById('edit-new-images-preview');
|
||||
|
||||
editDropzone.onclick = () => editNewImagesInput.click();
|
||||
editDropzone.ondragover = (e) => { e.preventDefault(); editDropzone.style.borderColor = 'var(--primary)'; };
|
||||
editDropzone.ondragleave = () => { editDropzone.style.borderColor = 'rgba(79,140,255,0.4)'; };
|
||||
editDropzone.ondrop = (e) => {
|
||||
e.preventDefault();
|
||||
editDropzone.style.borderColor = 'rgba(79,140,255,0.4)';
|
||||
handleNewImages(e.dataTransfer.files);
|
||||
};
|
||||
|
||||
editNewImagesInput.onchange = () => handleNewImages(editNewImagesInput.files);
|
||||
|
||||
function handleNewImages(files) {
|
||||
for(const file of files) {
|
||||
if(!file.type.startsWith('image/')) continue;
|
||||
if(keepImages.length + newImageFiles.length >= 10) {
|
||||
API.toast('最多10张图片');
|
||||
break;
|
||||
}
|
||||
newImageFiles.push(file);
|
||||
|
||||
// 预览
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
const idx = newImageFiles.length - 1;
|
||||
const div = document.createElement('div');
|
||||
div.className = 'new-image-item';
|
||||
div.dataset.idx = idx;
|
||||
div.style.cssText = 'position:relative;width:80px;height:80px;border-radius:6px;overflow:hidden;border:1px solid var(--border)';
|
||||
div.innerHTML = `
|
||||
<img src="${e.target.result}" style="width:100%;height:100%;object-fit:cover" />
|
||||
<button class="new-image-delete" data-idx="${idx}" style="position:absolute;top:2px;right:2px;width:20px;height:20px;border-radius:50%;background:rgba(239,68,68,0.9);color:white;border:none;cursor:pointer;font-size:12px;display:flex;align-items:center;justify-content:center">×</button>
|
||||
`;
|
||||
editNewImagesPreview.appendChild(div);
|
||||
|
||||
div.querySelector('.new-image-delete').onclick = (ev) => {
|
||||
ev.stopPropagation();
|
||||
const i = parseInt(div.dataset.idx);
|
||||
newImageFiles[i] = null;
|
||||
div.remove();
|
||||
};
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
editMethodRequired.style.display = editDefectSelect.value === '无不良' ? 'none' : 'inline';
|
||||
}
|
||||
|
||||
// 不良原因输入变化
|
||||
editDefectInput.oninput = () => {
|
||||
const defectValue = editDefectInput.value.trim();
|
||||
editMethodRequired.style.display = defectValue === '无不良' ? 'none' : 'inline';
|
||||
};
|
||||
|
||||
document.getElementById('edit-repair-cancel').onclick = () => modal.remove();
|
||||
document.getElementById('edit-repair-save').onclick = async () => {
|
||||
const newSn = document.getElementById('edit-repair-sn').value.trim();
|
||||
const newMachineType = document.getElementById('edit-repair-machine-type').value;
|
||||
let newDefect = editDefectSelect.value;
|
||||
if(newDefect === '其他') {
|
||||
newDefect = editDefectOther.value.trim();
|
||||
}
|
||||
const newSymptom = document.getElementById('edit-repair-symptom').value.trim();
|
||||
const newDefect = editDefectInput.value.trim();
|
||||
const newMethod = document.getElementById('edit-repair-method').value.trim();
|
||||
const newTracking = document.getElementById('edit-repair-tracking').value.trim();
|
||||
|
||||
// 验证
|
||||
if(!newSn || newSn.length < 6) {
|
||||
@ -1291,29 +1457,54 @@ const Upload = (() => {
|
||||
API.toast('请选择机种');
|
||||
return;
|
||||
}
|
||||
if(!newSymptom) {
|
||||
API.toast('请输入不良现象');
|
||||
return;
|
||||
}
|
||||
if(!newDefect) {
|
||||
API.toast('请选择或输入不良原因');
|
||||
API.toast('请输入不良原因');
|
||||
return;
|
||||
}
|
||||
if(newDefect !== '无不良' && !newMethod) {
|
||||
API.toast('选择了不良原因时,维修方法为必填项');
|
||||
API.toast('填写了不良原因时,维修方法为必填项');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 先更新基本信息
|
||||
const result = await API.updateRepairRecord(id, {
|
||||
sn: newSn,
|
||||
machine_type: newMachineType,
|
||||
defect_symptom: newSymptom,
|
||||
defect_reason: newDefect,
|
||||
repair_method: newMethod
|
||||
repair_method: newMethod,
|
||||
tracking_number: newTracking
|
||||
});
|
||||
if(result.ok) {
|
||||
API.toast('保存成功');
|
||||
modal.remove();
|
||||
refreshRepairsList();
|
||||
} else {
|
||||
|
||||
if(!result.ok) {
|
||||
API.toast(result.error || '保存失败');
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果图片有变化,更新图片
|
||||
const actualNewFiles = newImageFiles.filter(f => f !== null);
|
||||
const imagesChanged = keepImages.length !== images.length || actualNewFiles.length > 0;
|
||||
|
||||
if(imagesChanged) {
|
||||
const formData = new FormData();
|
||||
keepImages.forEach(img => formData.append('keep_images', img));
|
||||
actualNewFiles.forEach(file => formData.append('new_images', file));
|
||||
|
||||
const imgResult = await API.updateRepairImages(id, formData);
|
||||
if(!imgResult.ok) {
|
||||
API.toast('图片更新失败: ' + (imgResult.error || '未知错误'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
API.toast('保存成功');
|
||||
modal.remove();
|
||||
refreshRepairsList();
|
||||
} catch(e) {
|
||||
API.toast('保存失败: ' + (e.message || '网络错误'));
|
||||
}
|
||||
@ -1377,6 +1568,64 @@ const Upload = (() => {
|
||||
updateSelectedCount();
|
||||
});
|
||||
|
||||
// 添加快递单号按钮
|
||||
const trackingBtn = document.getElementById('repairs-tracking-btn');
|
||||
addListener(trackingBtn, 'click', async () => {
|
||||
if(selectedIds.size === 0) {
|
||||
API.toast('请先勾选要添加快递单号的记录');
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建弹窗
|
||||
const modal = document.createElement('div');
|
||||
modal.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center;z-index:1000';
|
||||
modal.innerHTML = `
|
||||
<div style="background:var(--surface);border-radius:12px;width:90%;max-width:400px;box-shadow:0 4px 20px rgba(0,0,0,0.15)">
|
||||
<div style="padding:16px 20px;border-bottom:1px solid var(--border);display:flex;justify-content:space-between;align-items:center">
|
||||
<h3 style="margin:0;font-size:16px">添加快递单号</h3>
|
||||
<button id="tracking-modal-close" style="background:none;border:none;font-size:20px;cursor:pointer;color:var(--text-2)">×</button>
|
||||
</div>
|
||||
<div style="padding:20px">
|
||||
<div style="margin-bottom:16px;font-size:14px;color:var(--text-2)">
|
||||
已选择 <strong>${selectedIds.size}</strong> 条记录
|
||||
</div>
|
||||
<div class="form-group" style="margin-bottom:16px">
|
||||
<label style="display:block;font-weight:600;margin-bottom:6px">快递单号 <span style="color:#ef4444">*</span></label>
|
||||
<input type="text" id="tracking-number-input" class="input" placeholder="请输入快递单号" style="width:100%" />
|
||||
</div>
|
||||
<div style="display:flex;gap:12px">
|
||||
<button class="btn" id="tracking-submit" style="flex:1">确定</button>
|
||||
<button class="btn btn-secondary" id="tracking-cancel">取消</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(modal);
|
||||
|
||||
document.getElementById('tracking-modal-close').onclick = () => modal.remove();
|
||||
document.getElementById('tracking-cancel').onclick = () => modal.remove();
|
||||
document.getElementById('tracking-submit').onclick = async () => {
|
||||
const trackingNumber = document.getElementById('tracking-number-input').value.trim();
|
||||
if(!trackingNumber) {
|
||||
API.toast('请输入快递单号');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await API.batchUpdateTracking(Array.from(selectedIds), trackingNumber);
|
||||
if(result.ok) {
|
||||
API.toast(`成功更新 ${result.updated} 条记录的快递单号`);
|
||||
modal.remove();
|
||||
refreshRepairsList();
|
||||
} else {
|
||||
API.toast(result.error || '更新失败');
|
||||
}
|
||||
} catch(e) {
|
||||
API.toast('更新失败: ' + (e.message || '网络错误'));
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// 导出按钮
|
||||
const exportBtn = document.getElementById('repairs-export-btn');
|
||||
addListener(exportBtn, 'click', async () => {
|
||||
@ -1437,17 +1686,18 @@ const Upload = (() => {
|
||||
hasError = true;
|
||||
}
|
||||
|
||||
// 验证不良现象
|
||||
const defectSymptom = symptomInput.value.trim();
|
||||
if(!defectSymptom) {
|
||||
symptomError.textContent = '请输入不良现象';
|
||||
symptomError.style.display = 'block';
|
||||
hasError = true;
|
||||
}
|
||||
|
||||
// 验证不良原因
|
||||
let defectReason = defectSelect.value;
|
||||
if(defectReason === '其他') {
|
||||
defectReason = defectOtherInput.value.trim();
|
||||
if(!defectReason) {
|
||||
defectError.textContent = '请输入其他原因';
|
||||
defectError.style.display = 'block';
|
||||
hasError = true;
|
||||
}
|
||||
} else if(!defectReason) {
|
||||
defectError.textContent = '请选择不良原因';
|
||||
const defectReason = defectInput.value.trim();
|
||||
if(!defectReason) {
|
||||
defectError.textContent = '请输入不良原因';
|
||||
defectError.style.display = 'block';
|
||||
hasError = true;
|
||||
}
|
||||
@ -1466,6 +1716,7 @@ const Upload = (() => {
|
||||
const formData = new FormData();
|
||||
formData.append('sn', sn);
|
||||
formData.append('machine_type', machineType);
|
||||
formData.append('defect_symptom', defectSymptom);
|
||||
formData.append('defect_reason', defectReason);
|
||||
formData.append('repair_method', methodInput.value.trim());
|
||||
|
||||
@ -1487,9 +1738,8 @@ const Upload = (() => {
|
||||
// 清空表单
|
||||
snInput.value = '';
|
||||
machineTypeSelect.value = '';
|
||||
defectSelect.value = '';
|
||||
defectOtherInput.value = '';
|
||||
defectOtherInput.style.display = 'none';
|
||||
symptomInput.value = '';
|
||||
defectInput.value = '';
|
||||
methodInput.value = '';
|
||||
methodGroup.style.opacity = '0.5';
|
||||
methodInput.disabled = true;
|
||||
|
||||
@ -54,6 +54,7 @@ const Router = (() => {
|
||||
|
||||
function updateBreadcrumb(path) {
|
||||
const bc = document.getElementById('breadcrumb');
|
||||
if (!bc) return; // 元素不存在时直接返回
|
||||
const parts = path.split('/').filter(Boolean);
|
||||
let acc = '#';
|
||||
bc.innerHTML = parts.map((p, i) => {
|
||||
|
||||
275
server/app.py
275
server/app.py
@ -152,6 +152,16 @@ def init_db():
|
||||
c.execute('ALTER TABLE repair_records ADD COLUMN machine_type TEXT')
|
||||
except:
|
||||
pass
|
||||
# 添加tracking_number字段(快递单号)
|
||||
try:
|
||||
c.execute('ALTER TABLE repair_records ADD COLUMN tracking_number TEXT')
|
||||
except:
|
||||
pass
|
||||
# 添加defect_symptom字段(不良现象)
|
||||
try:
|
||||
c.execute('ALTER TABLE repair_records ADD COLUMN defect_symptom TEXT')
|
||||
except:
|
||||
pass
|
||||
c.execute('''CREATE TABLE IF NOT EXISTS sop_files(
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
filename TEXT NOT NULL,
|
||||
@ -849,9 +859,8 @@ def dashboard():
|
||||
# 计算不良率
|
||||
bad_rate = "{}%".format(round((bad/(good+bad)) * 100, 2)) if (good+bad) > 0 else u'—'
|
||||
|
||||
# 计算直通良品率(FPY = First Pass Yield)
|
||||
total_produced = good + bad
|
||||
fpy_rate = "{}%".format(round((fpy_good/total_produced) * 100, 2)) if total_produced > 0 else u'—'
|
||||
# 直通良品数(FPY = First Pass Yield)
|
||||
fpy_count = fpy_good
|
||||
|
||||
conn.close()
|
||||
|
||||
@ -965,7 +974,7 @@ def dashboard():
|
||||
shipments_count = (ship['total'] or 0) if ship else 0
|
||||
|
||||
return jsonify({
|
||||
'fpyRate': fpy_rate,
|
||||
'fpyCount': fpy_count,
|
||||
'goodRate': rate,
|
||||
'badRate': bad_rate,
|
||||
'shipments': shipments_count,
|
||||
@ -1370,7 +1379,7 @@ def upload_stats():
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
platform_name = {'pdd': '拼多多', 'yt': '圆通', 'tx': '兔喜'}.get(platform, platform)
|
||||
platform_name = {'pdd': '拼多多', 'yt': '圆通', 'tx': '兔喜', 'mt': '美团', 'drf': '大润发', 'std': '标准版'}.get(platform, platform)
|
||||
log('upload_stats', json.dumps({'good': good, 'bad': bad, 'fpy_good': fpy_good, 'platform': platform, 'details_count': len(details)}))
|
||||
notify_superadmin('上传良/不良统计', f"平台: {platform_name}, 良品: {good}, 不良品: {bad}, 直通良品: {fpy_good}")
|
||||
return jsonify({'ok': True})
|
||||
@ -1404,9 +1413,10 @@ def upload_repairs():
|
||||
@require_login
|
||||
@require_any_role('admin','superadmin')
|
||||
def upload_repair_record():
|
||||
"""上传返修记录(含设备SN、机种、不良原因、维修方法、图片)"""
|
||||
"""上传返修记录(含设备SN、机种、不良现象、不良原因、维修方法、图片)"""
|
||||
sn = request.form.get('sn', '').strip()
|
||||
machine_type = request.form.get('machine_type', '').strip()
|
||||
defect_symptom = request.form.get('defect_symptom', '').strip()
|
||||
defect_reason = request.form.get('defect_reason', '').strip()
|
||||
repair_method = request.form.get('repair_method', '').strip()
|
||||
|
||||
@ -1415,6 +1425,8 @@ def upload_repair_record():
|
||||
return jsonify({'error': '设备SN为必填项'}), 400
|
||||
if not machine_type:
|
||||
return jsonify({'error': '机种为必填项'}), 400
|
||||
if not defect_symptom:
|
||||
return jsonify({'error': '不良现象为必填项'}), 400
|
||||
if not defect_reason:
|
||||
return jsonify({'error': '不良原因为必填项'}), 400
|
||||
|
||||
@ -1464,14 +1476,14 @@ def upload_repair_record():
|
||||
now = get_beijing_time()
|
||||
images_json = json.dumps(image_filenames) if image_filenames else ''
|
||||
|
||||
c.execute('INSERT INTO repair_records(sn, machine_type, defect_reason, repair_method, images, ts) VALUES(?,?,?,?,?,?)',
|
||||
(sn, machine_type, defect_reason, repair_method, images_json, now))
|
||||
c.execute('INSERT INTO repair_records(sn, machine_type, defect_symptom, defect_reason, repair_method, images, ts) VALUES(?,?,?,?,?,?,?)',
|
||||
(sn, machine_type, defect_symptom, defect_reason, repair_method, images_json, now))
|
||||
conn.commit()
|
||||
record_id = c.lastrowid
|
||||
conn.close()
|
||||
|
||||
log('upload_repair_record', json.dumps({'sn': sn, 'machine_type': machine_type, 'defect_reason': defect_reason, 'images_count': len(image_filenames)}))
|
||||
notify_superadmin('上传返修记录', f"SN: {sn}, 机种: {machine_type}, 不良原因: {defect_reason}")
|
||||
log('upload_repair_record', json.dumps({'sn': sn, 'machine_type': machine_type, 'defect_symptom': defect_symptom, 'defect_reason': defect_reason, 'images_count': len(image_filenames)}))
|
||||
notify_superadmin('上传返修记录', f"SN: {sn}, 机种: {machine_type}, 不良现象: {defect_symptom}, 不良原因: {defect_reason}")
|
||||
return jsonify({'ok': True, 'id': record_id})
|
||||
|
||||
|
||||
@ -1487,16 +1499,16 @@ def list_repair_records():
|
||||
|
||||
if sn_query:
|
||||
# 支持模糊搜索
|
||||
c.execute('SELECT id, sn, machine_type, defect_reason, repair_method, images, ts FROM repair_records WHERE sn LIKE ? ORDER BY id DESC', (f'%{sn_query}%',))
|
||||
c.execute('SELECT id, sn, machine_type, defect_symptom, defect_reason, repair_method, images, ts, tracking_number FROM repair_records WHERE sn LIKE ? ORDER BY id DESC', (f'%{sn_query}%',))
|
||||
elif limit:
|
||||
try:
|
||||
lim = int(limit)
|
||||
c.execute('SELECT id, sn, machine_type, defect_reason, repair_method, images, ts FROM repair_records ORDER BY id DESC LIMIT ?', (lim,))
|
||||
c.execute('SELECT id, sn, machine_type, defect_symptom, defect_reason, repair_method, images, ts, tracking_number FROM repair_records ORDER BY id DESC LIMIT ?', (lim,))
|
||||
except:
|
||||
c.execute('SELECT id, sn, machine_type, defect_reason, repair_method, images, ts FROM repair_records ORDER BY id DESC')
|
||||
c.execute('SELECT id, sn, machine_type, defect_symptom, defect_reason, repair_method, images, ts, tracking_number FROM repair_records ORDER BY id DESC')
|
||||
else:
|
||||
# 不限制数量,返回所有记录
|
||||
c.execute('SELECT id, sn, machine_type, defect_reason, repair_method, images, ts FROM repair_records ORDER BY id DESC')
|
||||
c.execute('SELECT id, sn, machine_type, defect_symptom, defect_reason, repair_method, images, ts, tracking_number FROM repair_records ORDER BY id DESC')
|
||||
|
||||
rows = [dict(r) for r in c.fetchall()]
|
||||
conn.close()
|
||||
@ -1549,7 +1561,7 @@ def export_repair_records():
|
||||
|
||||
# 获取选中的记录
|
||||
placeholders = ','.join(['?' for _ in record_ids])
|
||||
c.execute(f'SELECT id, sn, machine_type, defect_reason, repair_method, images, ts FROM repair_records WHERE id IN ({placeholders}) ORDER BY id DESC', record_ids)
|
||||
c.execute(f'SELECT id, sn, machine_type, defect_symptom, defect_reason, repair_method, images, ts, tracking_number FROM repair_records WHERE id IN ({placeholders}) ORDER BY id DESC', record_ids)
|
||||
rows = [dict(r) for r in c.fetchall()]
|
||||
conn.close()
|
||||
|
||||
@ -1564,7 +1576,7 @@ def export_repair_records():
|
||||
from openpyxl.styles import Alignment
|
||||
|
||||
# 设置表头
|
||||
headers = ['序号', 'SN', '机种', '不良原因', '维修方法', '时间', '图片']
|
||||
headers = ['序号', 'SN', '机种', '不良现象', '不良原因', '维修方法', '快递单号', '时间', '图片']
|
||||
for col, header in enumerate(headers, 1):
|
||||
cell = ws.cell(row=1, column=col, value=header)
|
||||
cell.alignment = Alignment(horizontal='left', vertical='center')
|
||||
@ -1573,7 +1585,7 @@ def export_repair_records():
|
||||
# 序号列宽度缩小
|
||||
ws.column_dimensions['A'].width = 4.5
|
||||
# 图片列宽度更大
|
||||
ws.column_dimensions['G'].width = 60
|
||||
ws.column_dimensions['I'].width = 60
|
||||
|
||||
upload_dir = os.path.join(os.path.dirname(__file__), 'uploads', 'repair_images')
|
||||
|
||||
@ -1588,16 +1600,20 @@ def export_repair_records():
|
||||
cell2.alignment = data_alignment
|
||||
cell3 = ws.cell(row=row_idx, column=3, value=record.get('machine_type') or '')
|
||||
cell3.alignment = data_alignment
|
||||
cell4 = ws.cell(row=row_idx, column=4, value=record['defect_reason'])
|
||||
cell4 = ws.cell(row=row_idx, column=4, value=record.get('defect_symptom') or '')
|
||||
cell4.alignment = data_alignment
|
||||
cell5 = ws.cell(row=row_idx, column=5, value=record['repair_method'] or '')
|
||||
cell5 = ws.cell(row=row_idx, column=5, value=record['defect_reason'])
|
||||
cell5.alignment = data_alignment
|
||||
cell6 = ws.cell(row=row_idx, column=6, value=record['repair_method'] or '')
|
||||
cell6.alignment = data_alignment
|
||||
cell7 = ws.cell(row=row_idx, column=7, value=record.get('tracking_number') or '')
|
||||
cell7.alignment = data_alignment
|
||||
# 格式化时间:去掉T和时区,只显示到秒
|
||||
ts = record['ts']
|
||||
if ts:
|
||||
ts = ts.replace('T', ' ').split('.')[0].split('+')[0]
|
||||
cell6 = ws.cell(row=row_idx, column=6, value=ts)
|
||||
cell6.alignment = data_alignment
|
||||
cell8 = ws.cell(row=row_idx, column=8, value=ts)
|
||||
cell8.alignment = data_alignment
|
||||
|
||||
# 处理图片
|
||||
images = []
|
||||
@ -1611,7 +1627,7 @@ def export_repair_records():
|
||||
# 设置行高以容纳图片(增大到150像素)
|
||||
ws.row_dimensions[row_idx].height = 150
|
||||
|
||||
img_col = 7 # 图片列改为第7列
|
||||
img_col = 9 # 图片列改为第9列
|
||||
for img_idx, img_name in enumerate(images[:3]): # 最多显示3张图片
|
||||
img_path = os.path.join(upload_dir, secure_filename(img_name))
|
||||
if os.path.exists(img_path):
|
||||
@ -1634,7 +1650,7 @@ def export_repair_records():
|
||||
else:
|
||||
ws.cell(row=row_idx, column=img_col, value=f'图片不存在: {img_name}')
|
||||
else:
|
||||
ws.cell(row=row_idx, column=7, value='无图片')
|
||||
ws.cell(row=row_idx, column=9, value='无图片')
|
||||
|
||||
# 保存到内存
|
||||
output = BytesIO()
|
||||
@ -1661,7 +1677,7 @@ def get_repair_record(record_id):
|
||||
"""获取单条返修记录详情"""
|
||||
conn = get_db()
|
||||
c = conn.cursor()
|
||||
c.execute('SELECT id, sn, machine_type, defect_reason, repair_method, images, ts FROM repair_records WHERE id=?', (record_id,))
|
||||
c.execute('SELECT id, sn, machine_type, defect_symptom, defect_reason, repair_method, images, ts, tracking_number FROM repair_records WHERE id=?', (record_id,))
|
||||
row = c.fetchone()
|
||||
conn.close()
|
||||
|
||||
@ -1688,6 +1704,7 @@ def update_repair_record(record_id):
|
||||
data = request.get_json() or {}
|
||||
sn = data.get('sn', '').strip()
|
||||
machine_type = data.get('machine_type', '').strip()
|
||||
defect_symptom = data.get('defect_symptom', '').strip()
|
||||
defect_reason = data.get('defect_reason', '').strip()
|
||||
repair_method = data.get('repair_method', '').strip()
|
||||
|
||||
@ -1696,6 +1713,8 @@ def update_repair_record(record_id):
|
||||
return jsonify({'error': '设备SN为必填项'}), 400
|
||||
if not machine_type:
|
||||
return jsonify({'error': '机种为必填项'}), 400
|
||||
if not defect_symptom:
|
||||
return jsonify({'error': '不良现象为必填项'}), 400
|
||||
if not defect_reason:
|
||||
return jsonify({'error': '不良原因为必填项'}), 400
|
||||
|
||||
@ -1717,15 +1736,211 @@ def update_repair_record(record_id):
|
||||
conn.close()
|
||||
return jsonify({'error': '记录不存在'}), 404
|
||||
|
||||
c.execute('UPDATE repair_records SET sn=?, machine_type=?, defect_reason=?, repair_method=? WHERE id=?',
|
||||
(sn, machine_type, defect_reason, repair_method, record_id))
|
||||
# 获取快递单号(可选)
|
||||
tracking_number = data.get('tracking_number', '').strip() if 'tracking_number' in data else None
|
||||
|
||||
if tracking_number is not None:
|
||||
c.execute('UPDATE repair_records SET sn=?, machine_type=?, defect_symptom=?, defect_reason=?, repair_method=?, tracking_number=? WHERE id=?',
|
||||
(sn, machine_type, defect_symptom, defect_reason, repair_method, tracking_number, record_id))
|
||||
else:
|
||||
c.execute('UPDATE repair_records SET sn=?, machine_type=?, defect_symptom=?, defect_reason=?, repair_method=? WHERE id=?',
|
||||
(sn, machine_type, defect_symptom, defect_reason, repair_method, record_id))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
log('update_repair_record', json.dumps({'id': record_id, 'sn': sn, 'defect_reason': defect_reason}))
|
||||
log('update_repair_record', json.dumps({'id': record_id, 'sn': sn, 'defect_symptom': defect_symptom, 'defect_reason': defect_reason}))
|
||||
return jsonify({'ok': True})
|
||||
|
||||
|
||||
@app.post('/api/repair-record/<int:record_id>/images')
|
||||
@require_login
|
||||
@require_any_role('admin','superadmin')
|
||||
def update_repair_images(record_id):
|
||||
"""更新返修记录的图片(支持添加新图片和删除现有图片)"""
|
||||
conn = get_db()
|
||||
c = conn.cursor()
|
||||
|
||||
# 检查记录是否存在
|
||||
c.execute('SELECT images FROM repair_records WHERE id=?', (record_id,))
|
||||
row = c.fetchone()
|
||||
if not row:
|
||||
conn.close()
|
||||
return jsonify({'error': '记录不存在'}), 404
|
||||
|
||||
# 获取现有图片列表
|
||||
existing_images = []
|
||||
if row['images']:
|
||||
try:
|
||||
existing_images = json.loads(row['images'])
|
||||
except:
|
||||
existing_images = []
|
||||
|
||||
# 获取要保留的图片列表(从表单中)
|
||||
keep_images = request.form.getlist('keep_images')
|
||||
|
||||
# 删除不再保留的图片文件
|
||||
upload_dir = os.path.join(os.path.dirname(__file__), 'uploads', 'repair_images')
|
||||
for img in existing_images:
|
||||
if img not in keep_images:
|
||||
img_path = os.path.join(upload_dir, secure_filename(img))
|
||||
if os.path.exists(img_path):
|
||||
try:
|
||||
os.remove(img_path)
|
||||
except:
|
||||
pass
|
||||
|
||||
# 保留的图片
|
||||
new_images = [img for img in keep_images if img in existing_images]
|
||||
|
||||
# 处理新上传的图片
|
||||
os.makedirs(upload_dir, exist_ok=True)
|
||||
files = request.files.getlist('new_images')
|
||||
|
||||
if len(new_images) + len(files) > 10:
|
||||
conn.close()
|
||||
return jsonify({'error': '最多保留10张图片'}), 400
|
||||
|
||||
for f in files:
|
||||
if f and f.filename:
|
||||
ext = f.filename.rsplit('.', 1)[-1].lower() if '.' in f.filename else ''
|
||||
if ext not in ['jpg', 'jpeg', 'png', 'gif']:
|
||||
conn.close()
|
||||
return jsonify({'error': f'不支持的图片格式: {ext}'}), 400
|
||||
|
||||
f.seek(0, 2)
|
||||
size = f.tell()
|
||||
f.seek(0)
|
||||
if size > 10 * 1024 * 1024:
|
||||
conn.close()
|
||||
return jsonify({'error': f'图片 {f.filename} 超过10MB限制'}), 400
|
||||
|
||||
import time
|
||||
timestamp = int(time.time() * 1000)
|
||||
filename = f"{timestamp}_{secure_filename(f.filename)}"
|
||||
filepath = os.path.join(upload_dir, filename)
|
||||
f.save(filepath)
|
||||
new_images.append(filename)
|
||||
|
||||
# 更新数据库
|
||||
images_json = json.dumps(new_images) if new_images else ''
|
||||
c.execute('UPDATE repair_records SET images=? WHERE id=?', (images_json, record_id))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
log('update_repair_images', json.dumps({'id': record_id, 'images_count': len(new_images)}))
|
||||
return jsonify({'ok': True, 'images': new_images})
|
||||
|
||||
|
||||
@app.post('/api/repair-records/batch-tracking')
|
||||
@require_login
|
||||
@require_any_role('admin','superadmin')
|
||||
def batch_update_tracking():
|
||||
"""批量更新返修记录的快递单号"""
|
||||
data = request.get_json() or {}
|
||||
record_ids = data.get('ids', [])
|
||||
tracking_number = data.get('tracking_number', '').strip()
|
||||
|
||||
if not record_ids:
|
||||
return jsonify({'error': '请选择要更新的记录'}), 400
|
||||
if not tracking_number:
|
||||
return jsonify({'error': '请输入快递单号'}), 400
|
||||
|
||||
conn = get_db()
|
||||
c = conn.cursor()
|
||||
|
||||
placeholders = ','.join(['?' for _ in record_ids])
|
||||
c.execute(f'UPDATE repair_records SET tracking_number=? WHERE id IN ({placeholders})',
|
||||
[tracking_number] + record_ids)
|
||||
updated_count = c.rowcount
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
log('batch_update_tracking', json.dumps({'ids': record_ids, 'tracking_number': tracking_number}))
|
||||
return jsonify({'ok': True, 'updated': updated_count})
|
||||
|
||||
|
||||
@app.get('/api/tracking/<tracking_number>')
|
||||
@require_login
|
||||
def query_tracking(tracking_number):
|
||||
"""查询快递物流信息"""
|
||||
import hashlib
|
||||
import urllib.request
|
||||
import urllib.parse
|
||||
|
||||
# 快递100 API配置
|
||||
key = 'TsoMoTqO4243'
|
||||
customer = 'EE2065B5BA6BE5ECEF52EB64F604532B'
|
||||
url = 'https://poll.kuaidi100.com/poll/query.do'
|
||||
|
||||
# 根据单号前缀自动识别快递公司
|
||||
tracking_number = tracking_number.strip().upper()
|
||||
com = 'auto' # 默认自动识别
|
||||
|
||||
# 常见快递公司单号前缀
|
||||
if tracking_number.startswith('SF'):
|
||||
com = 'shunfeng'
|
||||
elif tracking_number.startswith('YT'):
|
||||
com = 'yuantong'
|
||||
elif tracking_number.startswith('ZT'):
|
||||
com = 'zhongtong'
|
||||
elif tracking_number.startswith('YD'):
|
||||
com = 'yunda'
|
||||
elif tracking_number.startswith('JD') or tracking_number.startswith('JDVB'):
|
||||
com = 'jd'
|
||||
elif tracking_number.startswith('EMS') or tracking_number.startswith('10'):
|
||||
com = 'ems'
|
||||
elif tracking_number.startswith('STO'):
|
||||
com = 'shentong'
|
||||
elif tracking_number.startswith('DBL'):
|
||||
com = 'debangkuaidi'
|
||||
|
||||
param = {
|
||||
'com': com,
|
||||
'num': tracking_number,
|
||||
'phone': '',
|
||||
'from': '',
|
||||
'to': '',
|
||||
'resultv2': '1',
|
||||
'show': '0',
|
||||
'order': 'desc'
|
||||
}
|
||||
param_str = json.dumps(param)
|
||||
|
||||
# 签名加密
|
||||
temp_sign = param_str + key + customer
|
||||
md = hashlib.md5()
|
||||
md.update(temp_sign.encode())
|
||||
sign = md.hexdigest().upper()
|
||||
|
||||
request_data = urllib.parse.urlencode({'customer': customer, 'param': param_str, 'sign': sign}).encode('utf-8')
|
||||
|
||||
try:
|
||||
req = urllib.request.Request(url, data=request_data, method='POST')
|
||||
req.add_header('Content-Type', 'application/x-www-form-urlencoded')
|
||||
with urllib.request.urlopen(req, timeout=10) as response:
|
||||
result = json.loads(response.read().decode('utf-8'))
|
||||
|
||||
# 快递100返回格式: {"message":"ok","state":"3","status":"200","condition":"F00","ischeck":"1","com":"jd","nu":"JDVB42605717549","data":[...]}
|
||||
if result.get('status') == '200' or result.get('message') == 'ok':
|
||||
return jsonify({
|
||||
'ok': True,
|
||||
'com': result.get('com', com),
|
||||
'nu': result.get('nu', tracking_number),
|
||||
'state': result.get('state', '0'),
|
||||
'ischeck': result.get('ischeck', '0'),
|
||||
'data': result.get('data', [])
|
||||
})
|
||||
else:
|
||||
return jsonify({
|
||||
'ok': False,
|
||||
'error': result.get('message', '查询失败'),
|
||||
'returnCode': result.get('returnCode', '')
|
||||
})
|
||||
except Exception as e:
|
||||
log('tracking_query_error', str(e))
|
||||
return jsonify({'ok': False, 'error': f'查询失败: {str(e)}'}), 500
|
||||
|
||||
|
||||
@app.delete('/api/repair-record/<int:record_id>')
|
||||
@require_login
|
||||
@require_any_role('superadmin')
|
||||
@ -1817,7 +2032,7 @@ def upload_shipments():
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
platform_name = {'pdd': '拼多多', 'yt': '圆通', 'tx': '兔喜'}.get(platform, platform)
|
||||
platform_name = {'pdd': '拼多多', 'yt': '圆通', 'tx': '兔喜', 'mt': '美团', 'drf': '大润发', 'std': '标准版'}.get(platform, platform)
|
||||
log_data = {'date': date, 'qty': qty, 'to': to, 'platform': platform}
|
||||
if box_no:
|
||||
log_data['box_no'] = box_no
|
||||
@ -2893,7 +3108,7 @@ def query_shipment_by_sn():
|
||||
# 解析 JSON 数据
|
||||
shipment_info = json.loads(result)
|
||||
platform = shipment_info.get('platform', 'pdd') # 默认拼多多
|
||||
platform_name = {'pdd': '拼多多', 'yt': '圆通', 'tx': '兔喜'}.get(platform, platform)
|
||||
platform_name = {'pdd': '拼多多', 'yt': '圆通', 'tx': '兔喜', 'mt': '美团', 'drf': '大润发', 'std': '标准版'}.get(platform, platform)
|
||||
return jsonify({
|
||||
'found': True,
|
||||
'sn': sn,
|
||||
@ -2938,7 +3153,7 @@ def query_shipment_by_box():
|
||||
shipment_info = json.loads(data)
|
||||
if shipment_info.get('box') == box_no:
|
||||
platform = shipment_info.get('platform', 'pdd')
|
||||
platform_name = {'pdd': '拼多多', 'yt': '圆通', 'tx': '兔喜'}.get(platform, platform)
|
||||
platform_name = {'pdd': '拼多多', 'yt': '圆通', 'tx': '兔喜', 'mt': '美团', 'drf': '大润发', 'std': '标准版'}.get(platform, platform)
|
||||
matched_records.append({
|
||||
'sn': sn.decode('utf-8') if isinstance(sn, bytes) else sn,
|
||||
'date': shipment_info.get('date'),
|
||||
|
||||
46
server/synquery.py
Normal file
46
server/synquery.py
Normal file
@ -0,0 +1,46 @@
|
||||
# coding = utf-8
|
||||
import hashlib
|
||||
import json
|
||||
|
||||
import requests
|
||||
|
||||
|
||||
class KuaiDi100:
|
||||
def __init__(self):
|
||||
self.key = '' # TODO 客户授权key
|
||||
self.customer = '' # TODO 查询公司编号
|
||||
self.url = 'https://poll.kuaidi100.com/poll/query.do' # 请求地址
|
||||
|
||||
def track(self, com, num, phone, ship_from, ship_to):
|
||||
"""
|
||||
物流轨迹实时查询
|
||||
:param com: 查询的快递公司的编码,一律用小写字母
|
||||
:param num: 查询的快递单号,单号的最大长度是32个字符
|
||||
:param phone: 收件人或寄件人的手机号或固话(也可以填写后四位,如果是固话,请不要上传分机号)
|
||||
:param ship_from: 出发地城市,省-市-区,非必填,填了有助于提升签收状态的判断的准确率,请尽量提供
|
||||
:param ship_to: 目的地城市,省-市-区,非必填,填了有助于提升签收状态的判断的准确率,且到达目的地后会加大监控频率,请尽量提供
|
||||
:return: requests.Response.text
|
||||
"""
|
||||
param = {
|
||||
'com': com,
|
||||
'num': num,
|
||||
'phone': phone,
|
||||
'from': ship_from,
|
||||
'to': ship_to,
|
||||
'resultv2': '1', # 添加此字段表示开通行政区域解析功能。0:关闭(默认),1:开通行政区域解析功能,2:开通行政解析功能并且返回出发、目的及当前城市信息
|
||||
'show': '0', # 返回数据格式。0:json(默认),1:xml,2:html,3:text
|
||||
'order': 'desc' # 返回结果排序方式。desc:降序(默认),asc:升序
|
||||
}
|
||||
param_str = json.dumps(param) # 转json字符串
|
||||
|
||||
# 签名加密, 用于验证身份, 按param + key + customer 的顺序进行MD5加密(注意加密后字符串要转大写), 不需要“+”号
|
||||
temp_sign = param_str + self.key + self.customer
|
||||
md = hashlib.md5()
|
||||
md.update(temp_sign.encode())
|
||||
sign = md.hexdigest().upper()
|
||||
request_data = {'customer': self.customer, 'param': param_str, 'sign': sign}
|
||||
return requests.post(self.url, request_data).text # 发送请求
|
||||
|
||||
|
||||
result = KuaiDi100().track('yuantong', 'YT9693083639795', '', '广东省江门市', '广东省深圳市')
|
||||
print(result)
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 45 KiB |
BIN
server/uploads/repair_images/1765521258711_png
Normal file
BIN
server/uploads/repair_images/1765521258711_png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.0 KiB |
BIN
server/uploads/repair_images/1765522104011_1.png
Normal file
BIN
server/uploads/repair_images/1765522104011_1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 680 KiB |
BIN
server/uploads/repair_images/1765525223925_1.png
Normal file
BIN
server/uploads/repair_images/1765525223925_1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 680 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 64 KiB |
BIN
server/uploads/repair_images/1765528878210_IMG_5165.jpeg
Normal file
BIN
server/uploads/repair_images/1765528878210_IMG_5165.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 100 KiB |
Loading…
Reference in New Issue
Block a user