diff --git a/frontend/js/api.js b/frontend/js/api.js index 47e3395..035833f 100644 --- a/frontend/js/api.js +++ b/frontend/js/api.js @@ -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', { diff --git a/frontend/js/components/dashboard.js b/frontend/js/components/dashboard.js index 7f7c285..59cb468 100644 --- a/frontend/js/components/dashboard.js +++ b/frontend/js/components/dashboard.js @@ -2,7 +2,7 @@ const Dashboard = (() => { const truckIcon = `
`; const metricsIcons = { - '直通良品率': '', + '直通良品数': '', '良品率': '', '发货数量': truckIcon, '不良数量': '', @@ -886,7 +886,7 @@ const Dashboard = (() => { return `
- ${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')} diff --git a/frontend/js/components/shipments.js b/frontend/js/components/shipments.js index f538235..88bfeaf 100644 --- a/frontend/js/components/shipments.js +++ b/frontend/js/components/shipments.js @@ -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 () => { + + +
@@ -234,6 +237,9 @@ Router.register('/upload/shipments', async () => { + + +
diff --git a/frontend/js/components/upload.js b/frontend/js/components/upload.js index 763f861..18a20d8 100644 --- a/frontend/js/components/upload.js +++ b/frontend/js/components/upload.js @@ -177,15 +177,19 @@ const Upload = (() => {
+
+ + + +
+
- - +
@@ -242,6 +246,7 @@ const Upload = (() => { + @@ -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 = (() => {
SN: ${r.sn} - ${ts} +
+
${ts}
+ ${r.tracking_number ? `📦 ${r.tracking_number}` : ''} +
${r.machine_type ? `${r.machine_type}` : ''} + ${r.defect_symptom ? `${r.defect_symptom}` : ''} ${r.defect_reason} ${imgCount > 0 ? `${imgCount}张图片` : ''}
@@ -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 = ` +
+
+

📦 物流查询

+ +
+
+
+
+
正在查询物流信息...
+
+
+
+ `; + 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 = ` +
+
+
${comName}
+
${trackingNumber}
+
+ ${stateName} +
+
+ ${result.data.map((item, idx) => ` +
+
+
${item.time || item.ftime || ''}
+
${item.context || ''}
+
+ `).join('')} +
+ `; + } else { + contentEl.innerHTML = ` +
+
📭
+
${result.error || '暂无物流信息'}
+
+ `; + } + } catch(e) { + document.getElementById('tracking-content').innerHTML = ` +
+
+
查询失败: ${e.message || '网络错误'}
+
+ `; + } + }; + }); }; // 更新全选框状态 @@ -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 ? `
${record.images.map(img => ` @@ -1197,21 +1280,38 @@ const Upload = (() => { ${machineTypeOptions.map(opt => ``).join('')}
+
+ + +
- - +
- - ${imagesHtml} -
编辑模式暂不支持修改图片
+ + +
+
+ +
+ ${images.map(img => ` +
+ + +
+ `).join('')} +
+
+ +
点击或拖拽添加新图片
+
+
+
最多10张图片,点击×删除
@@ -1226,6 +1326,10 @@ const Upload = (() => {
机种
${record.machine_type || '未知'}
+
+
不良现象
+
${record.defect_symptom || '未填写'}
+
不良原因
${record.defect_reason} @@ -1234,6 +1338,10 @@ const Upload = (() => {
维修方法
${record.repair_method || '无'}
+
+
快递单号
+
${record.tracking_number ? `📦 ${record.tracking_number}` : '未填写'}
+
上传时间
${ts}
@@ -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 = ` + + + `; + 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 = ` +
+
+

添加快递单号

+ +
+
+
+ 已选择 ${selectedIds.size} 条记录 +
+
+ + +
+
+ + +
+
+
+ `; + 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; diff --git a/frontend/js/router.js b/frontend/js/router.js index bd9eddf..d977afd 100644 --- a/frontend/js/router.js +++ b/frontend/js/router.js @@ -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) => { diff --git a/server/app.py b/server/app.py index ccd4a95..c63e463 100644 --- a/server/app.py +++ b/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//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/') +@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/') @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'), diff --git a/server/synquery.py b/server/synquery.py new file mode 100644 index 0000000..8b2c67f --- /dev/null +++ b/server/synquery.py @@ -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) diff --git a/server/uploads/repair_images/1765519655516_5e3d7696-81a5-412d-ad1e-699921618764.jpg b/server/uploads/repair_images/1765519655516_5e3d7696-81a5-412d-ad1e-699921618764.jpg new file mode 100644 index 0000000..82768b2 Binary files /dev/null and b/server/uploads/repair_images/1765519655516_5e3d7696-81a5-412d-ad1e-699921618764.jpg differ diff --git a/server/uploads/repair_images/1765521258711_png b/server/uploads/repair_images/1765521258711_png new file mode 100644 index 0000000..700031a Binary files /dev/null and b/server/uploads/repair_images/1765521258711_png differ diff --git a/server/uploads/repair_images/1765522104011_1.png b/server/uploads/repair_images/1765522104011_1.png new file mode 100644 index 0000000..69f0067 Binary files /dev/null and b/server/uploads/repair_images/1765522104011_1.png differ diff --git a/server/uploads/repair_images/1765525223925_1.png b/server/uploads/repair_images/1765525223925_1.png new file mode 100644 index 0000000..69f0067 Binary files /dev/null and b/server/uploads/repair_images/1765525223925_1.png differ diff --git a/server/uploads/repair_images/1765526570241_20251210153156_81_205.jpg b/server/uploads/repair_images/1765526570241_20251210153156_81_205.jpg new file mode 100644 index 0000000..f096cf8 Binary files /dev/null and b/server/uploads/repair_images/1765526570241_20251210153156_81_205.jpg differ diff --git a/server/uploads/repair_images/1765528878210_IMG_5165.jpeg b/server/uploads/repair_images/1765528878210_IMG_5165.jpeg new file mode 100644 index 0000000..7cfbb29 Binary files /dev/null and b/server/uploads/repair_images/1765528878210_IMG_5165.jpeg differ