完善返修记录上传的功能
This commit is contained in:
parent
f5432ba8b0
commit
8916fc56ec
@ -133,6 +133,23 @@ const API = (() => {
|
||||
},
|
||||
uploadStats: data => request('/upload/stats', { method: 'POST', body: JSON.stringify(data) }),
|
||||
uploadRepairs: data => request('/upload/repairs', { method: 'POST', body: JSON.stringify(data) }),
|
||||
uploadRepairRecord: formData => uploadFile('/upload/repair-record', formData),
|
||||
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) }),
|
||||
deleteRepairRecord: id => request(`/repair-record/${id}`, { method: 'DELETE' }),
|
||||
exportRepairRecords: ids => {
|
||||
// 使用fetch直接下载文件
|
||||
return fetch('/api/export/repair-records', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ ids })
|
||||
}).then(res => {
|
||||
if(!res.ok) return res.json().then(d => Promise.reject(d));
|
||||
return res.blob();
|
||||
});
|
||||
},
|
||||
uploadDefects: data => request('/upload/defects', { method: 'POST', body: JSON.stringify(data) }),
|
||||
uploadDefectsFile: file => {
|
||||
const fd = new FormData();
|
||||
|
||||
@ -134,17 +134,128 @@ const Upload = (() => {
|
||||
`);
|
||||
}
|
||||
|
||||
// 不良原因选项
|
||||
const defectReasonOptions = [
|
||||
'无不良',
|
||||
'外观不良',
|
||||
'功能异常',
|
||||
'连接问题',
|
||||
'电源故障',
|
||||
'软件问题',
|
||||
'物理损坏',
|
||||
'其他'
|
||||
];
|
||||
|
||||
// 机种选项
|
||||
const machineTypeOptions = [
|
||||
'拼多多',
|
||||
'圆通',
|
||||
'兔喜',
|
||||
'美团',
|
||||
'大润发',
|
||||
'标准版'
|
||||
];
|
||||
|
||||
async function renderRepairs(){
|
||||
return section('返修记录',`
|
||||
${numberInput('repair-qty','返修完成数量')}
|
||||
${textarea('repair-note','备注(可选)','例如:批次号、问题描述等')}
|
||||
<div class="actions"><button class="btn" id="repairs-upload">上传</button></div>
|
||||
<div style="margin-top:12px">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
|
||||
<span style="font-weight:600">最新记录</span>
|
||||
<button class="btn btn-secondary" id="repairs-show-all" style="font-size:12px;padding:4px 8px">查看全部</button>
|
||||
return section('返修记录上传',`
|
||||
<div class="form-group" style="margin-bottom:16px">
|
||||
<label style="display:block;font-weight:600;margin-bottom:6px">
|
||||
设备SN <span style="color:#ef4444">*</span>
|
||||
</label>
|
||||
<input type="text" id="repair-sn" class="input" placeholder="请输入设备序列号" style="width:100%" />
|
||||
<div id="repair-sn-error" style="color:#ef4444;font-size:12px;margin-top:4px;display:none"></div>
|
||||
</div>
|
||||
<ul id="repairs-list" class="list" style="max-height:300px;overflow-y:auto"></ul>
|
||||
|
||||
<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-machine-type" class="input" style="width:100%">
|
||||
<option value="">请选择机种</option>
|
||||
${machineTypeOptions.map(opt => `<option value="${opt}">${opt}</option>`).join('')}
|
||||
</select>
|
||||
<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>
|
||||
<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" />
|
||||
<div id="repair-defect-error" style="color:#ef4444;font-size:12px;margin-top:4px;display:none"></div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" style="margin-bottom:16px" id="repair-method-group">
|
||||
<label style="display:block;font-weight:600;margin-bottom:6px">
|
||||
维修方法 <span id="repair-method-required" style="color:#ef4444;display:none">*</span>
|
||||
</label>
|
||||
<textarea id="repair-method" class="input" placeholder="请描述维修方法" rows="3" style="width:100%;resize:vertical"></textarea>
|
||||
<div id="repair-method-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">图片上传</label>
|
||||
<div style="background:rgba(79,140,255,0.08);border:1px dashed rgba(79,140,255,0.4);border-radius:8px;padding:16px;text-align:center;cursor:pointer;transition:all 0.2s" id="repair-image-dropzone">
|
||||
<input type="file" id="repair-images" multiple accept="image/jpeg,image/png,image/gif" style="display:none" />
|
||||
<div style="color:var(--primary);margin-bottom:8px">
|
||||
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
||||
<polyline points="17 8 12 3 7 8"/>
|
||||
<line x1="12" y1="3" x2="12" y2="15"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div style="font-size:14px;color:var(--text-1)">点击或拖拽图片到此处上传</div>
|
||||
<div style="font-size:12px;color:var(--text-2);margin-top:4px">支持 JPG/PNG/GIF,单张最大10MB,最多10张</div>
|
||||
</div>
|
||||
<div id="repair-image-preview" style="display:flex;flex-wrap:wrap;gap:8px;margin-top:12px"></div>
|
||||
<div id="repair-image-error" style="color:#ef4444;font-size:12px;margin-top:4px;display:none"></div>
|
||||
</div>
|
||||
|
||||
<div class="actions" style="display:flex;gap:12px">
|
||||
<button class="btn" id="repairs-upload" style="flex:1">
|
||||
<span id="repairs-upload-text">提交</span>
|
||||
<span id="repairs-upload-loading" style="display:none">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="animation:spin 1s linear infinite">
|
||||
<circle cx="12" cy="12" r="10" stroke-opacity="0.25"/>
|
||||
<path d="M12 2a10 10 0 0 1 10 10" stroke-opacity="1"/>
|
||||
</svg>
|
||||
提交中...
|
||||
</span>
|
||||
</button>
|
||||
<button class="btn btn-secondary" id="repairs-clear" style="flex:0 0 auto">清空</button>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
|
||||
#repair-image-dropzone:hover { background:rgba(79,140,255,0.12); border-color:var(--primary); }
|
||||
#repair-image-dropzone.dragover { background:rgba(79,140,255,0.15); border-color:var(--primary); border-style:solid; }
|
||||
</style>
|
||||
|
||||
<div style="margin-top:20px">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;flex-wrap:wrap;gap:8px">
|
||||
<span style="font-weight:600">历史记录</span>
|
||||
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
|
||||
<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-export-btn" style="font-size:12px;padding:4px 8px">导出选中</button>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
|
||||
<div id="repairs-list-info" style="font-size:12px;color:var(--text-2)"></div>
|
||||
<div style="display:flex;gap:8px;align-items:center">
|
||||
<label style="font-size:12px;color:var(--text-2);cursor:pointer;display:flex;align-items:center;gap:4px">
|
||||
<input type="checkbox" id="repairs-select-all" style="cursor:pointer" />
|
||||
全选
|
||||
</label>
|
||||
<span id="repairs-selected-count" style="font-size:12px;color:var(--primary)"></span>
|
||||
</div>
|
||||
</div>
|
||||
<ul id="repairs-list" class="list" style="max-height:500px;overflow-y:auto"></ul>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
@ -695,57 +806,714 @@ const Upload = (() => {
|
||||
|
||||
async function bindRepairsEvents(){
|
||||
const btn = document.getElementById('repairs-upload');
|
||||
const clearBtn = document.getElementById('repairs-clear');
|
||||
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 methodInput = document.getElementById('repair-method');
|
||||
const methodGroup = document.getElementById('repair-method-group');
|
||||
const imageInput = document.getElementById('repair-images');
|
||||
const dropzone = document.getElementById('repair-image-dropzone');
|
||||
const previewContainer = document.getElementById('repair-image-preview');
|
||||
const snError = document.getElementById('repair-sn-error');
|
||||
const machineTypeError = document.getElementById('repair-machine-type-error');
|
||||
const defectError = document.getElementById('repair-defect-error');
|
||||
const methodError = document.getElementById('repair-method-error');
|
||||
const methodRequired = document.getElementById('repair-method-required');
|
||||
const imageError = document.getElementById('repair-image-error');
|
||||
const uploadText = document.getElementById('repairs-upload-text');
|
||||
const uploadLoading = document.getElementById('repairs-upload-loading');
|
||||
|
||||
// 查看全部按钮
|
||||
addListener(showAllBtn, 'click', async ()=>{
|
||||
const listEl = document.getElementById('repairs-list');
|
||||
const data = await API.listRepairs().catch(()=>({list:[]}));
|
||||
if(listEl){
|
||||
if(data.list && data.list.length > 0){
|
||||
listEl.innerHTML = data.list.map(r=>{
|
||||
const ts = new Date(r.ts).toLocaleString('zh-CN');
|
||||
return `<li><span>数量: ${r.qty}</span><span class="badge">${r.note||'无备注'}</span><span style="font-size:11px;color:var(--text-2)">${ts}</span></li>`;
|
||||
// 存储待上传的图片文件
|
||||
let pendingImages = [];
|
||||
|
||||
// 显示/隐藏"其他"原因输入框,并根据不良原因决定维修方法是否必填
|
||||
addListener(defectSelect, 'change', () => {
|
||||
if(defectSelect.value === '其他') {
|
||||
defectOtherInput.style.display = 'block';
|
||||
defectOtherInput.focus();
|
||||
} else {
|
||||
defectOtherInput.style.display = 'none';
|
||||
defectOtherInput.value = '';
|
||||
}
|
||||
// 清除错误提示
|
||||
defectError.style.display = 'none';
|
||||
methodError.style.display = 'none';
|
||||
|
||||
// 条件性显示维修方法:无不良时可选,其他原因时必填
|
||||
if(defectSelect.value) {
|
||||
methodGroup.style.opacity = '1';
|
||||
methodInput.disabled = false;
|
||||
|
||||
if(defectSelect.value === '无不良') {
|
||||
// 无不良时维修方法可选
|
||||
methodRequired.style.display = 'none';
|
||||
methodInput.placeholder = '请描述维修方法(可选)';
|
||||
} else {
|
||||
// 其他原因时维修方法必填
|
||||
methodRequired.style.display = 'inline';
|
||||
methodInput.placeholder = '请描述维修方法(必填)';
|
||||
}
|
||||
} else {
|
||||
methodGroup.style.opacity = '0.5';
|
||||
methodInput.disabled = true;
|
||||
methodRequired.style.display = 'none';
|
||||
methodInput.placeholder = '请描述维修方法';
|
||||
}
|
||||
});
|
||||
|
||||
// 初始化维修方法状态
|
||||
methodGroup.style.opacity = '0.5';
|
||||
methodInput.disabled = true;
|
||||
|
||||
// SN输入验证
|
||||
addListener(snInput, 'input', () => {
|
||||
snError.style.display = 'none';
|
||||
});
|
||||
|
||||
// 图片上传处理
|
||||
const validateAndAddImages = (files) => {
|
||||
const validTypes = ['image/jpeg', 'image/png', 'image/gif'];
|
||||
const maxSize = 10 * 1024 * 1024; // 10MB
|
||||
const maxCount = 10;
|
||||
|
||||
for(const file of files) {
|
||||
if(pendingImages.length >= maxCount) {
|
||||
imageError.textContent = `最多上传${maxCount}张图片`;
|
||||
imageError.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
|
||||
if(!validTypes.includes(file.type)) {
|
||||
imageError.textContent = `不支持的格式: ${file.name},仅支持JPG/PNG/GIF`;
|
||||
imageError.style.display = 'block';
|
||||
continue;
|
||||
}
|
||||
|
||||
if(file.size > maxSize) {
|
||||
imageError.textContent = `图片 ${file.name} 超过10MB限制`;
|
||||
imageError.style.display = 'block';
|
||||
continue;
|
||||
}
|
||||
|
||||
// 检查是否已存在同名文件
|
||||
if(pendingImages.some(f => f.name === file.name && f.size === file.size)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
pendingImages.push(file);
|
||||
imageError.style.display = 'none';
|
||||
}
|
||||
|
||||
renderImagePreviews();
|
||||
};
|
||||
|
||||
const renderImagePreviews = () => {
|
||||
previewContainer.innerHTML = pendingImages.map((file, index) => {
|
||||
const url = URL.createObjectURL(file);
|
||||
return `
|
||||
<div style="position:relative;width:80px;height:80px;border-radius:6px;overflow:hidden;border:1px solid var(--border)">
|
||||
<img src="${url}" style="width:100%;height:100%;object-fit:cover" />
|
||||
<button type="button" data-index="${index}" class="remove-image-btn" style="position:absolute;top:2px;right:2px;width:20px;height:20px;border-radius:50%;background:rgba(0,0,0,0.6);color:#fff;border:none;cursor:pointer;font-size:14px;line-height:1;display:flex;align-items:center;justify-content:center">×</button>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
API.toast(`显示全部 ${data.list.length} 条记录`);
|
||||
|
||||
// 绑定删除按钮事件
|
||||
previewContainer.querySelectorAll('.remove-image-btn').forEach(btn => {
|
||||
btn.onclick = (e) => {
|
||||
e.stopPropagation();
|
||||
const index = parseInt(btn.dataset.index);
|
||||
pendingImages.splice(index, 1);
|
||||
renderImagePreviews();
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
// 点击上传区域
|
||||
addListener(dropzone, 'click', () => {
|
||||
imageInput.click();
|
||||
});
|
||||
|
||||
// 文件选择
|
||||
addListener(imageInput, 'change', () => {
|
||||
if(imageInput.files.length > 0) {
|
||||
validateAndAddImages(Array.from(imageInput.files));
|
||||
imageInput.value = ''; // 清空以便重复选择同一文件
|
||||
}
|
||||
});
|
||||
|
||||
// 拖拽上传
|
||||
addListener(dropzone, 'dragover', (e) => {
|
||||
e.preventDefault();
|
||||
dropzone.classList.add('dragover');
|
||||
});
|
||||
|
||||
addListener(dropzone, 'dragleave', () => {
|
||||
dropzone.classList.remove('dragover');
|
||||
});
|
||||
|
||||
addListener(dropzone, 'drop', (e) => {
|
||||
e.preventDefault();
|
||||
dropzone.classList.remove('dragover');
|
||||
if(e.dataTransfer.files.length > 0) {
|
||||
validateAndAddImages(Array.from(e.dataTransfer.files));
|
||||
}
|
||||
});
|
||||
|
||||
// 清空按钮
|
||||
addListener(clearBtn, 'click', () => {
|
||||
snInput.value = '';
|
||||
machineTypeSelect.value = '';
|
||||
defectSelect.value = '';
|
||||
defectOtherInput.value = '';
|
||||
defectOtherInput.style.display = 'none';
|
||||
methodInput.value = '';
|
||||
methodGroup.style.opacity = '0.5';
|
||||
methodInput.disabled = true;
|
||||
methodRequired.style.display = 'none';
|
||||
methodInput.placeholder = '请描述维修方法';
|
||||
pendingImages = [];
|
||||
previewContainer.innerHTML = '';
|
||||
snError.style.display = 'none';
|
||||
machineTypeError.style.display = 'none';
|
||||
defectError.style.display = 'none';
|
||||
methodError.style.display = 'none';
|
||||
imageError.style.display = 'none';
|
||||
API.toast('已清空表单');
|
||||
});
|
||||
|
||||
// 获取当前用户角色
|
||||
let userRole = window.__userRole || localStorage.getItem('userRole') || '';
|
||||
let isSuperAdmin = userRole === 'superadmin';
|
||||
|
||||
// 如果没有缓存角色,从服务器获取
|
||||
if (!userRole) {
|
||||
try {
|
||||
const userRes = await fetch('/api/auth/me', { credentials: 'include' });
|
||||
const userData = await userRes.json();
|
||||
userRole = userData.role || '';
|
||||
isSuperAdmin = userRole === 'superadmin';
|
||||
window.__userRole = userRole;
|
||||
localStorage.setItem('userRole', userRole);
|
||||
} catch (e) {
|
||||
console.error('Failed to get user role:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// 存储选中的记录ID
|
||||
let selectedIds = new Set();
|
||||
|
||||
// 更新选中计数
|
||||
const updateSelectedCount = () => {
|
||||
const countEl = document.getElementById('repairs-selected-count');
|
||||
if(countEl) {
|
||||
countEl.textContent = selectedIds.size > 0 ? `已选 ${selectedIds.size} 条` : '';
|
||||
}
|
||||
};
|
||||
|
||||
// 渲染单条记录的HTML
|
||||
const renderRecordItem = (r) => {
|
||||
const ts = new Date(r.ts).toLocaleString('zh-CN');
|
||||
const imgCount = (r.images || []).length;
|
||||
const isChecked = selectedIds.has(r.id);
|
||||
return `
|
||||
<li style="display:flex;gap:10px;padding:10px 0;border-bottom:1px solid var(--border)" data-id="${r.id}">
|
||||
<div style="flex:0 0 auto;padding-top:2px">
|
||||
<input type="checkbox" class="repair-checkbox" data-id="${r.id}" ${isChecked ? 'checked' : ''} style="cursor:pointer;width:16px;height:16px" />
|
||||
</div>
|
||||
<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>
|
||||
<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>` : ''}
|
||||
<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>
|
||||
${r.repair_method ? `<div style="font-size:12px;color:var(--text-2)">维修: ${r.repair_method}</div>` : ''}
|
||||
<div style="display:flex;gap:8px;margin-top:4px">
|
||||
<button class="btn btn-secondary repair-view-btn" data-id="${r.id}" style="font-size:11px;padding:2px 8px">查看</button>
|
||||
<button class="btn btn-secondary repair-edit-btn" data-id="${r.id}" style="font-size:11px;padding:2px 8px">编辑</button>
|
||||
${isSuperAdmin ? `<button class="btn btn-secondary repair-delete-btn" data-id="${r.id}" style="font-size:11px;padding:2px 8px;color:var(--danger)">删除</button>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
`;
|
||||
};
|
||||
|
||||
// 绑定列表项按钮事件
|
||||
const bindListItemEvents = () => {
|
||||
// 勾选框
|
||||
document.querySelectorAll('.repair-checkbox').forEach(cb => {
|
||||
cb.onchange = () => {
|
||||
const id = parseInt(cb.dataset.id);
|
||||
if(cb.checked) {
|
||||
selectedIds.add(id);
|
||||
} else {
|
||||
selectedIds.delete(id);
|
||||
}
|
||||
updateSelectedCount();
|
||||
updateSelectAllState();
|
||||
};
|
||||
});
|
||||
|
||||
// 查看按钮
|
||||
document.querySelectorAll('.repair-view-btn').forEach(btn => {
|
||||
btn.onclick = async () => {
|
||||
const id = btn.dataset.id;
|
||||
await showRepairDetailModal(id, false);
|
||||
};
|
||||
});
|
||||
|
||||
// 编辑按钮
|
||||
document.querySelectorAll('.repair-edit-btn').forEach(btn => {
|
||||
btn.onclick = async () => {
|
||||
const id = btn.dataset.id;
|
||||
await showRepairDetailModal(id, true);
|
||||
};
|
||||
});
|
||||
|
||||
// 删除按钮(仅超级管理员可见)
|
||||
document.querySelectorAll('.repair-delete-btn').forEach(btn => {
|
||||
btn.onclick = async () => {
|
||||
const id = btn.dataset.id;
|
||||
if(confirm('确定要删除这条返修记录吗?此操作不可恢复。')) {
|
||||
try {
|
||||
const result = await API.deleteRepairRecord(id);
|
||||
if(result.ok) {
|
||||
API.toast('删除成功');
|
||||
selectedIds.delete(parseInt(id));
|
||||
updateSelectedCount();
|
||||
refreshRepairsList();
|
||||
} else {
|
||||
API.toast(result.error || '删除失败');
|
||||
}
|
||||
} catch(e) {
|
||||
API.toast('删除失败: ' + (e.message || '网络错误'));
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
// 更新全选框状态
|
||||
const updateSelectAllState = () => {
|
||||
const selectAllCb = document.getElementById('repairs-select-all');
|
||||
const checkboxes = document.querySelectorAll('.repair-checkbox');
|
||||
if(selectAllCb && checkboxes.length > 0) {
|
||||
const allChecked = Array.from(checkboxes).every(cb => cb.checked);
|
||||
const someChecked = Array.from(checkboxes).some(cb => cb.checked);
|
||||
selectAllCb.checked = allChecked;
|
||||
selectAllCb.indeterminate = someChecked && !allChecked;
|
||||
}
|
||||
};
|
||||
|
||||
// 刷新列表(支持搜索参数)
|
||||
const refreshRepairsList = async (searchSn = null, showAll = false) => {
|
||||
const listEl = document.getElementById('repairs-list');
|
||||
const infoEl = document.getElementById('repairs-list-info');
|
||||
try {
|
||||
let params = {};
|
||||
if(searchSn) {
|
||||
params.sn = searchSn;
|
||||
} else if(!showAll) {
|
||||
params.limit = 10;
|
||||
}
|
||||
const data = await API.listRepairRecords(params);
|
||||
if(listEl && data.list) {
|
||||
if(data.list.length > 0) {
|
||||
listEl.innerHTML = data.list.map(renderRecordItem).join('');
|
||||
bindListItemEvents();
|
||||
if(infoEl) {
|
||||
if(searchSn) {
|
||||
infoEl.textContent = `搜索 "${searchSn}" 找到 ${data.list.length} 条记录`;
|
||||
} else if(showAll) {
|
||||
infoEl.textContent = `共 ${data.list.length} 条记录`;
|
||||
} else {
|
||||
infoEl.textContent = `显示最新 ${data.list.length} 条记录`;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
listEl.innerHTML = '<li>暂无记录</li>';
|
||||
if(infoEl) {
|
||||
infoEl.textContent = searchSn ? `未找到包含 "${searchSn}" 的记录` : '';
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch(e) {
|
||||
if(listEl) listEl.innerHTML = '<li>加载失败</li>';
|
||||
if(infoEl) infoEl.textContent = '';
|
||||
}
|
||||
};
|
||||
|
||||
// 显示详情/编辑弹窗
|
||||
const showRepairDetailModal = async (id, isEdit) => {
|
||||
try {
|
||||
const record = await API.getRepairRecord(id);
|
||||
if(record.error) {
|
||||
API.toast(record.error);
|
||||
return;
|
||||
}
|
||||
|
||||
const modalId = 'repair-detail-modal';
|
||||
let modal = document.getElementById(modalId);
|
||||
if(modal) modal.remove();
|
||||
|
||||
const imagesHtml = (record.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)">
|
||||
<img src="/api/repair-image/${img}" style="width:100%;height:100%;object-fit:cover" />
|
||||
</a>
|
||||
`).join('')}
|
||||
</div>`
|
||||
: '<div style="color:var(--text-2);font-size:12px">无图片</div>';
|
||||
|
||||
const ts = new Date(record.ts).toLocaleString('zh-CN');
|
||||
const isNoDefect = record.defect_reason === '无不良';
|
||||
|
||||
modal = document.createElement('div');
|
||||
modal.id = modalId;
|
||||
modal.innerHTML = `
|
||||
<div style="position:fixed;inset:0;background:rgba(0,0,0,0.5);z-index:1000;display:flex;align-items:center;justify-content:center;padding:20px">
|
||||
<div style="background:var(--bg);border-radius:12px;max-width:500px;width:100%;max-height:90vh;overflow-y:auto;box-shadow:0 20px 40px rgba(0,0,0,0.2)">
|
||||
<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">${isEdit ? '编辑返修记录' : '返修记录详情'}</h3>
|
||||
<button id="repair-modal-close" style="background:none;border:none;font-size:20px;cursor:pointer;color:var(--text-2)">×</button>
|
||||
</div>
|
||||
<div style="padding:20px">
|
||||
${isEdit ? `
|
||||
<div class="form-group" style="margin-bottom:16px">
|
||||
<label style="display:block;font-weight:600;margin-bottom:6px">设备SN <span style="color:#ef4444">*</span></label>
|
||||
<input type="text" id="edit-repair-sn" class="input" value="${record.sn}" style="width:100%" />
|
||||
</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-machine-type" class="input" style="width:100%">
|
||||
${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>
|
||||
<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 : ''}" />
|
||||
</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>
|
||||
</div>
|
||||
<div style="display:flex;gap:12px">
|
||||
<button class="btn" id="edit-repair-save" style="flex:1">保存</button>
|
||||
<button class="btn btn-secondary" id="edit-repair-cancel">取消</button>
|
||||
</div>
|
||||
` : `
|
||||
<div style="margin-bottom:12px">
|
||||
<div style="font-weight:600;color:var(--text-2);font-size:12px;margin-bottom:4px">设备SN</div>
|
||||
<div style="font-size:14px">${record.sn}</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: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>
|
||||
<span class="badge" style="background:${isNoDefect ? 'var(--success-bg)' : 'var(--danger-bg)'};color:${isNoDefect ? 'var(--success)' : 'var(--danger)'}">${record.defect_reason}</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.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">${ts}</div>
|
||||
</div>
|
||||
<div style="margin-bottom:12px">
|
||||
<div style="font-weight:600;color:var(--text-2);font-size:12px;margin-bottom:4px">图片</div>
|
||||
${imagesHtml}
|
||||
</div>
|
||||
<div style="display:flex;gap:12px;margin-top:16px">
|
||||
<button class="btn btn-secondary" id="view-repair-edit" style="flex:1">编辑</button>
|
||||
<button class="btn btn-secondary" id="view-repair-close">关闭</button>
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(modal);
|
||||
|
||||
// 绑定弹窗事件
|
||||
document.getElementById('repair-modal-close').onclick = () => modal.remove();
|
||||
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 editMethodRequired = document.getElementById('edit-method-required');
|
||||
|
||||
// 不良原因选择变化
|
||||
editDefectSelect.onchange = () => {
|
||||
if(editDefectSelect.value === '其他') {
|
||||
editDefectOther.style.display = 'block';
|
||||
} else {
|
||||
editDefectOther.style.display = 'none';
|
||||
}
|
||||
editMethodRequired.style.display = editDefectSelect.value === '无不良' ? '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 newMethod = document.getElementById('edit-repair-method').value.trim();
|
||||
|
||||
// 验证
|
||||
if(!newSn || newSn.length < 6) {
|
||||
API.toast('SN至少需要6个字符');
|
||||
return;
|
||||
}
|
||||
if(!newMachineType) {
|
||||
API.toast('请选择机种');
|
||||
return;
|
||||
}
|
||||
if(!newDefect) {
|
||||
API.toast('请选择或输入不良原因');
|
||||
return;
|
||||
}
|
||||
if(newDefect !== '无不良' && !newMethod) {
|
||||
API.toast('选择了不良原因时,维修方法为必填项');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await API.updateRepairRecord(id, {
|
||||
sn: newSn,
|
||||
machine_type: newMachineType,
|
||||
defect_reason: newDefect,
|
||||
repair_method: newMethod
|
||||
});
|
||||
if(result.ok) {
|
||||
API.toast('保存成功');
|
||||
modal.remove();
|
||||
refreshRepairsList();
|
||||
} else {
|
||||
API.toast(result.error || '保存失败');
|
||||
}
|
||||
} catch(e) {
|
||||
API.toast('保存失败: ' + (e.message || '网络错误'));
|
||||
}
|
||||
};
|
||||
} else {
|
||||
document.getElementById('view-repair-close').onclick = () => modal.remove();
|
||||
document.getElementById('view-repair-edit').onclick = () => {
|
||||
modal.remove();
|
||||
showRepairDetailModal(id, true);
|
||||
};
|
||||
}
|
||||
} catch(e) {
|
||||
API.toast('加载详情失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 搜索框和按钮
|
||||
const searchInput = document.getElementById('repairs-search-sn');
|
||||
const searchBtn = document.getElementById('repairs-search-btn');
|
||||
|
||||
// 搜索按钮点击
|
||||
addListener(searchBtn, 'click', async () => {
|
||||
const searchSn = searchInput.value.trim();
|
||||
if(!searchSn) {
|
||||
API.toast('请输入SN进行搜索');
|
||||
return;
|
||||
}
|
||||
await refreshRepairsList(searchSn);
|
||||
});
|
||||
|
||||
// 搜索框回车
|
||||
addListener(searchInput, 'keypress', async (e) => {
|
||||
if(e.key === 'Enter') {
|
||||
const searchSn = searchInput.value.trim();
|
||||
if(searchSn) {
|
||||
await refreshRepairsList(searchSn);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
addListener(btn, 'click', async ()=>{
|
||||
const qty = parseInt(document.getElementById('repair-qty').value||'0', 10);
|
||||
const note = document.getElementById('repair-note')?.value.trim() || '';
|
||||
// 查看全部按钮
|
||||
addListener(showAllBtn, 'click', async () => {
|
||||
searchInput.value = '';
|
||||
await refreshRepairsList(null, true);
|
||||
API.toast('显示全部记录');
|
||||
});
|
||||
|
||||
if(qty <= 0){
|
||||
return API.toast('请输入有效的返修数量');
|
||||
// 全选复选框
|
||||
const selectAllCb = document.getElementById('repairs-select-all');
|
||||
addListener(selectAllCb, 'change', () => {
|
||||
const checkboxes = document.querySelectorAll('.repair-checkbox');
|
||||
checkboxes.forEach(cb => {
|
||||
cb.checked = selectAllCb.checked;
|
||||
const id = parseInt(cb.dataset.id);
|
||||
if(selectAllCb.checked) {
|
||||
selectedIds.add(id);
|
||||
} else {
|
||||
selectedIds.delete(id);
|
||||
}
|
||||
});
|
||||
updateSelectedCount();
|
||||
});
|
||||
|
||||
// 导出按钮
|
||||
const exportBtn = document.getElementById('repairs-export-btn');
|
||||
addListener(exportBtn, 'click', async () => {
|
||||
if(selectedIds.size === 0) {
|
||||
API.toast('请先勾选要导出的记录');
|
||||
return;
|
||||
}
|
||||
|
||||
exportBtn.disabled = true;
|
||||
exportBtn.textContent = '导出中...';
|
||||
|
||||
try {
|
||||
const blob = await API.exportRepairRecords(Array.from(selectedIds));
|
||||
// 下载文件
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `返修记录_${new Date().toISOString().slice(0,10)}.xlsx`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
API.toast(`成功导出 ${selectedIds.size} 条记录`);
|
||||
} catch(e) {
|
||||
API.toast('导出失败: ' + (e.error || e.message || '网络错误'));
|
||||
} finally {
|
||||
exportBtn.disabled = false;
|
||||
exportBtn.textContent = '导出选中';
|
||||
}
|
||||
});
|
||||
|
||||
// 提交按钮
|
||||
addListener(btn, 'click', async () => {
|
||||
// 表单验证
|
||||
let hasError = false;
|
||||
|
||||
// 验证SN
|
||||
const sn = snInput.value.trim();
|
||||
if(!sn) {
|
||||
snError.textContent = '请输入设备SN';
|
||||
snError.style.display = 'block';
|
||||
hasError = true;
|
||||
} else if(sn.length < 6) {
|
||||
snError.textContent = 'SN至少需要6个字符';
|
||||
snError.style.display = 'block';
|
||||
hasError = true;
|
||||
} else if(!/^[A-Za-z0-9\-_:]+$/.test(sn)) {
|
||||
snError.textContent = 'SN只能包含字母、数字、横线、下划线或冒号';
|
||||
snError.style.display = 'block';
|
||||
hasError = true;
|
||||
}
|
||||
|
||||
// 验证机种
|
||||
const machineType = machineTypeSelect.value;
|
||||
if(!machineType) {
|
||||
machineTypeError.textContent = '请选择机种';
|
||||
machineTypeError.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 = '请选择不良原因';
|
||||
defectError.style.display = 'block';
|
||||
hasError = true;
|
||||
}
|
||||
|
||||
// 验证维修方法:非"无不良"时必填
|
||||
const repairMethod = methodInput.value.trim();
|
||||
if(defectReason && defectReason !== '无不良' && !repairMethod) {
|
||||
methodError.textContent = '选择了不良原因时,维修方法为必填项';
|
||||
methodError.style.display = 'block';
|
||||
hasError = true;
|
||||
}
|
||||
|
||||
if(hasError) return;
|
||||
|
||||
// 构建FormData
|
||||
const formData = new FormData();
|
||||
formData.append('sn', sn);
|
||||
formData.append('machine_type', machineType);
|
||||
formData.append('defect_reason', defectReason);
|
||||
formData.append('repair_method', methodInput.value.trim());
|
||||
|
||||
// 添加图片
|
||||
pendingImages.forEach(file => {
|
||||
formData.append('images', file);
|
||||
});
|
||||
|
||||
// 显示加载状态
|
||||
btn.disabled = true;
|
||||
try{
|
||||
await API.uploadRepairs({qty, note});
|
||||
API.toast('上传成功');
|
||||
uploadText.style.display = 'none';
|
||||
uploadLoading.style.display = 'inline-flex';
|
||||
|
||||
try {
|
||||
const result = await API.uploadRepairRecord(formData);
|
||||
if(result.ok) {
|
||||
API.toast('提交成功');
|
||||
|
||||
// 清空表单
|
||||
snInput.value = '';
|
||||
machineTypeSelect.value = '';
|
||||
defectSelect.value = '';
|
||||
defectOtherInput.value = '';
|
||||
defectOtherInput.style.display = 'none';
|
||||
methodInput.value = '';
|
||||
methodGroup.style.opacity = '0.5';
|
||||
methodInput.disabled = true;
|
||||
methodRequired.style.display = 'none';
|
||||
methodInput.placeholder = '请描述维修方法';
|
||||
pendingImages = [];
|
||||
previewContainer.innerHTML = '';
|
||||
|
||||
// 刷新列表
|
||||
const listEl = document.getElementById('repairs-list');
|
||||
const data = await API.listRepairs().catch(()=>({list:[]}));
|
||||
if(listEl){
|
||||
listEl.innerHTML = (data.list||[]).slice(0,10).map(r=>{
|
||||
const ts = new Date(r.ts).toLocaleString('zh-CN');
|
||||
return `<li><span>数量: ${r.qty}</span><span class="badge">${r.note||'无备注'}</span><span style="font-size:11px;color:var(--text-2)">${ts}</span></li>`;
|
||||
}).join('')||'<li>暂无数据</li>';
|
||||
await refreshRepairsList();
|
||||
} else {
|
||||
API.toast(result.error || '提交失败');
|
||||
}
|
||||
|
||||
// 清空输入
|
||||
document.getElementById('repair-qty').value = '';
|
||||
document.getElementById('repair-note').value = '';
|
||||
}catch(e){
|
||||
API.toast('上传失败');
|
||||
} catch(e) {
|
||||
API.toast('提交失败: ' + (e.message || '网络错误'));
|
||||
} finally {
|
||||
if (btn) btn.disabled = false;
|
||||
btn.disabled = false;
|
||||
uploadText.style.display = 'inline';
|
||||
uploadLoading.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
// 初始加载最新记录
|
||||
setTimeout(refreshRepairsList, 100);
|
||||
}
|
||||
|
||||
async function bindDefectsEvents(){
|
||||
|
||||
378
server/app.py
378
server/app.py
@ -4,7 +4,7 @@ import json
|
||||
import sqlite3
|
||||
from datetime import datetime
|
||||
from functools import wraps
|
||||
from flask import Flask, request, jsonify, session, send_from_directory
|
||||
from flask import Flask, request, jsonify, session, send_from_directory, send_file
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
from werkzeug.utils import secure_filename
|
||||
try:
|
||||
@ -138,6 +138,20 @@ def init_db():
|
||||
note TEXT,
|
||||
ts TEXT
|
||||
)''')
|
||||
c.execute('''CREATE TABLE IF NOT EXISTS repair_records(
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
sn TEXT NOT NULL,
|
||||
machine_type TEXT,
|
||||
defect_reason TEXT NOT NULL,
|
||||
repair_method TEXT,
|
||||
images TEXT,
|
||||
ts TEXT
|
||||
)''')
|
||||
# 添加machine_type字段(如果表已存在)
|
||||
try:
|
||||
c.execute('ALTER TABLE repair_records ADD COLUMN machine_type TEXT')
|
||||
except:
|
||||
pass
|
||||
c.execute('''CREATE TABLE IF NOT EXISTS sop_files(
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
filename TEXT NOT NULL,
|
||||
@ -1386,6 +1400,368 @@ def upload_repairs():
|
||||
return jsonify({'ok': True})
|
||||
|
||||
|
||||
@app.post('/api/upload/repair-record')
|
||||
@require_login
|
||||
@require_any_role('admin','superadmin')
|
||||
def upload_repair_record():
|
||||
"""上传返修记录(含设备SN、机种、不良原因、维修方法、图片)"""
|
||||
sn = request.form.get('sn', '').strip()
|
||||
machine_type = request.form.get('machine_type', '').strip()
|
||||
defect_reason = request.form.get('defect_reason', '').strip()
|
||||
repair_method = request.form.get('repair_method', '').strip()
|
||||
|
||||
# 验证必填字段
|
||||
if not sn:
|
||||
return jsonify({'error': '设备SN为必填项'}), 400
|
||||
if not machine_type:
|
||||
return jsonify({'error': '机种为必填项'}), 400
|
||||
if not defect_reason:
|
||||
return jsonify({'error': '不良原因为必填项'}), 400
|
||||
|
||||
# 验证维修方法:非"无不良"时必填
|
||||
if defect_reason != '无不良' and not repair_method:
|
||||
return jsonify({'error': '选择了不良原因时,维修方法为必填项'}), 400
|
||||
|
||||
# 验证SN格式(基本校验:至少6个字符,只包含字母数字和常见分隔符)
|
||||
import re
|
||||
if len(sn) < 6 or not re.match(r'^[A-Za-z0-9\-_:]+$', sn):
|
||||
return jsonify({'error': 'SN格式不正确,应至少6个字符,只包含字母、数字、横线、下划线或冒号'}), 400
|
||||
|
||||
# 处理图片上传
|
||||
image_filenames = []
|
||||
upload_dir = os.path.join(os.path.dirname(__file__), 'uploads', 'repair_images')
|
||||
os.makedirs(upload_dir, exist_ok=True)
|
||||
|
||||
files = request.files.getlist('images')
|
||||
if len(files) > 10:
|
||||
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']:
|
||||
return jsonify({'error': f'不支持的图片格式: {ext},仅支持JPG/PNG/GIF'}), 400
|
||||
|
||||
# 验证文件大小(10MB)
|
||||
f.seek(0, 2)
|
||||
size = f.tell()
|
||||
f.seek(0)
|
||||
if size > 10 * 1024 * 1024:
|
||||
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)
|
||||
image_filenames.append(filename)
|
||||
|
||||
# 保存到数据库
|
||||
conn = get_db()
|
||||
c = conn.cursor()
|
||||
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))
|
||||
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}")
|
||||
return jsonify({'ok': True, 'id': record_id})
|
||||
|
||||
|
||||
@app.get('/api/list/repair-records')
|
||||
@require_login
|
||||
def list_repair_records():
|
||||
"""获取返修记录列表,支持SN搜索和分页"""
|
||||
sn_query = request.args.get('sn', '').strip()
|
||||
limit = request.args.get('limit', '')
|
||||
|
||||
conn = get_db()
|
||||
c = conn.cursor()
|
||||
|
||||
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}%',))
|
||||
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,))
|
||||
except:
|
||||
c.execute('SELECT id, sn, machine_type, defect_reason, repair_method, images, ts 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')
|
||||
|
||||
rows = [dict(r) for r in c.fetchall()]
|
||||
conn.close()
|
||||
|
||||
# 解析images JSON
|
||||
for r in rows:
|
||||
if r.get('images'):
|
||||
try:
|
||||
r['images'] = json.loads(r['images'])
|
||||
except:
|
||||
r['images'] = []
|
||||
else:
|
||||
r['images'] = []
|
||||
|
||||
return jsonify({'list': rows, 'total': len(rows)})
|
||||
|
||||
|
||||
@app.get('/api/repair-image/<filename>')
|
||||
@require_login
|
||||
def get_repair_image(filename):
|
||||
"""获取返修记录图片"""
|
||||
upload_dir = os.path.join(os.path.dirname(__file__), 'uploads', 'repair_images')
|
||||
filepath = os.path.join(upload_dir, secure_filename(filename))
|
||||
if os.path.exists(filepath):
|
||||
return send_file(filepath)
|
||||
return jsonify({'error': 'not found'}), 404
|
||||
|
||||
|
||||
@app.post('/api/export/repair-records')
|
||||
@require_login
|
||||
@require_any_role('admin','superadmin')
|
||||
def export_repair_records():
|
||||
"""导出返修记录到Excel(含图片)"""
|
||||
try:
|
||||
from openpyxl import Workbook
|
||||
from openpyxl.drawing.image import Image as XLImage
|
||||
from openpyxl.utils import get_column_letter
|
||||
from io import BytesIO
|
||||
from PIL import Image as PILImage
|
||||
import tempfile
|
||||
|
||||
data = request.get_json() or {}
|
||||
record_ids = data.get('ids', [])
|
||||
|
||||
if not record_ids:
|
||||
return jsonify({'error': '请选择要导出的记录'}), 400
|
||||
|
||||
conn = get_db()
|
||||
c = conn.cursor()
|
||||
|
||||
# 获取选中的记录
|
||||
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)
|
||||
rows = [dict(r) for r in c.fetchall()]
|
||||
conn.close()
|
||||
|
||||
if not rows:
|
||||
return jsonify({'error': '未找到记录'}), 404
|
||||
|
||||
# 创建Excel工作簿
|
||||
wb = Workbook()
|
||||
ws = wb.active
|
||||
ws.title = '返修记录'
|
||||
|
||||
from openpyxl.styles import Alignment
|
||||
|
||||
# 设置表头
|
||||
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')
|
||||
ws.column_dimensions[get_column_letter(col)].width = 20
|
||||
|
||||
# 序号列宽度缩小
|
||||
ws.column_dimensions['A'].width = 4.5
|
||||
# 图片列宽度更大
|
||||
ws.column_dimensions['G'].width = 60
|
||||
|
||||
upload_dir = os.path.join(os.path.dirname(__file__), 'uploads', 'repair_images')
|
||||
|
||||
# 单元格对齐样式:垂直居中、左对齐
|
||||
data_alignment = Alignment(horizontal='left', vertical='center')
|
||||
|
||||
# 填充数据
|
||||
for row_idx, record in enumerate(rows, 2):
|
||||
cell1 = ws.cell(row=row_idx, column=1, value=row_idx - 1)
|
||||
cell1.alignment = data_alignment
|
||||
cell2 = ws.cell(row=row_idx, column=2, value=record['sn'])
|
||||
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.alignment = data_alignment
|
||||
cell5 = ws.cell(row=row_idx, column=5, value=record['repair_method'] or '')
|
||||
cell5.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
|
||||
|
||||
# 处理图片
|
||||
images = []
|
||||
if record['images']:
|
||||
try:
|
||||
images = json.loads(record['images']) if isinstance(record['images'], str) else record['images']
|
||||
except:
|
||||
images = []
|
||||
|
||||
if images:
|
||||
# 设置行高以容纳图片(增大到150像素)
|
||||
ws.row_dimensions[row_idx].height = 150
|
||||
|
||||
img_col = 7 # 图片列改为第7列
|
||||
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):
|
||||
try:
|
||||
# 压缩图片(增大到200x200)
|
||||
with PILImage.open(img_path) as pil_img:
|
||||
pil_img.thumbnail((300, 300))
|
||||
img_buffer = BytesIO()
|
||||
pil_img.save(img_buffer, format='PNG')
|
||||
img_buffer.seek(0)
|
||||
|
||||
xl_img = XLImage(img_buffer)
|
||||
xl_img.width = 180
|
||||
xl_img.height = 180
|
||||
|
||||
cell = ws.cell(row=row_idx, column=img_col + img_idx)
|
||||
ws.add_image(xl_img, cell.coordinate)
|
||||
except Exception as e:
|
||||
ws.cell(row=row_idx, column=img_col, value=f'图片加载失败: {img_name}')
|
||||
else:
|
||||
ws.cell(row=row_idx, column=img_col, value=f'图片不存在: {img_name}')
|
||||
else:
|
||||
ws.cell(row=row_idx, column=7, value='无图片')
|
||||
|
||||
# 保存到内存
|
||||
output = BytesIO()
|
||||
wb.save(output)
|
||||
output.seek(0)
|
||||
|
||||
log('export_repair_records', json.dumps({'count': len(rows)}))
|
||||
|
||||
filename = f'返修记录_{datetime.now().strftime("%Y%m%d_%H%M%S")}.xlsx'
|
||||
return send_file(
|
||||
output,
|
||||
mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
as_attachment=True,
|
||||
download_name=filename
|
||||
)
|
||||
except Exception as e:
|
||||
log('export_repair_records_error', str(e))
|
||||
return jsonify({'error': f'导出失败: {str(e)}'}), 500
|
||||
|
||||
|
||||
@app.get('/api/repair-record/<int:record_id>')
|
||||
@require_login
|
||||
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,))
|
||||
row = c.fetchone()
|
||||
conn.close()
|
||||
|
||||
if not row:
|
||||
return jsonify({'error': '记录不存在'}), 404
|
||||
|
||||
record = dict(row)
|
||||
if record.get('images'):
|
||||
try:
|
||||
record['images'] = json.loads(record['images'])
|
||||
except:
|
||||
record['images'] = []
|
||||
else:
|
||||
record['images'] = []
|
||||
|
||||
return jsonify(record)
|
||||
|
||||
|
||||
@app.put('/api/repair-record/<int:record_id>')
|
||||
@require_login
|
||||
@require_any_role('admin','superadmin')
|
||||
def update_repair_record(record_id):
|
||||
"""更新返修记录"""
|
||||
data = request.get_json() or {}
|
||||
sn = data.get('sn', '').strip()
|
||||
machine_type = data.get('machine_type', '').strip()
|
||||
defect_reason = data.get('defect_reason', '').strip()
|
||||
repair_method = data.get('repair_method', '').strip()
|
||||
|
||||
# 验证必填字段
|
||||
if not sn:
|
||||
return jsonify({'error': '设备SN为必填项'}), 400
|
||||
if not machine_type:
|
||||
return jsonify({'error': '机种为必填项'}), 400
|
||||
if not defect_reason:
|
||||
return jsonify({'error': '不良原因为必填项'}), 400
|
||||
|
||||
# 验证维修方法:非"无不良"时必填
|
||||
if defect_reason != '无不良' and not repair_method:
|
||||
return jsonify({'error': '选择了不良原因时,维修方法为必填项'}), 400
|
||||
|
||||
# 验证SN格式
|
||||
import re
|
||||
if len(sn) < 6 or not re.match(r'^[A-Za-z0-9\-_:]+$', sn):
|
||||
return jsonify({'error': 'SN格式不正确'}), 400
|
||||
|
||||
conn = get_db()
|
||||
c = conn.cursor()
|
||||
|
||||
# 检查记录是否存在
|
||||
c.execute('SELECT id FROM repair_records WHERE id=?', (record_id,))
|
||||
if not c.fetchone():
|
||||
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))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
log('update_repair_record', json.dumps({'id': record_id, 'sn': sn, 'defect_reason': defect_reason}))
|
||||
return jsonify({'ok': True})
|
||||
|
||||
|
||||
@app.delete('/api/repair-record/<int:record_id>')
|
||||
@require_login
|
||||
@require_any_role('superadmin')
|
||||
def delete_repair_record(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
|
||||
|
||||
# 删除关联的图片文件
|
||||
if row['images']:
|
||||
try:
|
||||
images = json.loads(row['images'])
|
||||
upload_dir = os.path.join(os.path.dirname(__file__), 'uploads', 'repair_images')
|
||||
for img in images:
|
||||
filepath = os.path.join(upload_dir, secure_filename(img))
|
||||
if os.path.exists(filepath):
|
||||
os.remove(filepath)
|
||||
except:
|
||||
pass
|
||||
|
||||
# 删除数据库记录
|
||||
c.execute('DELETE FROM repair_records WHERE id=?', (record_id,))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
log('delete_repair_record', json.dumps({'id': record_id}))
|
||||
return jsonify({'ok': True})
|
||||
|
||||
|
||||
@app.post('/api/upload/defects')
|
||||
@require_login
|
||||
@require_any_role('admin','superadmin')
|
||||
|
||||
Loading…
Reference in New Issue
Block a user