2025-11-21 13:27:40 +00:00
const Upload = ( ( ) => {
// 使用localStorage保存上传记录
const STORAGE _KEY = 'mac_upload_history' ;
// 事件监听器清理
const eventListeners = [ ] ;
const addListener = ( element , event , handler ) => {
if ( element ) {
element . addEventListener ( event , handler ) ;
eventListeners . push ( { element , event , handler } ) ;
}
} ;
const cleanupListeners = ( ) => {
eventListeners . forEach ( ( { element , event , handler } ) => {
element . removeEventListener ( event , handler ) ;
} ) ;
eventListeners . length = 0 ;
} ;
Router . onBeforeEach ( ( path ) => {
if ( ! path . startsWith ( '/upload' ) ) {
cleanupListeners ( ) ;
}
} ) ;
function getHistory ( ) {
try {
return JSON . parse ( localStorage . getItem ( STORAGE _KEY ) || '[]' ) ;
} catch {
return [ ] ;
}
}
function saveToHistory ( records ) {
try {
const history = getHistory ( ) ;
const newRecords = records . map ( r => ( {
... r ,
timestamp : new Date ( ) . toISOString ( )
} ) ) ;
const updated = [ ... newRecords , ... history ] . slice ( 0 , 100 ) ; // 保留最近100条
localStorage . setItem ( STORAGE _KEY , JSON . stringify ( updated ) ) ;
} catch ( e ) {
console . error ( '保存历史记录失败:' , e ) ;
}
}
function clearHistory ( ) {
localStorage . removeItem ( STORAGE _KEY ) ;
}
function section ( title , inner ) {
return ` <div class="card"><div style="font-weight:600;margin-bottom:8px"> ${ title } </div> ${ inner } </div> ` ;
}
function filePicker ( id , label , accept ) {
return ` <div class="field"><label> ${ label } </label><input id=" ${ id } " type="file" class="input" accept=" ${ accept } " /></div> ` ;
}
function numberInput ( id , label ) { return ` <div class="field"><label> ${ label } </label><input id=" ${ id } " type="number" class="input" /></div> ` }
function textarea ( id , label , placeholder = '' ) { return ` <div class="field"><label> ${ label } </label><textarea id=" ${ id } " class="input" rows="4" placeholder=" ${ placeholder } "></textarea></div> ` }
async function renderMac ( ) {
2025-11-21 14:45:00 +00:00
return section ( 'MAC与批次( MAC与批次对应关系表) ' , `
2025-11-21 13:27:40 +00:00
< div class = "field" >
2025-11-22 01:53:18 +00:00
< label > 上传机种 < span style = "color:#ff4444" > * < / s p a n > < / l a b e l >
2025-11-21 13:27:40 +00:00
< select id = "mac-type" class = "input" >
2025-11-22 01:53:18 +00:00
< option value = "" > 请选择上传机种 < / o p t i o n >
2025-11-21 13:27:40 +00:00
< option value = "pdd" > 拼多多 < / o p t i o n >
< option value = "yt" > 圆通 < / o p t i o n >
< option value = "tx" > 兔喜 < / o p t i o n >
< / s e l e c t >
< / d i v >
$ { filePicker ( 'mac-file' , '批量导入(Excel)' , 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.ms-excel' ) }
< div class = "actions" > < button class = "btn" id = "mac-upload" > 上传 < / b u t t o n > < / d i v >
< div id = "upload-log" style = "margin-top:12px;display:none" >
< div style = "font-weight:600;margin-bottom:8px" > 上传日志 < / d i v >
< pre style = "background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:12px;max-height:300px;overflow-y:auto;font-size:12px;color:var(--text);white-space:pre-wrap" > < / p r e >
< / d i v >
< div style = "margin-top:12px" >
< div style = "display:flex;justify-content:space-between;align-items:center;margin-bottom:8px" >
< span style = "font-weight:600" > 最新记录 < / s p a n >
< div style = "display:flex;gap:8px" >
< button class = "btn btn-secondary" id = "mac-show-history" style = "font-size:12px;padding:4px 8px" > 查看历史 < / b u t t o n >
< button class = "btn btn-secondary" id = "mac-clear-display" style = "font-size:12px;padding:4px 8px" > 清空显示 < / b u t t o n >
< / d i v >
< / d i v >
< ul id = "mac-list" class = "list" style = "max-height:300px;overflow-y:auto" > < / u l >
< / d i v >
` );
}
async function renderStats ( ) {
return section ( '良/不良统计' , `
< div style = "background:rgba(79,140,255,0.08);border:1px solid rgba(79,140,255,0.2);border-radius:8px;padding:12px;margin-bottom:12px;font-size:13px;line-height:1.6" >
< div style = "font-weight:600;margin-bottom:6px;color:var(--primary)" > 📊 数据说明 < / d i v >
< div style = "color:var(--text)" >
< div > < strong > 直通良品数 : < / s t r o n g > 一 次 检 测 就 通 过 的 产 品 数 量 < / d i v >
< div > < strong > 良品数 : < / s t r o n g > 最 终 通 过 检 测 的 产 品 总 数 ( 包 含 直 通 良 品 + 返 修 后 通 过 的 产 品 ) < / d i v >
< div > < strong > 不良品数 : < / s t r o n g > 最 终 未 通 过 检 测 的 产 品 数 量 ( 报 废 或 待 返 修 ) < / d i v >
< div style = "margin-top:6px;padding-top:6px;border-top:1px solid rgba(79,140,255,0.2);color:var(--text-2)" >
💡 < strong > 计算公式 : < / s t r o n g > < b r >
直通良品率 = 直通良品数 / ( 良品数 + 不良品数 ) × 100 % < br >
总良品率 = 良品数 / ( 良品数 + 不良品数 ) × 100 %
< / d i v >
< / d i v >
< / d i v >
< div class = "field" >
< label > 平台类型 < / l a b e l >
< select id = "stats-platform" class = "input" >
< option value = "pdd" > 拼多多 < / o p t i o n >
< option value = "yt" > 圆通 < / o p t i o n >
< option value = "tx" > 兔喜 < / o p t i o n >
< / s e l e c t >
< / d i v >
$ { numberInput ( 'fpy-good-count' , '直通良品数量(一次检测通过)' ) }
$ { numberInput ( 'good-count' , '良品数量(最终通过检测的总数)' ) }
$ { numberInput ( 'bad-count' , '不良品数量(最终未通过)' ) }
< div class = "field" >
< label > 不良明细 ( 可选 ) < / l a b e l >
< textarea id = "bad-details" class = "input" rows = "4" placeholder = "每行一个不良记录, 格式: MAC地址,批次号 例如: 90:A9:F7:DD:EE:FF,D20250000000001" > < / t e x t a r e a >
< / d i v >
< div class = "actions" > < button class = "btn" id = "stats-upload" > 上传 < / b u t t o n > < / d i v >
< div style = "margin-top:12px" >
< div style = "display:flex;justify-content:space-between;align-items:center;margin-bottom:8px" >
< span style = "font-weight:600" > 最新记录 < / s p a n >
< div style = "display:flex;gap:8px" >
< button class = "btn btn-secondary" id = "stats-show-history" style = "font-size:12px;padding:4px 8px" > 查看历史 < / b u t t o n >
< button class = "btn btn-secondary" id = "stats-clear-display" style = "font-size:12px;padding:4px 8px" > 清空显示 < / b u t t o n >
< / d i v >
< / d i v >
< ul id = "stats-list" class = "list" style = "max-height:300px;overflow-y:auto" > < / u l >
< / d i v >
` );
}
2025-12-12 06:11:47 +00:00
// 不良原因选项
const defectReasonOptions = [
'无不良' ,
'外观不良' ,
'功能异常' ,
'连接问题' ,
'电源故障' ,
'软件问题' ,
'物理损坏' ,
'其他'
] ;
// 机种选项
const machineTypeOptions = [
'拼多多' ,
'圆通' ,
'兔喜' ,
'美团' ,
'大润发' ,
'标准版'
] ;
2025-11-21 13:27:40 +00:00
async function renderRepairs ( ) {
2025-12-12 06:11:47 +00:00
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" > * < / s p a n >
< / l a b e l >
< 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" > < / d i v >
< / d i v >
< div class = "form-group" style = "margin-bottom:16px" >
< label style = "display:block;font-weight:600;margin-bottom:6px" >
机种 < span style = "color:#ef4444" > * < / s p a n >
< / l a b e l >
< select id = "repair-machine-type" class = "input" style = "width:100%" >
< option value = "" > 请选择机种 < / o p t i o n >
$ { machineTypeOptions . map ( opt => ` <option value=" ${ opt } "> ${ opt } </option> ` ) . join ( '' ) }
< / s e l e c t >
< div id = "repair-machine-type-error" style = "color:#ef4444;font-size:12px;margin-top:4px;display:none" > < / d i v >
< / d i v >
2025-12-15 01:38:42 +00:00
< div class = "form-group" style = "margin-bottom:16px" >
< label style = "display:block;font-weight:600;margin-bottom:6px" >
不良现象 < span style = "color:#ef4444" > * < / s p a n >
< / l a b e l >
< 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" > < / d i v >
< / d i v >
2025-12-12 06:11:47 +00:00
< div class = "form-group" style = "margin-bottom:16px" >
< label style = "display:block;font-weight:600;margin-bottom:6px" >
不良原因 < span style = "color:#ef4444" > * < / s p a n >
< / l a b e l >
2025-12-15 01:38:42 +00:00
< input type = "text" id = "repair-defect-reason" class = "input" placeholder = "例: MMC损坏 | 填写[无不良]则维修方法可不填" style = "width:100%" / >
2025-12-12 06:11:47 +00:00
< div id = "repair-defect-error" style = "color:#ef4444;font-size:12px;margin-top:4px;display:none" > < / d i v >
< / d i v >
< 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" > * < / s p a n >
< / l a b e l >
< textarea id = "repair-method" class = "input" placeholder = "请描述维修方法" rows = "3" style = "width:100%;resize:vertical" > < / t e x t a r e a >
< div id = "repair-method-error" style = "color:#ef4444;font-size:12px;margin-top:4px;display:none" > < / d i v >
< / d i v >
< div class = "form-group" style = "margin-bottom:16px" >
< label style = "display:block;font-weight:600;margin-bottom:6px" > 图片上传 < / l a b e l >
< 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" / >
< / s v g >
< / d i v >
< div style = "font-size:14px;color:var(--text-1)" > 点击或拖拽图片到此处上传 < / d i v >
< div style = "font-size:12px;color:var(--text-2);margin-top:4px" > 支持 JPG / PNG / GIF , 单张最大10MB , 最多10张 < / d i v >
< / d i v >
< div id = "repair-image-preview" style = "display:flex;flex-wrap:wrap;gap:8px;margin-top:12px" > < / d i v >
< div id = "repair-image-error" style = "color:#ef4444;font-size:12px;margin-top:4px;display:none" > < / d i v >
< / d i v >
< div class = "actions" style = "display:flex;gap:12px" >
< button class = "btn" id = "repairs-upload" style = "flex:1" >
< span id = "repairs-upload-text" > 提交 < / s p a n >
< 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" / >
< / s v g >
提交中 ...
< / s p a n >
< / b u t t o n >
< button class = "btn btn-secondary" id = "repairs-clear" style = "flex:0 0 auto" > 清空 < / b u t t o n >
< / d i v >
< style >
@ keyframes spin { from { transform : rotate ( 0 deg ) ; } to { transform : rotate ( 360 deg ) ; } }
# 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 ; }
< / s t y l e >
< 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" > 历史记录 < / s p a n >
< 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" > 搜索 < / b u t t o n >
< button class = "btn btn-secondary" id = "repairs-show-all" style = "font-size:12px;padding:4px 8px" > 全部 < / b u t t o n >
2025-12-15 01:38:42 +00:00
< button class = "btn btn-secondary" id = "repairs-tracking-btn" style = "font-size:12px;padding:4px 8px" > 添加快递单号 < / b u t t o n >
2025-12-12 06:11:47 +00:00
< button class = "btn btn-secondary" id = "repairs-export-btn" style = "font-size:12px;padding:4px 8px" > 导出选中 < / b u t t o n >
< / d i v >
< / d i v >
2025-11-21 13:27:40 +00:00
< div style = "display:flex;justify-content:space-between;align-items:center;margin-bottom:8px" >
2025-12-12 06:11:47 +00:00
< div id = "repairs-list-info" style = "font-size:12px;color:var(--text-2)" > < / d i v >
< 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" / >
全选
< / l a b e l >
< span id = "repairs-selected-count" style = "font-size:12px;color:var(--primary)" > < / s p a n >
< / d i v >
2025-11-21 13:27:40 +00:00
< / d i v >
2025-12-12 06:11:47 +00:00
< ul id = "repairs-list" class = "list" style = "max-height:500px;overflow-y:auto" > < / u l >
2025-11-21 13:27:40 +00:00
< / d i v >
` );
}
async function renderDefects ( ) {
return section ( '不良明细' , `
$ { filePicker ( 'defects-file' , '批量导入不良MAC与批次(Excel/CSV)' , 'text/csv,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.ms-excel' ) }
$ { textarea ( 'defects-manual' , '手动输入' , 'AA:BB:...,BATCH-xyz; ...' ) }
< div class = "actions" > < button class = "btn" id = "defects-upload" > 上传 < / b u t t o n > < / d i v >
< div style = "margin-top:12px" > < div style = "font-weight:600;margin-bottom:8px" > 最新记录 < / d i v > < u l i d = " d e f e c t s - l i s t " c l a s s = " l i s t " > < / u l > < / d i v >
` );
}
Router . register ( '/upload/mac' , async ( ) => {
const html = await renderMac ( ) ;
setTimeout ( bindMacEvents , 0 ) ;
2025-11-21 14:45:00 +00:00
setTimeout ( async ( ) => {
// 从服务器获取最新10条记录
2025-11-21 13:27:40 +00:00
const listEl = document . getElementById ( 'mac-list' ) ;
2025-11-21 14:45:00 +00:00
try {
const data = await API . listMac ( ) ;
if ( listEl && data . list && data . list . length > 0 ) {
2025-11-22 01:53:18 +00:00
const platformNames = { pdd : '拼多多' , yt : '圆通' , tx : '兔喜' } ;
listEl . innerHTML = data . list . slice ( 0 , 10 ) . map ( r => {
const platformName = platformNames [ r . platform ] || r . platform || '未知' ;
return ` <li><span> ${ r . mac } </span><span class="badge"> ${ r . batch } </span><span class="badge" style="background:var(--primary-light);color:var(--primary)"> ${ platformName } </span></li> ` ;
} ) . join ( '' ) ;
2025-11-21 14:45:00 +00:00
} else if ( listEl ) {
listEl . innerHTML = '<li>暂无数据</li>' ;
}
} catch ( e ) {
if ( listEl ) listEl . innerHTML = '<li>加载失败</li>' ;
2025-11-21 13:27:40 +00:00
}
} , 0 ) ;
return html ;
} ) ;
Router . register ( '/upload/stats' , async ( ) => {
const html = await renderStats ( ) ;
setTimeout ( bindStatsEvents , 0 ) ;
2025-11-21 14:45:00 +00:00
setTimeout ( async ( ) => {
// 从服务器获取最新10条记录
const listEl = document . getElementById ( 'stats-list' ) ;
2025-11-21 13:27:40 +00:00
try {
2025-11-21 14:45:00 +00:00
const data = await API . listStats ( ) ;
if ( listEl && data . list && data . list . length > 0 ) {
listEl . innerHTML = data . list . slice ( 0 , 10 ) . map ( r => {
2025-11-21 13:27:40 +00:00
const platformName = { pdd : '拼多多' , yt : '圆通' , tx : '兔喜' } [ r . platform ] || '' ;
const platformText = platformName ? ` ${ platformName } - ` : '' ;
let html = ` <li><span> ${ platformText } 直通良: ${ r . fpy _good || 0 } 良: ${ r . good } 不良: ${ r . bad } </span> ` ;
html += '</li>' ;
return html ;
} ) . join ( '' ) ;
} else if ( listEl ) {
listEl . innerHTML = '<li>暂无数据</li>' ;
}
} catch ( e ) {
console . error ( '加载历史记录失败:' , e ) ;
2025-11-21 14:45:00 +00:00
if ( listEl ) listEl . innerHTML = '<li>加载失败</li>' ;
2025-11-21 13:27:40 +00:00
}
} , 0 ) ;
return html ;
} ) ;
Router . register ( '/upload/repairs' , async ( ) => {
const html = await renderRepairs ( ) ;
setTimeout ( bindRepairsEvents , 0 ) ;
setTimeout ( async ( ) => {
const listEl = document . getElementById ( 'repairs-list' ) ;
const data = await API . listRepairs ( ) . catch ( ( ) => ( { list : [ ] } ) ) ;
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>' ;
} , 0 ) ;
return html ;
} ) ;
Router . register ( '/upload/defects' , async ( ) => {
const html = await renderDefects ( ) ;
setTimeout ( bindDefectsEvents , 0 ) ;
setTimeout ( async ( ) => {
const listEl = document . getElementById ( 'defects-list' ) ;
const data = await API . listDefects ( ) . catch ( ( ) => ( { list : [ ] } ) ) ;
listEl . innerHTML = ( data . list || [ ] ) . slice ( 0 , 10 ) . map ( r => ` <li><span> ${ r . mac } </span><span class="badge"> ${ r . batch } </span></li> ` ) . join ( '' ) || '<li>暂无数据</li>' ;
} , 0 ) ;
return html ;
} ) ;
2025-11-22 12:40:46 +00:00
async function renderSop ( ) {
return section ( 'SOP 文件管理' , `
< div style = "background:rgba(79,140,255,0.08);border:1px solid rgba(79,140,255,0.2);border-radius:8px;padding:12px;margin-bottom:12px;font-size:13px;line-height:1.6" >
< div style = "font-weight:600;margin-bottom:6px;color:var(--primary)" > 📄 SOP 说明 < / d i v >
< div style = "color:var(--text)" >
< div > • 所有用户均可在线查看和下载 SOP 文件 < / d i v >
< div > • 管理员可以上传新的 SOP 文件 ( 支持 Excel 和 Word 格式 ) < / d i v >
< div > • 建议为每个 SOP 文件添加清晰的描述说明 < / d i v >
< / d i v >
< / d i v >
$ { filePicker ( 'sop-file' , '上传 SOP 文件 (Excel/Word)' , 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.ms-excel,text/csv,application/vnd.openxmlformats-officedocument.wordprocessingml.document,application/msword' ) }
< div class = "field" >
< label > 文件描述 ( 可选 ) < / l a b e l >
< textarea id = "sop-description" class = "input" rows = "2" placeholder = "例如:拼多多生产流程 SOP v1.0" > < / t e x t a r e a >
< / d i v >
< div class = "actions" > < button class = "btn" id = "sop-upload" > 上传 < / b u t t o n > < / d i v >
< div id = "sop-upload-progress" style = "display:none;margin-top:12px;padding:12px;background:var(--surface);border-radius:8px;border:1px solid var(--border)" >
< div style = "display:flex;justify-content:space-between;margin-bottom:8px" >
< span style = "font-weight:500" > 上传进度 < / s p a n >
< span id = "sop-progress-percent" > 0 % < / s p a n >
< / d i v >
< div style = "width:100%;height:8px;background:var(--bg);border-radius:4px;overflow:hidden" >
< div id = "sop-progress-bar" style = "width:0%;height:100%;background:var(--primary);transition:width 0.3s" > < / d i v >
< / d i v >
< div style = "display:flex;justify-content:space-between;margin-top:8px;font-size:12px;color:var(--text-2)" >
< span id = "sop-progress-size" > 0 MB / 0 MB < / s p a n >
< span id = "sop-progress-speed" > 0 KB / s < / s p a n >
< / d i v >
< / d i v >
< div style = "margin-top:12px" >
< div style = "font-weight:600;margin-bottom:8px" > SOP 文件列表 < / d i v >
< ul id = "sop-list" class = "list" style = "max-height:400px;overflow-y:auto" > < / u l >
< / d i v >
` );
}
Router . register ( '/upload/sop' , async ( ) => {
const html = await renderSop ( ) ;
setTimeout ( bindSopEvents , 0 ) ;
setTimeout ( loadSopList , 0 ) ;
return html ;
} ) ;
async function loadSopList ( ) {
const listEl = document . getElementById ( 'sop-list' ) ;
if ( ! listEl ) return ;
try {
const data = await API . listSopFiles ( ) ;
if ( data . list && data . list . length > 0 ) {
listEl . innerHTML = data . list . map ( r => {
const ts = new Date ( r . ts ) . toLocaleString ( 'zh-CN' ) ;
const desc = r . description ? ` <div style="font-size:11px;color:var(--text-2);margin-top:2px"> ${ r . description } </div> ` : '' ;
const uploader = r . uploader ? ` <span style="font-size:11px;color:var(--text-2)">上传者: ${ r . uploader } </span> ` : '' ;
return ` <li style="display:flex;justify-content:space-between;align-items:center;padding:8px 12px">
< div style = "flex:1" >
< div style = "font-weight:500" > $ { r . original _name } < / d i v >
$ { desc }
< div style = "font-size:11px;color:var(--text-2);margin-top:4px" > $ { uploader } • $ { ts } < / d i v >
< / d i v >
< div style = "display:flex;gap:8px" >
< a href = "/api/sop/download/${r.id}" class = "btn btn-secondary" style = "font-size:12px;padding:4px 12px;text-decoration:none" download > 下载 < / a >
< button class = "btn btn-secondary sop-delete" data - id = "${r.id}" style = "font-size:12px;padding:4px 12px;background:var(--danger);color:white" > 删除 < / b u t t o n >
< / d i v >
< / l i > ` ;
} ) . join ( '' ) ;
// 绑定删除按钮事件
document . querySelectorAll ( '.sop-delete' ) . forEach ( btn => {
addListener ( btn , 'click' , async ( e ) => {
const id = e . target . dataset . id ;
if ( ! confirm ( '确定要删除这个 SOP 文件吗?' ) ) return ;
try {
await API . deleteSopFile ( id ) ;
API . toast ( '删除成功' ) ;
loadSopList ( ) ;
} catch ( e ) {
API . toast ( '删除失败' ) ;
}
} ) ;
} ) ;
} else {
listEl . innerHTML = '<li>暂无 SOP 文件</li>' ;
}
} catch ( e ) {
listEl . innerHTML = '<li>加载失败</li>' ;
}
}
async function bindSopEvents ( ) {
const fileEl = document . getElementById ( 'sop-file' ) ;
const btn = document . getElementById ( 'sop-upload' ) ;
const descEl = document . getElementById ( 'sop-description' ) ;
addListener ( btn , 'click' , async ( ) => {
const file = fileEl . files [ 0 ] ;
if ( ! file ) {
API . toast ( '请选择文件' ) ;
return ;
}
const description = descEl ? descEl . value . trim ( ) : '' ;
// 获取进度显示元素
const progressContainer = document . getElementById ( 'sop-upload-progress' ) ;
const progressBar = document . getElementById ( 'sop-progress-bar' ) ;
const progressPercent = document . getElementById ( 'sop-progress-percent' ) ;
const progressSize = document . getElementById ( 'sop-progress-size' ) ;
const progressSpeed = document . getElementById ( 'sop-progress-speed' ) ;
let startTime = Date . now ( ) ;
let lastLoaded = 0 ;
let lastTime = Date . now ( ) ;
try {
btn . disabled = true ;
// 显示进度条
if ( progressContainer ) progressContainer . style . display = 'block' ;
await API . uploadSopFileWithProgress ( file , description , ( progress ) => {
const { percent , loaded , total } = progress ;
// 更新进度条
if ( progressBar ) progressBar . style . width = percent + '%' ;
if ( progressPercent ) progressPercent . textContent = percent + '%' ;
// 更新大小
const loadedMB = ( loaded / 1024 / 1024 ) . toFixed ( 2 ) ;
const totalMB = ( total / 1024 / 1024 ) . toFixed ( 2 ) ;
if ( progressSize ) progressSize . textContent = ` ${ loadedMB } MB / ${ totalMB } MB ` ;
// 计算速度
const now = Date . now ( ) ;
const timeDiff = ( now - lastTime ) / 1000 ; // 秒
if ( timeDiff > 0.1 ) { // 每100ms更新一次速度
const loadedDiff = loaded - lastLoaded ;
const speed = loadedDiff / timeDiff ; // bytes/s
let speedText ;
if ( speed > 1024 * 1024 ) {
speedText = ( speed / 1024 / 1024 ) . toFixed ( 2 ) + ' MB/s' ;
} else if ( speed > 1024 ) {
speedText = ( speed / 1024 ) . toFixed ( 2 ) + ' KB/s' ;
} else {
speedText = speed . toFixed ( 0 ) + ' B/s' ;
}
if ( progressSpeed ) progressSpeed . textContent = speedText ;
lastLoaded = loaded ;
lastTime = now ;
}
} ) ;
API . toast ( '上传成功' ) ;
// 清空输入
if ( fileEl ) fileEl . value = '' ;
if ( descEl ) descEl . value = '' ;
// 隐藏进度条
setTimeout ( ( ) => {
if ( progressContainer ) progressContainer . style . display = 'none' ;
if ( progressBar ) progressBar . style . width = '0%' ;
if ( progressPercent ) progressPercent . textContent = '0%' ;
if ( progressSize ) progressSize . textContent = '0 MB / 0 MB' ;
if ( progressSpeed ) progressSpeed . textContent = '0 KB/s' ;
} , 2000 ) ;
// 刷新列表
loadSopList ( ) ;
} catch ( e ) {
API . toast ( '上传失败' ) ;
// 隐藏进度条
if ( progressContainer ) progressContainer . style . display = 'none' ;
} finally {
if ( btn ) btn . disabled = false ;
}
} ) ;
}
2025-11-21 13:27:40 +00:00
function readText ( file ) { return new Promise ( ( resolve , reject ) => { const r = new FileReader ( ) ; r . onload = ( ) => resolve ( r . result ) ; r . onerror = reject ; r . readAsText ( file ) } ) }
function parseManual ( text ) {
return text . split ( /\n+/ ) . map ( l => l . trim ( ) ) . filter ( Boolean ) . map ( l => {
const [ mac , batch ] = l . split ( ',' ) ;
return { mac , batch } ;
} ) ;
}
async function bindMacEvents ( ) {
const fileEl = document . getElementById ( 'mac-file' ) ;
const btn = document . getElementById ( 'mac-upload' ) ;
const showHistoryBtn = document . getElementById ( 'mac-show-history' ) ;
const clearDisplayBtn = document . getElementById ( 'mac-clear-display' ) ;
// 文件选择后立即验证
addListener ( fileEl , 'change' , async ( ) => {
const file = fileEl . files [ 0 ] ;
if ( ! file ) return ;
try {
const formData = new FormData ( ) ;
formData . append ( 'file' , file ) ;
const res = await fetch ( '/api/validate/mac-file' , {
method : 'POST' ,
body : formData
} ) ;
const result = await res . json ( ) ;
if ( ! result . valid ) {
API . toast ( result . message || '文件格式不正确' ) ;
fileEl . value = '' ;
return ;
}
API . toast ( result . message || '文件验证通过' ) ;
} catch ( e ) {
API . toast ( '文件验证失败' ) ;
fileEl . value = '' ;
}
} ) ;
2025-11-21 14:45:00 +00:00
// 查看历史按钮 - 从服务器获取所有用户的上传记录
addListener ( showHistoryBtn , 'click' , async ( ) => {
2025-11-21 13:27:40 +00:00
const listEl = document . getElementById ( 'mac-list' ) ;
2025-11-21 14:45:00 +00:00
try {
const data = await API . listMac ( ) ;
if ( listEl ) {
if ( data . list && data . list . length > 0 ) {
2025-11-22 01:53:18 +00:00
const platformNames = { pdd : '拼多多' , yt : '圆通' , tx : '兔喜' } ;
listEl . innerHTML = data . list . map ( r => {
const platformName = platformNames [ r . platform ] || r . platform || '未知' ;
return ` <li><span> ${ r . mac } </span><span class="badge"> ${ r . batch } </span><span class="badge" style="background:var(--primary-light);color:var(--primary)"> ${ platformName } </span><span style="font-size:11px;color:var(--text-2);margin-left:8px"> ${ new Date ( r . ts ) . toLocaleString ( 'zh-CN' ) } </span></li> ` ;
} ) . join ( '' ) ;
2025-11-21 14:45:00 +00:00
API . toast ( ` 显示全部 ${ data . list . length } 条历史记录 ` ) ;
} else {
listEl . innerHTML = '<li>暂无历史记录</li>' ;
}
2025-11-21 13:27:40 +00:00
}
2025-11-21 14:45:00 +00:00
} catch ( e ) {
API . toast ( '加载历史记录失败' ) ;
if ( listEl ) listEl . innerHTML = '<li>加载失败</li>' ;
2025-11-21 13:27:40 +00:00
}
} ) ;
// 清空显示按钮
addListener ( clearDisplayBtn , 'click' , ( ) => {
const listEl = document . getElementById ( 'mac-list' ) ;
if ( listEl ) {
listEl . innerHTML = '<li>已清空显示</li>' ;
API . toast ( '已清空显示(历史记录仍保留)' ) ;
}
} ) ;
addListener ( btn , 'click' , async ( ) => {
const file = fileEl . files [ 0 ] ;
if ( ! file ) {
API . toast ( '请选择文件' ) ;
return ;
}
const typeEl = document . getElementById ( 'mac-type' ) ;
2025-11-22 01:53:18 +00:00
const uploadType = typeEl ? typeEl . value : '' ;
// 验证是否选择了上传类型
if ( ! uploadType ) {
API . toast ( '请选择上传机种(拼多多/圆通/兔喜)' ) ;
return ;
}
// 确认上传类型
const typeNames = { pdd : '拼多多' , yt : '圆通' , tx : '兔喜' } ;
const typeName = typeNames [ uploadType ] || uploadType ;
const confirmed = confirm ( ` 请确认上传机种: ${ typeName } \n \n 文件名: ${ file . name } \n \n 确认无误后点击"确定"继续上传 ` ) ;
if ( ! confirmed ) {
return ;
}
2025-11-21 13:27:40 +00:00
const logContainer = document . getElementById ( 'upload-log' ) ;
const logPre = logContainer ? logContainer . querySelector ( 'pre' ) : null ;
try {
btn . disabled = true ;
if ( logContainer ) logContainer . style . display = 'block' ;
if ( logPre ) logPre . textContent = '正在上传文件...\n' ;
const formData = new FormData ( ) ;
formData . append ( 'file' , file ) ;
formData . append ( 'type' , uploadType ) ;
const res = await fetch ( '/api/upload/mac-file' , {
method : 'POST' ,
body : formData
} ) ;
const result = await res . json ( ) ;
if ( logPre ) {
logPre . textContent = result . output || '上传完成' ;
}
if ( result . ok ) {
API . toast ( '上传成功' ) ;
// 解析并显示成功上传的记录
const output = result . output || '' ;
const jsonMatch = output . match ( /=== 成功导入的数据 ===\n([\s\S]*?)\n=== 数据输出结束 ===/ ) ;
if ( jsonMatch && jsonMatch [ 1 ] ) {
try {
const records = JSON . parse ( jsonMatch [ 1 ] . trim ( ) ) ;
if ( records . length > 0 ) {
// 保存到历史记录
saveToHistory ( records ) ;
// 显示最新记录
const listEl = document . getElementById ( 'mac-list' ) ;
if ( listEl ) {
listEl . innerHTML = records . map ( r => ` <li><span> ${ r . mac } </span><span class="badge"> ${ r . batch } </span></li> ` ) . join ( '' ) ;
}
}
} catch ( e ) {
console . error ( '解析上传记录失败:' , e ) ;
}
}
} else {
API . toast ( result . error || '上传失败' ) ;
}
if ( fileEl ) fileEl . value = '' ;
} catch ( e ) {
API . toast ( '上传失败: ' + e . message ) ;
if ( logPre ) logPre . textContent += '\n错误: ' + e . message ;
} finally {
if ( btn ) btn . disabled = false ;
}
} ) ;
}
async function bindStatsEvents ( ) {
const STATS _STORAGE _KEY = 'stats_upload_history' ;
const getStatsHistory = ( ) => {
try {
return JSON . parse ( localStorage . getItem ( STATS _STORAGE _KEY ) || '[]' ) ;
} catch {
return [ ] ;
}
} ;
const saveToStatsHistory = ( record ) => {
try {
const history = getStatsHistory ( ) ;
const newRecord = {
... record ,
timestamp : new Date ( ) . toISOString ( )
} ;
const updated = [ newRecord , ... history ] . slice ( 0 , 100 ) ;
localStorage . setItem ( STATS _STORAGE _KEY , JSON . stringify ( updated ) ) ;
} catch ( e ) {
console . error ( '保存历史记录失败:' , e ) ;
}
} ;
const btn = document . getElementById ( 'stats-upload' ) ;
const showHistoryBtn = document . getElementById ( 'stats-show-history' ) ;
const clearDisplayBtn = document . getElementById ( 'stats-clear-display' ) ;
2025-11-21 14:45:00 +00:00
// 查看历史按钮 - 从服务器获取所有用户的上传记录
addListener ( showHistoryBtn , 'click' , async ( ) => {
2025-11-21 13:27:40 +00:00
const listEl = document . getElementById ( 'stats-list' ) ;
2025-11-21 14:45:00 +00:00
try {
const data = await API . listStats ( ) ;
if ( listEl ) {
if ( data . list && data . list . length > 0 ) {
listEl . innerHTML = data . list . map ( r => {
const platformName = { pdd : '拼多多' , yt : '圆通' , tx : '兔喜' } [ r . platform ] || '' ;
const platformText = platformName ? ` ${ platformName } - ` : '' ;
let html = ` <li><span> ${ platformText } 直通良: ${ r . fpy _good || 0 } 良: ${ r . good } 不良: ${ r . bad } </span> ` ;
html += ` <span style="font-size:11px;color:var(--text-2);margin-left:8px"> ${ new Date ( r . ts ) . toLocaleString ( 'zh-CN' ) } </span></li> ` ;
return html ;
} ) . join ( '' ) ;
API . toast ( ` 显示全部 ${ data . list . length } 条历史记录 ` ) ;
} else {
listEl . innerHTML = '<li>暂无历史记录</li>' ;
}
2025-11-21 13:27:40 +00:00
}
2025-11-21 14:45:00 +00:00
} catch ( e ) {
API . toast ( '加载历史记录失败' ) ;
if ( listEl ) listEl . innerHTML = '<li>加载失败</li>' ;
2025-11-21 13:27:40 +00:00
}
} ) ;
// 清空显示按钮
addListener ( clearDisplayBtn , 'click' , ( ) => {
const listEl = document . getElementById ( 'stats-list' ) ;
if ( listEl ) {
listEl . innerHTML = '<li>已清空显示</li>' ;
API . toast ( '已清空显示(历史记录仍保留)' ) ;
}
} ) ;
addListener ( btn , 'click' , async ( ) => {
const platform = document . getElementById ( 'stats-platform' ) . value ;
const fpyGood = parseInt ( document . getElementById ( 'fpy-good-count' ) . value || '0' , 10 ) ;
const good = parseInt ( document . getElementById ( 'good-count' ) . value || '0' , 10 ) ;
const bad = parseInt ( document . getElementById ( 'bad-count' ) . value || '0' , 10 ) ;
const detailsText = document . getElementById ( 'bad-details' ) ? . value . trim ( ) || '' ;
if ( fpyGood < 0 || good < 0 || bad < 0 ) { return API . toast ( '数量不能为负数' ) }
// 解析不良明细
const details = [ ] ;
if ( detailsText ) {
const lines = detailsText . split ( '\n' ) . filter ( l => l . trim ( ) ) ;
for ( const line of lines ) {
const [ mac , batch ] = line . split ( ',' ) . map ( s => s . trim ( ) ) ;
if ( mac && batch ) {
details . push ( { mac , batch } ) ;
}
}
}
btn . disabled = true ;
try {
await API . uploadStats ( { platform , fpy _good : fpyGood , good , bad , details } ) ;
API . toast ( '上传成功' ) ;
// 保存到历史记录
saveToStatsHistory ( { platform , fpy _good : fpyGood , good , bad , details } ) ;
// 显示最新记录
const listEl = document . getElementById ( 'stats-list' ) ;
if ( listEl ) {
const platformName = { pdd : '拼多多' , yt : '圆通' , tx : '兔喜' } [ platform ] || platform ;
let html = ` <li><span> ${ platformName } - 直通良: ${ fpyGood } 良: ${ good } 不良: ${ bad } </span> ` ;
if ( details . length > 0 ) {
html += ` <span class="badge"> ${ details . length } 条明细</span> ` ;
}
html += '</li>' ;
listEl . innerHTML = html ;
}
// 清空输入
document . getElementById ( 'fpy-good-count' ) . value = '' ;
document . getElementById ( 'good-count' ) . value = '' ;
document . getElementById ( 'bad-count' ) . value = '' ;
document . getElementById ( 'bad-details' ) . value = '' ;
} catch ( e ) {
API . toast ( '上传失败' ) ;
} finally {
if ( btn ) btn . disabled = false ;
}
} ) ;
}
async function bindRepairsEvents ( ) {
const btn = document . getElementById ( 'repairs-upload' ) ;
2025-12-12 06:11:47 +00:00
const clearBtn = document . getElementById ( 'repairs-clear' ) ;
2025-11-21 13:27:40 +00:00
const showAllBtn = document . getElementById ( 'repairs-show-all' ) ;
2025-12-12 06:11:47 +00:00
const snInput = document . getElementById ( 'repair-sn' ) ;
const machineTypeSelect = document . getElementById ( 'repair-machine-type' ) ;
2025-12-15 01:38:42 +00:00
const symptomInput = document . getElementById ( 'repair-defect-symptom' ) ;
const defectInput = document . getElementById ( 'repair-defect-reason' ) ;
2025-12-12 06:11:47 +00:00
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' ) ;
2025-12-15 01:38:42 +00:00
const symptomError = document . getElementById ( 'repair-symptom-error' ) ;
2025-12-12 06:11:47 +00:00
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' ) ;
2025-11-21 13:27:40 +00:00
2025-12-12 06:11:47 +00:00
// 存储待上传的图片文件
let pendingImages = [ ] ;
2025-12-15 01:38:42 +00:00
// 根据不良原因决定维修方法是否必填
addListener ( defectInput , 'input' , ( ) => {
2025-12-12 06:11:47 +00:00
// 清除错误提示
defectError . style . display = 'none' ;
methodError . style . display = 'none' ;
2025-12-15 01:38:42 +00:00
const defectValue = defectInput . value . trim ( ) ;
2025-12-12 06:11:47 +00:00
// 条件性显示维修方法:无不良时可选,其他原因时必填
2025-12-15 01:38:42 +00:00
if ( defectValue ) {
2025-12-12 06:11:47 +00:00
methodGroup . style . opacity = '1' ;
methodInput . disabled = false ;
2025-12-15 01:38:42 +00:00
if ( defectValue === '无不良' ) {
2025-12-12 06:11:47 +00:00
// 无不良时维修方法可选
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" > × < / b u t t o n >
< / d i v >
` ;
} ) . join ( '' ) ;
// 绑定删除按钮事件
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 = '' ;
2025-12-15 01:38:42 +00:00
defectInput . value = '' ;
2025-12-12 06:11:47 +00:00
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" / >
< / d i v >
< 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 } < / s p a n >
2025-12-15 01:38:42 +00:00
< div style = "text-align:right" >
< div style = "font-size:11px;color:var(--text-2)" > $ { ts } < / d i v >
$ { 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> ` : '' }
< / d i v >
2025-12-12 06:11:47 +00:00
< / d i v >
< 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> ` : '' }
2025-12-15 01:38:42 +00:00
$ { r . defect _symptom ? ` <span class="badge" style="background:#e0e7ff;color:#4338ca"> ${ r . defect _symptom } </span> ` : '' }
2025-12-12 06:11:47 +00:00
< 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 } < / s p a n >
$ { imgCount > 0 ? ` <span class="badge"> ${ imgCount } 张图片</span> ` : '' }
< / d i v >
$ { 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" > 查看 < / b u t t o n >
< button class = "btn btn-secondary repair-edit-btn" data - id = "${r.id}" style = "font-size:11px;padding:2px 8px" > 编辑 < / b u t t o n >
$ { isSuperAdmin ? ` <button class="btn btn-secondary repair-delete-btn" data-id=" ${ r . id } " style="font-size:11px;padding:2px 8px;color:var(--danger)">删除</button> ` : '' }
< / d i v >
< / d i v >
< / l i >
` ;
} ;
// 绑定列表项按钮事件
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 || '网络错误' ) ) ;
}
}
} ;
} ) ;
2025-12-15 01:38:42 +00:00
// 快递单号点击查询物流
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" > 📦 物流查询 < / h 3 >
< button id = "tracking-modal-close" style = "background:none;border:none;font-size:20px;cursor:pointer;color:var(--text-2)" > & times ; < / b u t t o n >
< / d i v >
< 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" > < / d i v >
< div style = "margin-top:12px;color:var(--text-2)" > 正在查询物流信息 ... < / d i v >
< / d i v >
< / d i v >
< / d i v >
` ;
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 } < / d i v >
< div style = "font-size:12px;color:var(--text-2)" > $ { trackingNumber } < / d i v >
< / d i v >
< span class = "badge" style = "background:${result.state === '3' ? 'var(--success-bg)' : 'var(--primary-bg)'};color:${stateColor}" > $ { stateName } < / s p a n >
< / d i v >
< 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)" > < / d i v >
< div style = "font-size:12px;color:${idx === 0 ? 'var(--primary)' : 'var(--text-2)'};margin-bottom:4px" > $ { item . time || item . ftime || '' } < / d i v >
< div style = "font-size:13px;color:${idx === 0 ? 'var(--text)' : 'var(--text-2)'}" > $ { item . context || '' } < / d i v >
< / d i v >
` ).join('')}
< / d i v >
` ;
} else {
contentEl . innerHTML = `
< div style = "text-align:center;padding:40px 0;color:var(--text-2)" >
< div style = "font-size:48px;margin-bottom:12px" > 📭 < / d i v >
< div > $ { result . error || '暂无物流信息' } < / d i v >
< / d i v >
` ;
}
} 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" > ❌ < / d i v >
< div > 查询失败 : $ { e . message || '网络错误' } < / d i v >
< / d i v >
` ;
}
} ;
} ) ;
2025-12-12 06:11:47 +00:00
} ;
// 更新全选框状态
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 ) => {
2025-11-21 13:27:40 +00:00
const listEl = document . getElementById ( 'repairs-list' ) ;
2025-12-12 06:11:47 +00:00
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 ( ) ;
2025-12-15 01:38:42 +00:00
const images = record . images || [ ] ;
const imagesHtml = images . length > 0
2025-12-12 06:11:47 +00:00
? ` <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('')}
< / d i v > `
: '<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 ? '编辑返修记录' : '返修记录详情' } < / h 3 >
< button id = "repair-modal-close" style = "background:none;border:none;font-size:20px;cursor:pointer;color:var(--text-2)" > & times ; < / b u t t o n >
< / d i v >
< 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" > * < / s p a n > < / l a b e l >
< input type = "text" id = "edit-repair-sn" class = "input" value = "${record.sn}" style = "width:100%" / >
< / d i v >
< div class = "form-group" style = "margin-bottom:16px" >
< label style = "display:block;font-weight:600;margin-bottom:6px" > 机种 < span style = "color:#ef4444" > * < / s p a n > < / l a b e l >
< 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 ( '' ) }
< / s e l e c t >
< / d i v >
2025-12-15 01:38:42 +00:00
< div class = "form-group" style = "margin-bottom:16px" >
< label style = "display:block;font-weight:600;margin-bottom:6px" > 不良现象 < span style = "color:#ef4444" > * < / s p a n > < / l a b e l >
< input type = "text" id = "edit-repair-symptom" class = "input" placeholder = "例:红灯不亮" style = "width:100%" value = "${record.defect_symptom || ''}" / >
< / d i v >
2025-12-12 06:11:47 +00:00
< div class = "form-group" style = "margin-bottom:16px" >
< label style = "display:block;font-weight:600;margin-bottom:6px" > 不良原因 < span style = "color:#ef4444" > * < / s p a n > < / l a b e l >
2025-12-15 01:38:42 +00:00
< input type = "text" id = "edit-repair-defect" class = "input" placeholder = "填写【无不良】则维修方法可不填" style = "width:100%" value = "${record.defect_reason || ''}" / >
2025-12-12 06:11:47 +00:00
< / d i v >
< 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'}" > * < / s p a n > < / l a b e l >
< textarea id = "edit-repair-method" class = "input" rows = "3" style = "width:100%;resize:vertical" > $ { record . repair _method || '' } < / t e x t a r e a >
< / d i v >
< div class = "form-group" style = "margin-bottom:16px" >
2025-12-15 01:38:42 +00:00
< label style = "display:block;font-weight:600;margin-bottom:6px" > 快递单号 < / l a b e l >
< input type = "text" id = "edit-repair-tracking" class = "input" placeholder = "寄回客户的快递单号(可选)" style = "width:100%" value = "${record.tracking_number || ''}" / >
< / d i v >
< div class = "form-group" style = "margin-bottom:16px" >
< label style = "display:block;font-weight:600;margin-bottom:6px" > 图片管理 < / l a b e l >
< 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" > & times ; < / b u t t o n >
< / d i v >
` ).join('')}
< / d i v >
< 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" > 点击或拖拽添加新图片 < / d i v >
< / d i v >
< div id = "edit-new-images-preview" style = "display:flex;flex-wrap:wrap;gap:8px;margin-top:8px" > < / d i v >
< div style = "font-size:11px;color:var(--text-2);margin-top:4px" > 最多10张图片 , 点击 × 删除 < / d i v >
2025-12-12 06:11:47 +00:00
< / d i v >
< div style = "display:flex;gap:12px" >
< button class = "btn" id = "edit-repair-save" style = "flex:1" > 保存 < / b u t t o n >
< button class = "btn btn-secondary" id = "edit-repair-cancel" > 取消 < / b u t t o n >
< / d i v >
` : `
< div style = "margin-bottom:12px" >
< div style = "font-weight:600;color:var(--text-2);font-size:12px;margin-bottom:4px" > 设备SN < / d i v >
< div style = "font-size:14px" > $ { record . sn } < / d i v >
< / d i v >
< div style = "margin-bottom:12px" >
< div style = "font-weight:600;color:var(--text-2);font-size:12px;margin-bottom:4px" > 机种 < / d i v >
< span class = "badge" style = "background:var(--primary-bg);color:var(--primary)" > $ { record . machine _type || '未知' } < / s p a n >
< / d i v >
2025-12-15 01:38:42 +00:00
< div style = "margin-bottom:12px" >
< div style = "font-weight:600;color:var(--text-2);font-size:12px;margin-bottom:4px" > 不良现象 < / d i v >
< div style = "font-size:14px" > $ { record . defect _symptom || '未填写' } < / d i v >
< / d i v >
2025-12-12 06:11:47 +00:00
< div style = "margin-bottom:12px" >
< div style = "font-weight:600;color:var(--text-2);font-size:12px;margin-bottom:4px" > 不良原因 < / d i v >
< span class = "badge" style = "background:${isNoDefect ? 'var(--success-bg)' : 'var(--danger-bg)'};color:${isNoDefect ? 'var(--success)' : 'var(--danger)'}" > $ { record . defect _reason } < / s p a n >
< / d i v >
< div style = "margin-bottom:12px" >
< div style = "font-weight:600;color:var(--text-2);font-size:12px;margin-bottom:4px" > 维修方法 < / d i v >
< div style = "font-size:14px" > $ { record . repair _method || '无' } < / d i v >
< / d i v >
2025-12-15 01:38:42 +00:00
< div style = "margin-bottom:12px" >
< div style = "font-weight:600;color:var(--text-2);font-size:12px;margin-bottom:4px" > 快递单号 < / d i v >
< div style = "font-size:14px" > $ { record . tracking _number ? ` <span class="badge" style="background:#fef3c7;color:#92400e">📦 ${ record . tracking _number } </span> ` : '未填写' } < / d i v >
< / d i v >
2025-12-12 06:11:47 +00:00
< div style = "margin-bottom:12px" >
< div style = "font-weight:600;color:var(--text-2);font-size:12px;margin-bottom:4px" > 上传时间 < / d i v >
< div style = "font-size:14px" > $ { ts } < / d i v >
< / d i v >
< div style = "margin-bottom:12px" >
< div style = "font-weight:600;color:var(--text-2);font-size:12px;margin-bottom:4px" > 图片 < / d i v >
$ { imagesHtml }
< / d i v >
< div style = "display:flex;gap:12px;margin-top:16px" >
< button class = "btn btn-secondary" id = "view-repair-edit" style = "flex:1" > 编辑 < / b u t t o n >
< button class = "btn btn-secondary" id = "view-repair-close" > 关闭 < / b u t t o n >
< / d i v >
` }
< / d i v >
< / d i v >
< / d i v >
` ;
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 ) {
2025-12-15 01:38:42 +00:00
const editDefectInput = document . getElementById ( 'edit-repair-defect' ) ;
2025-12-12 06:11:47 +00:00
const editMethodRequired = document . getElementById ( 'edit-method-required' ) ;
2025-12-15 01:38:42 +00:00
// 图片管理相关
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" > & times ; < / b u t t o n >
` ;
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 ) ;
2025-12-12 06:11:47 +00:00
}
2025-12-15 01:38:42 +00:00
}
// 不良原因输入变化
editDefectInput . oninput = ( ) => {
const defectValue = editDefectInput . value . trim ( ) ;
editMethodRequired . style . display = defectValue === '无不良' ? 'none' : 'inline' ;
2025-12-12 06:11:47 +00:00
} ;
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 ;
2025-12-15 01:38:42 +00:00
const newSymptom = document . getElementById ( 'edit-repair-symptom' ) . value . trim ( ) ;
const newDefect = editDefectInput . value . trim ( ) ;
2025-12-12 06:11:47 +00:00
const newMethod = document . getElementById ( 'edit-repair-method' ) . value . trim ( ) ;
2025-12-15 01:38:42 +00:00
const newTracking = document . getElementById ( 'edit-repair-tracking' ) . value . trim ( ) ;
2025-12-12 06:11:47 +00:00
// 验证
if ( ! newSn || newSn . length < 6 ) {
API . toast ( 'SN至少需要6个字符' ) ;
return ;
}
if ( ! newMachineType ) {
API . toast ( '请选择机种' ) ;
return ;
}
2025-12-15 01:38:42 +00:00
if ( ! newSymptom ) {
API . toast ( '请输入不良现象' ) ;
return ;
}
2025-12-12 06:11:47 +00:00
if ( ! newDefect ) {
2025-12-15 01:38:42 +00:00
API . toast ( '请输入不良原因' ) ;
2025-12-12 06:11:47 +00:00
return ;
}
if ( newDefect !== '无不良' && ! newMethod ) {
2025-12-15 01:38:42 +00:00
API . toast ( '填写了不良原因时,维修方法为必填项' ) ;
2025-12-12 06:11:47 +00:00
return ;
}
try {
2025-12-15 01:38:42 +00:00
// 先更新基本信息
2025-12-12 06:11:47 +00:00
const result = await API . updateRepairRecord ( id , {
sn : newSn ,
machine _type : newMachineType ,
2025-12-15 01:38:42 +00:00
defect _symptom : newSymptom ,
2025-12-12 06:11:47 +00:00
defect _reason : newDefect ,
2025-12-15 01:38:42 +00:00
repair _method : newMethod ,
tracking _number : newTracking
2025-12-12 06:11:47 +00:00
} ) ;
2025-12-15 01:38:42 +00:00
if ( ! result . ok ) {
2025-12-12 06:11:47 +00:00
API . toast ( result . error || '保存失败' ) ;
2025-12-15 01:38:42 +00:00
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 ;
}
2025-12-12 06:11:47 +00:00
}
2025-12-15 01:38:42 +00:00
API . toast ( '保存成功' ) ;
modal . remove ( ) ;
refreshRepairsList ( ) ;
2025-12-12 06:11:47 +00:00
} 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 ( showAllBtn , 'click' , async ( ) => {
searchInput . value = '' ;
await refreshRepairsList ( null , true ) ;
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 ) ;
2025-11-21 13:27:40 +00:00
} else {
2025-12-12 06:11:47 +00:00
selectedIds . delete ( id ) ;
2025-11-21 13:27:40 +00:00
}
2025-12-12 06:11:47 +00:00
} ) ;
updateSelectedCount ( ) ;
} ) ;
2025-12-15 01:38:42 +00:00
// 添加快递单号按钮
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" > 添加快递单号 < / h 3 >
< button id = "tracking-modal-close" style = "background:none;border:none;font-size:20px;cursor:pointer;color:var(--text-2)" > & times ; < / b u t t o n >
< / d i v >
< div style = "padding:20px" >
< div style = "margin-bottom:16px;font-size:14px;color:var(--text-2)" >
已选择 < strong > $ { selectedIds . size } < / s t r o n g > 条 记 录
< / d i v >
< div class = "form-group" style = "margin-bottom:16px" >
< label style = "display:block;font-weight:600;margin-bottom:6px" > 快递单号 < span style = "color:#ef4444" > * < / s p a n > < / l a b e l >
< input type = "text" id = "tracking-number-input" class = "input" placeholder = "请输入快递单号" style = "width:100%" / >
< / d i v >
< div style = "display:flex;gap:12px" >
< button class = "btn" id = "tracking-submit" style = "flex:1" > 确定 < / b u t t o n >
< button class = "btn btn-secondary" id = "tracking-cancel" > 取消 < / b u t t o n >
< / d i v >
< / d i v >
< / d i v >
` ;
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 || '网络错误' ) ) ;
}
} ;
} ) ;
2025-12-12 06:11:47 +00:00
// 导出按钮
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 = '导出选中' ;
2025-11-21 13:27:40 +00:00
}
} ) ;
2025-12-12 06:11:47 +00:00
// 提交按钮
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 ;
}
2025-11-21 13:27:40 +00:00
2025-12-12 06:11:47 +00:00
// 验证机种
const machineType = machineTypeSelect . value ;
if ( ! machineType ) {
machineTypeError . textContent = '请选择机种' ;
machineTypeError . style . display = 'block' ;
hasError = true ;
2025-11-21 13:27:40 +00:00
}
2025-12-15 01:38:42 +00:00
// 验证不良现象
const defectSymptom = symptomInput . value . trim ( ) ;
if ( ! defectSymptom ) {
symptomError . textContent = '请输入不良现象' ;
symptomError . style . display = 'block' ;
hasError = true ;
}
2025-12-12 06:11:47 +00:00
// 验证不良原因
2025-12-15 01:38:42 +00:00
const defectReason = defectInput . value . trim ( ) ;
if ( ! defectReason ) {
defectError . textContent = '请输入不良原因' ;
2025-12-12 06:11:47 +00:00
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 ) ;
2025-12-15 01:38:42 +00:00
formData . append ( 'defect_symptom' , defectSymptom ) ;
2025-12-12 06:11:47 +00:00
formData . append ( 'defect_reason' , defectReason ) ;
formData . append ( 'repair_method' , methodInput . value . trim ( ) ) ;
// 添加图片
pendingImages . forEach ( file => {
formData . append ( 'images' , file ) ;
} ) ;
// 显示加载状态
2025-11-21 13:27:40 +00:00
btn . disabled = true ;
2025-12-12 06:11:47 +00:00
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 = '' ;
2025-12-15 01:38:42 +00:00
symptomInput . value = '' ;
defectInput . value = '' ;
2025-12-12 06:11:47 +00:00
methodInput . value = '' ;
methodGroup . style . opacity = '0.5' ;
methodInput . disabled = true ;
methodRequired . style . display = 'none' ;
methodInput . placeholder = '请描述维修方法' ;
pendingImages = [ ] ;
previewContainer . innerHTML = '' ;
// 刷新列表
await refreshRepairsList ( ) ;
} else {
API . toast ( result . error || '提交失败' ) ;
2025-11-21 13:27:40 +00:00
}
2025-12-12 06:11:47 +00:00
} catch ( e ) {
API . toast ( '提交失败: ' + ( e . message || '网络错误' ) ) ;
2025-11-21 13:27:40 +00:00
} finally {
2025-12-12 06:11:47 +00:00
btn . disabled = false ;
uploadText . style . display = 'inline' ;
uploadLoading . style . display = 'none' ;
2025-11-21 13:27:40 +00:00
}
} ) ;
2025-12-12 06:11:47 +00:00
// 初始加载最新记录
setTimeout ( refreshRepairsList , 100 ) ;
2025-11-21 13:27:40 +00:00
}
async function bindDefectsEvents ( ) {
const btn = document . getElementById ( 'defects-upload' ) ;
addListener ( btn , 'click' , async ( ) => {
try {
const fileEl = document . getElementById ( 'defects-file' ) ;
const manual = document . getElementById ( 'defects-manual' ) . value . trim ( ) ;
let rows = [ ] ;
if ( fileEl . files [ 0 ] ) {
const text = await readText ( fileEl . files [ 0 ] ) ;
rows = text . split ( /\n+/ ) . map ( l => l . split ( ',' ) ) ;
rows = rows . map ( ( [ mac , batch ] ) => ( { mac , batch } ) ) ;
} else if ( manual ) {
rows = parseManual ( manual ) ;
}
if ( ! rows . length ) { throw new Error ( '请提供文件或手动输入' ) }
btn . disabled = true ;
await API . uploadDefects ( { rows } ) ;
API . toast ( '上传成功' ) ;
const listEl = document . getElementById ( 'defects-list' ) ;
const data = await API . listDefects ( ) . catch ( ( ) => ( { list : [ ] } ) ) ;
if ( listEl ) listEl . innerHTML = ( data . list || [ ] ) . slice ( 0 , 10 ) . map ( r => ` <li><span> ${ r . mac } </span><span class="badge"> ${ r . batch } </span></li> ` ) . join ( '' ) || '<li>暂无数据</li>' ;
} catch ( e ) {
API . toast ( '上传失败' ) ;
} finally {
if ( btn ) btn . disabled = false ;
}
} ) ;
}
} ) ( ) ;