const api = require('../../utils/api') const pad = value => { const text = String(value) return text.length > 1 ? text : `0${text}` } const datePart = value => { const date = new Date(value) if (Number.isNaN(date.getTime())) { return '' } return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}` } const timePart = value => { const date = new Date(value) if (Number.isNaN(date.getTime())) { return '' } return `${pad(date.getHours())}:${pad(date.getMinutes())}` } const dateTimeValue = (date, time) => (date && time ? `${date}T${time}:00` : '') const productChoiceKey = item => `${item.attendancePointName || ''}||${item.projectNo}||${item.productName}` const optionKey = item => `${item.attendancePointName || ''}||${item.projectNo}||${item.productName}||${item.process || ''}||${api.normalizeDeviceNo(item.deviceNo)}` const segmentKey = item => `${api.normalizeDeviceNo(item.moldName || item.productName || item.deviceNo)}||${String(item.process || '').trim()}` const round2 = value => Math.round(Number(value || 0) * 100) / 100 const withCompareClasses = item => { const standardWorkload = item.isCleaning ? 0 : (item.isMisc ? 0 : (api.calculateStandardWorkload(item.allocatedMinutes, item.standardBeat) || Number(item.standardWorkload || 0))) const beatComparison = (item.isCleaning || item.isMisc) ? api.classifyBeat(0, 0) : api.classifyBeat(item.actualBeat, item.standardBeat) const workloadComparison = (item.isCleaning || item.isMisc) ? api.classifyWorkload(0, 0) : api.classifyWorkload(item.goodQty, standardWorkload) return { ...item, standardWorkload, beatCompareClass: beatComparison.className, beatReason: beatComparison.reason, workloadCompareClass: workloadComparison.className, workloadReason: workloadComparison.reason, } } const correctionOrCurrent = (corrections, key, currentValue) => ( corrections && corrections[key] !== undefined ? corrections[key] : currentValue ) const itemActualBeat = item => { const outputQty = Number(item.goodQty || 0) + Number(item.defectQty || 0) return outputQty > 0 ? round2(Number(item.allocatedMinutes || 0) * 60 / outputQty) : 0 } Page({ data: { user: null, rows: [], page: 1, pageInput: '1', totalPages: 1, focusReportId: '', refreshing: false, scrollTop: 0, expandedReportId: '', detailExpandedAll: false, filterVoided: true, productModalVisible: false, productModalRows: [], productModalKeyword: '', productModalPage: 1, productModalPageInput: '1', productModalTotalPages: 1, productModalContext: null, productModalLoading: false, }, onLoad(options) { this.setData({ focusReportId: options.reportId || '' }) }, onShow() { this.load() }, noop() {}, onScroll(e) { this.currentScrollTop = e.detail.scrollTop || 0 }, toggleDetails() { this.setData({ detailExpandedAll: !this.data.detailExpandedAll, expandedReportId: '', }) }, queryReportTop(id) { return new Promise(resolve => { this.createSelectorQuery() .select(`#review-report-${id}`) .boundingClientRect(rect => { resolve(rect ? rect.top : null) }) .exec() }) }, restoreReportPosition(id, beforeTop) { if (beforeTop === null || beforeTop === undefined) { return } wx.nextTick(() => { this.queryReportTop(id).then(afterTop => { if (afterTop === null || afterTop === undefined) { return } const nextScrollTop = Math.max( 0, (this.currentScrollTop || 0) + afterTop - beforeTop, ) this.currentScrollTop = nextScrollTop this.setData({ scrollTop: nextScrollTop }) }) }) }, toggleReportDetail(e) { const id = String(e.currentTarget.dataset.id || '') const switchingReport = this.data.detailExpandedAll || (!!this.data.expandedReportId && this.data.expandedReportId !== id) this.queryReportTop(id).then(beforeTop => { this.setData({ detailExpandedAll: false, expandedReportId: this.data.expandedReportId === id ? '' : id, }, () => { if (switchingReport) { this.restoreReportPosition(id, beforeTop) } }) }) }, async load(page = this.data.page) { const user = api.getCurrentUser() try { let result if (this.data.focusReportId) { const report = await api.getReport(this.data.focusReportId) result = { rows: (report.status === 'pending' || report.canEdit || report.isVoided) ? [report] : [], page: 1, totalPages: 1, } } else { result = await api.listReports({ status: 'pending', page, pageSize: 6, includeVoided: !this.data.filterVoided, }) } const rows = await this.buildReviewRows(result.rows || []) const defaultExpandedId = this.data.focusReportId && rows.length ? String(rows[0].id) : '' this.setData({ user, rows, page: result.page, pageInput: String(result.page || 1), totalPages: result.totalPages, expandedReportId: defaultExpandedId, detailExpandedAll: false, }) } catch (error) { this.setData({ user, rows: [] }) wx.showToast({ title: error.message, icon: 'none' }) } }, async onPullDownRefresh() { this.setData({ refreshing: true }) try { await this.load(this.data.page) } finally { this.setData({ refreshing: false }) if (wx.stopPullDownRefresh) { wx.stopPullDownRefresh() } } }, async buildReviewRows(rows) { const hydrated = [] for (const row of rows) { const editItems = [] const editDeviceSegments = (row.deviceSegments || []).map((segment, index) => this.makeEditDeviceSegment(segment, index)) for (const detail of row.items || []) { const editItem = await this.buildReviewItem(detail) const itemKey = segmentKey(editItem) editItem.deviceSegments = editDeviceSegments.filter(segment => segmentKey(segment) === itemKey) editItems.push(editItem) } hydrated.push({ ...row, timeRangeText: row.isCleaning ? '清洗报工' : `${row.startAtText} 至 ${row.endAtText}`, editStartDate: datePart(row.startAt), editStartTime: timePart(row.startAt), editEndDate: datePart(row.endAt), editEndTime: timePart(row.endAt), editReviewRemark: row.reviewRemark || '', editItems, }) } return hydrated }, async buildReviewItem(detail) { const [products, equipmentOptions] = await Promise.all([ this.loadProductOptions(detail.productName, detail.attendancePointName), detail.isMisc ? Promise.resolve([]) : this.loadEquipmentOptions(detail.attendancePointName, !!detail.isCleaning), ]) return this.makeEditItem(detail, products, equipmentOptions) }, async loadProductOptions(productName, attendancePointName = '', keyword = '') { try { const result = await api.listProducts({ productName: keyword ? '' : productName, keyword, attendancePointName, page: 1, pageSize: 100, }) return result.rows || [] } catch (error) { return [] } }, async loadEquipmentOptions(attendancePointName = '', isCleaning = false) { try { const result = await api.listEquipment({ attendancePointName, deviceType: isCleaning ? '清洗设备' : '冲压设备', page: 1, pageSize: 100, }) return result.rows || [] } catch (error) { return [] } }, makeEditItem(detail, products, equipmentOptions = []) { const corrections = detail.corrections || {} const current = { attendancePointName: detail.attendancePointName || '', projectNo: detail.projectNo, productName: detail.productName, process: detail.process || '', stampingMethod: detail.stampingMethod || '', deviceNo: detail.deviceNo, materialCode: detail.materialCode || '', materialName: detail.materialName || '', rawMaterialBatchNo: detail.rawMaterialBatchNo || '', standardBeat: detail.standardBeat || 0, standardWorkload: detail.standardWorkload || 0, isCleaning: !!detail.isCleaning, isContinuousDie: !!detail.isContinuousDie, isMisc: !!detail.isMisc, isMultiPerson: !!detail.isMultiPerson, operatorCount: detail.operatorCount || 1, changeoverCount: detail.changeoverCount || 0, remark: detail.remark || '', } const originalProduct = { attendancePointName: current.attendancePointName, projectNo: correctionOrCurrent(corrections, 'project_no', current.projectNo), productName: correctionOrCurrent(corrections, 'product_name', current.productName), process: correctionOrCurrent(corrections, 'process_name', current.process) || '', materialCode: current.materialCode, materialName: current.materialName, stampingMethod: correctionOrCurrent(corrections, 'stamping_method', current.stampingMethod) || '', operatorCount: current.operatorCount, standardBeat: current.standardBeat, isCleaning: current.isCleaning, isContinuousDie: current.isContinuousDie, isMisc: current.isMisc, isMultiPerson: current.isMultiPerson, } const optionMap = {} ;(products || []).forEach(product => { optionMap[optionKey(product)] = product }) optionMap[optionKey(current)] = current const options = Object.keys(optionMap).map(key => optionMap[key]) const productMap = {} options.forEach(product => { productMap[productChoiceKey(product)] = { attendancePointName: product.attendancePointName || current.attendancePointName, projectNo: product.projectNo, productName: product.productName, } }) const productChoices = Object.keys(productMap).map(key => productMap[key]) const productLabels = productChoices.map(item => `${item.projectNo} · ${item.productName}`) let productIndex = productChoices.findIndex(item => item.projectNo === current.projectNo && item.productName === current.productName) if (productIndex < 0) { productIndex = 0 } const selectedProduct = productChoices[productIndex] || current const processOptions = options.filter(item => ( item.attendancePointName === selectedProduct.attendancePointName && item.projectNo === selectedProduct.projectNo && item.productName === selectedProduct.productName )) const processLabels = processOptions.map(item => item.process || '未填写工序') let processIndex = processOptions.findIndex(item => (item.process || '') === current.process) if (processIndex < 0) { processIndex = 0 } const selectedOption = processOptions[processIndex] || current const selectedDeviceNo = api.normalizeDeviceNo(detail.deviceNo) const deviceIndex = selectedDeviceNo ? equipmentOptions.findIndex(item => api.normalizeDeviceNo(item.deviceNo) === selectedDeviceNo) : -1 return withCompareClasses({ id: detail.id, attendancePointName: current.attendancePointName, startAt: detail.startAt || '', isCleaning: !!detail.isCleaning, isContinuousDie: !!selectedOption.isContinuousDie, isMisc: !!detail.isMisc, isMultiPerson: !!selectedOption.isMultiPerson, operatorCount: selectedOption.operatorCount || detail.operatorCount || 1, changeoverCount: detail.changeoverCount || 0, changeoverCountError: false, remark: detail.remark || '', deviceError: false, deviceNo: detail.deviceNo, deviceIndex, equipmentOptions, projectNo: selectedOption.projectNo, productName: selectedOption.productName, process: selectedOption.process || '', stampingMethod: selectedOption.stampingMethod || current.stampingMethod || '', rawMaterialBatchNo: detail.rawMaterialBatchNo || '', productSearchKeyword: '', productOptions: options, productChoices, productLabels, productIndex, processOptions, processLabels, processIndex, originalProduct, goodQty: detail.goodQty, defectQty: detail.defectQty, allocatedMinutes: detail.allocatedMinutes || 0, workTimeText: detail.workTimeText || '0秒', actualBeat: detail.actualBeat || 0, standardBeat: selectedOption.standardBeat || detail.standardBeat || 0, standardWorkload: selectedOption.standardWorkload || detail.standardWorkload || 0, deviceSegments: detail.deviceSegments || [], }) }, productToEditItem(item, product) { const selectedDeviceNo = api.normalizeDeviceNo(item.deviceNo) const deviceIndex = selectedDeviceNo ? (item.equipmentOptions || []).findIndex(equipment => api.normalizeDeviceNo(equipment.deviceNo) === selectedDeviceNo) : -1 const nextItem = { ...item, attendancePointName: item.attendancePointName, projectNo: product.projectNo || item.projectNo, productName: product.productName || item.productName, materialCode: product.materialCode || '', materialName: product.materialName || '', process: product.process || '', stampingMethod: product.stampingMethod || '', standardBeat: Number(product.standardBeat || 0), standardWorkload: api.calculateStandardWorkload(item.allocatedMinutes, product.standardBeat), operatorCount: product.operatorCount || 1, isCleaning: !!product.isCleaning, isContinuousDie: !!product.isContinuousDie, isMisc: !!product.isMisc, isMultiPerson: !!product.isMultiPerson, changeoverCount: product.isContinuousDie ? (item.changeoverCount || 0) : 0, changeoverCountError: false, actualBeat: itemActualBeat(item), deviceIndex, productOptions: [product], productChoices: [product], productLabels: [`${product.projectNo || ''} · ${product.productName || ''}`], productIndex: 0, processOptions: [product], processLabels: [product.process || '未填写工序'], processIndex: 0, } return withCompareClasses(nextItem) }, replaceEditProduct(reportIndex, itemIndex, product) { const item = this.data.rows[reportIndex].editItems[itemIndex] if (!item || !product) { return } this.replaceEditItem(reportIndex, itemIndex, this.productToEditItem(item, product)) }, async openProductModal(e) { const reportIndex = Number(e.currentTarget.dataset.reportIndex) const itemIndex = Number(e.currentTarget.dataset.itemIndex) const row = this.data.rows[reportIndex] const item = row && row.editItems ? row.editItems[itemIndex] : null if (!item) { wx.showToast({ title: '报工明细不存在', icon: 'none' }) return } this.setData({ productModalVisible: true, productModalContext: { reportIndex, itemIndex }, productModalRows: [], productModalKeyword: '', productModalPage: 1, productModalPageInput: '1', productModalTotalPages: 1, }) await this.loadProductModal(1) }, closeProductModal() { this.setData({ productModalVisible: false, productModalRows: [], productModalKeyword: '', productModalContext: null, }) }, onDeviceChange(e) { const reportIndex = Number(e.currentTarget.dataset.reportIndex) const itemIndex = Number(e.currentTarget.dataset.itemIndex) const deviceIndex = Number(e.detail.value) const item = this.data.rows[reportIndex].editItems[itemIndex] const equipment = (item.equipmentOptions || [])[deviceIndex] if (!equipment) { return } this.setData({ [`rows[${reportIndex}].editItems[${itemIndex}].deviceIndex`]: deviceIndex, [`rows[${reportIndex}].editItems[${itemIndex}].deviceNo`]: equipment.deviceNo, [`rows[${reportIndex}].editItems[${itemIndex}].deviceError`]: false, }) }, async loadProductModal(page = this.data.productModalPage) { const context = this.data.productModalContext if (!context) { return } const item = this.data.rows[context.reportIndex].editItems[context.itemIndex] this.setData({ productModalLoading: true }) try { const result = await api.listProducts({ keyword: this.data.productModalKeyword, attendancePointName: item.attendancePointName, page, pageSize: 8, }) this.setData({ productModalRows: result.rows || [], productModalPage: result.page, productModalPageInput: String(result.page || 1), productModalTotalPages: result.totalPages, }) } catch (error) { wx.showToast({ title: error.message || '加载产品失败', icon: 'none' }) } finally { this.setData({ productModalLoading: false }) } }, onProductModalKeywordInput(e) { this.setData({ productModalKeyword: e.detail.value }) this.loadProductModal(1) }, replaceProductFromModal(e) { const index = Number(e.currentTarget.dataset.index) const context = this.data.productModalContext const product = this.data.productModalRows[index] if (!context || !product) { return } const item = this.data.rows[context.reportIndex].editItems[context.itemIndex] if (product.isMisc) { wx.showToast({ title: '处理杂活不能替换到普通报工', icon: 'none' }) return } if (!!item.isCleaning !== !!product.isCleaning) { wx.showToast({ title: item.isCleaning ? '清洗报工只能替换清洗工序' : '普通报工不能替换为清洗工序', icon: 'none' }) return } this.replaceEditProduct(context.reportIndex, context.itemIndex, product) this.closeProductModal() }, productModalPrevPage() { if (this.data.productModalPage > 1) { this.loadProductModal(this.data.productModalPage - 1) } }, productModalNextPage() { if (this.data.productModalPage < this.data.productModalTotalPages) { this.loadProductModal(this.data.productModalPage + 1) } }, onProductModalPageInput(e) { this.setData({ productModalPageInput: e.detail.value }) }, jumpProductModalPage(e) { wx.hideKeyboard() const inputValue = e && e.detail && e.detail.value !== undefined ? e.detail.value : this.data.productModalPageInput const target = Math.max(1, Math.min(Number(inputValue) || this.data.productModalPage, this.data.productModalTotalPages)) if (target === this.data.productModalPage) { this.setData({ productModalPageInput: String(this.data.productModalPage || 1) }) return } this.loadProductModal(target) }, async restoreProduct(e) { const reportIndex = Number(e.currentTarget.dataset.reportIndex) const itemIndex = Number(e.currentTarget.dataset.itemIndex) const item = this.data.rows[reportIndex].editItems[itemIndex] if (!item || !item.originalProduct) { return } const original = item.originalProduct wx.showLoading({ title: '恢复中' }) try { const products = await this.loadProductOptions(original.productName, item.attendancePointName) const product = (products || []).find(option => ( option.projectNo === original.projectNo && option.productName === original.productName && (option.process || '') === (original.process || '') )) || original this.replaceEditProduct(reportIndex, itemIndex, { ...product, projectNo: original.projectNo, productName: original.productName, process: original.process || '', stampingMethod: product.stampingMethod || original.stampingMethod || '', materialCode: product.materialCode || original.materialCode || '', materialName: product.materialName || original.materialName || '', operatorCount: product.operatorCount || original.operatorCount || 1, standardBeat: product.standardBeat === undefined ? original.standardBeat : product.standardBeat, isCleaning: product.isCleaning === undefined ? original.isCleaning : product.isCleaning, isContinuousDie: product.isContinuousDie === undefined ? original.isContinuousDie : product.isContinuousDie, isMisc: product.isMisc === undefined ? original.isMisc : product.isMisc, isMultiPerson: product.isMultiPerson === undefined ? original.isMultiPerson : product.isMultiPerson, changeoverCount: (product.isContinuousDie === undefined ? original.isContinuousDie : product.isContinuousDie) ? (item.changeoverCount || 0) : 0, }) } catch (error) { wx.showToast({ title: error.message || '恢复失败', icon: 'none' }) } finally { wx.hideLoading() } }, makeEditDeviceSegment(segment, index) { const editScanDate = datePart(segment.scannedAt) const editScanTime = timePart(segment.scannedAt) return { ...segment, label: index === 0 ? '开始模具' : '换模具', editScanDate, editScanTime, originalScanDate: editScanDate, originalScanTime: editScanTime, } }, replaceEditItem(reportIndex, itemIndex, item) { this.setData({ [`rows[${reportIndex}].editItems[${itemIndex}]`]: item, }) }, onDeviceSegmentFieldChange(e) { const reportIndex = e.currentTarget.dataset.reportIndex const itemIndex = e.currentTarget.dataset.itemIndex const segmentIndex = e.currentTarget.dataset.segmentIndex const field = e.currentTarget.dataset.field this.setData({ [`rows[${reportIndex}].editItems[${itemIndex}].deviceSegments[${segmentIndex}].${field}`]: e.detail.value, }) }, onReportFieldChange(e) { const reportIndex = e.currentTarget.dataset.reportIndex const field = e.currentTarget.dataset.field this.setData({ [`rows[${reportIndex}].${field}`]: e.detail.value, }) }, onItemInput(e) { const reportIndex = e.currentTarget.dataset.reportIndex const itemIndex = e.currentTarget.dataset.itemIndex const field = e.currentTarget.dataset.field const value = e.detail.value const updates = { [`rows[${reportIndex}].editItems[${itemIndex}].${field}`]: value, } if (field === 'goodQty' || field === 'defectQty') { const item = { ...this.data.rows[reportIndex].editItems[itemIndex], [field]: value, } const outputQty = Number(item.goodQty || 0) + Number(item.defectQty || 0) updates[`rows[${reportIndex}].editItems[${itemIndex}].actualBeat`] = outputQty > 0 ? round2(Number(item.allocatedMinutes || 0) * 60 / outputQty) : 0 const nextItem = { ...item, actualBeat: updates[`rows[${reportIndex}].editItems[${itemIndex}].actualBeat`], } const compared = withCompareClasses(nextItem) updates[`rows[${reportIndex}].editItems[${itemIndex}].standardWorkload`] = compared.standardWorkload updates[`rows[${reportIndex}].editItems[${itemIndex}].beatCompareClass`] = compared.beatCompareClass updates[`rows[${reportIndex}].editItems[${itemIndex}].beatReason`] = compared.beatReason updates[`rows[${reportIndex}].editItems[${itemIndex}].workloadCompareClass`] = compared.workloadCompareClass updates[`rows[${reportIndex}].editItems[${itemIndex}].workloadReason`] = compared.workloadReason } if (field === 'changeoverCount') { updates[`rows[${reportIndex}].editItems[${itemIndex}].changeoverCountError`] = false } this.setData(updates) }, async reloadItemOptions(e) { const reportIndex = e.currentTarget.dataset.reportIndex const itemIndex = e.currentTarget.dataset.itemIndex const item = this.data.rows[reportIndex].editItems[itemIndex] wx.showLoading({ title: '匹配中' }) try { const keyword = String(item.productSearchKeyword || '').trim() const products = await this.loadProductOptions(item.productName, item.attendancePointName, keyword) this.replaceEditItem(reportIndex, itemIndex, this.makeEditItem(item, products)) } finally { wx.hideLoading() } }, onProductSearchInput(e) { const reportIndex = e.currentTarget.dataset.reportIndex const itemIndex = e.currentTarget.dataset.itemIndex this.setData({ [`rows[${reportIndex}].editItems[${itemIndex}].productSearchKeyword`]: e.detail.value, }) }, onProductChange(e) { const reportIndex = e.currentTarget.dataset.reportIndex const itemIndex = e.currentTarget.dataset.itemIndex const item = this.data.rows[reportIndex].editItems[itemIndex] const productIndex = Number(e.detail.value) const selectedProduct = item.productChoices[productIndex] const processOptions = item.productOptions.filter(option => ( option.attendancePointName === selectedProduct.attendancePointName && option.projectNo === selectedProduct.projectNo && option.productName === selectedProduct.productName )) const nextOption = processOptions[0] || selectedProduct this.replaceEditItem(reportIndex, itemIndex, withCompareClasses({ ...item, productIndex, attendancePointName: item.attendancePointName, projectNo: selectedProduct.projectNo, productName: selectedProduct.productName, processOptions, processLabels: processOptions.map(option => option.process || '未填写工序'), processIndex: 0, process: nextOption.process || '', stampingMethod: nextOption.stampingMethod || '', deviceNo: item.deviceNo, standardBeat: nextOption.standardBeat || 0, standardWorkload: nextOption.standardWorkload || 0, operatorCount: nextOption.operatorCount || 1, isMultiPerson: !!nextOption.isMultiPerson, })) }, onProcessChange(e) { const reportIndex = e.currentTarget.dataset.reportIndex const itemIndex = e.currentTarget.dataset.itemIndex const item = this.data.rows[reportIndex].editItems[itemIndex] const processIndex = Number(e.detail.value) const selected = item.processOptions[processIndex] if (!selected) { return } this.replaceEditItem(reportIndex, itemIndex, withCompareClasses({ ...item, processIndex, attendancePointName: item.attendancePointName, deviceNo: item.deviceNo, projectNo: selected.projectNo, productName: selected.productName, process: selected.process || '', stampingMethod: selected.stampingMethod || '', standardBeat: selected.standardBeat || 0, standardWorkload: selected.standardWorkload || 0, operatorCount: selected.operatorCount || 1, isMultiPerson: !!selected.isMultiPerson, })) }, async approve(e) { const id = e.currentTarget.dataset.id const user = this.data.user if (!user || user.role !== 'admin') { wx.showToast({ title: '无审核权限', icon: 'none' }) return } const row = this.data.rows.find(item => String(item.id) === String(id)) if (!row) { wx.showToast({ title: '报工记录不存在', icon: 'none' }) return } if (row.isVoided) { wx.showToast({ title: '作废记录不能编辑', icon: 'none' }) return } if (!row.canEdit) { wx.showToast({ title: '已超过可编辑期限', icon: 'none' }) return } const invalidItem = (row.editItems || []).find(item => ( Number(item.goodQty || 0) < 0 || Number(item.defectQty || 0) < 0 || !Number.isFinite(Number(item.goodQty || 0)) || !Number.isFinite(Number(item.defectQty || 0)) )) if (invalidItem) { wx.showToast({ title: '产出数量不能为负数', icon: 'none' }) return } const invalidChangeoverItemIndex = (row.editItems || []).findIndex(item => { if (!item.isContinuousDie) { return false } const value = Number(item.changeoverCount) return !Number.isInteger(value) || value < 0 }) if (invalidChangeoverItemIndex >= 0) { this.setData({ [`rows[${this.data.rows.findIndex(item => String(item.id) === String(id))}].editItems[${invalidChangeoverItemIndex}].changeoverCountError`]: true, }) wx.showToast({ title: '请填写非负整数换料次数', icon: 'none' }) return } const missingDeviceItemIndex = (row.editItems || []).findIndex(item => !item.isMisc && !api.normalizeDeviceNo(item.deviceNo)) if (missingDeviceItemIndex >= 0) { this.setData({ [`rows[${this.data.rows.findIndex(item => String(item.id) === String(id))}].editItems[${missingDeviceItemIndex}].deviceError`]: true, }) wx.showToast({ title: '请选择设备号', icon: 'none' }) return } await this.submitApprove(id, row, user) }, async submitApprove(id, row, user) { const deviceCorrections = (row.editItems || []).flatMap(item => item.deviceSegments || []) .filter(segment => ( segment.editScanDate !== segment.originalScanDate || segment.editScanTime !== segment.originalScanTime )) .map(segment => ({ id: segment.id, scannedAt: dateTimeValue(segment.editScanDate, segment.editScanTime), })) const invalidDeviceCorrection = deviceCorrections.find(item => !item.scannedAt) if (invalidDeviceCorrection) { wx.showToast({ title: '换模具时间不能为空', icon: 'none' }) return } const patch = { startAt: row.isCleaning ? null : dateTimeValue(row.editStartDate, row.editStartTime), endAt: row.isCleaning ? null : dateTimeValue(row.editEndDate, row.editEndTime), deviceCorrections: row.isCleaning ? [] : deviceCorrections, reviewRemark: row.editReviewRemark || '', itemCorrections: (row.editItems || []).map(item => ({ id: item.id, device_no: api.normalizeDeviceNo(item.deviceNo), project_no: item.projectNo, product_name: item.productName, raw_material_batch_no: item.rawMaterialBatchNo || null, process_name: item.process || '', changeover_count: item.isContinuousDie ? Number(item.changeoverCount || 0) : 0, good_qty: Number(item.goodQty || 0), defect_qty: Number(item.defectQty || 0), scrap_qty: 0, remark: item.remark || null, })), } try { await api.approveReport(id, user, patch) wx.showToast({ title: row.status === 'approved' ? '已保存更改' : '已审核通过', icon: 'success' }) if (this.data.focusReportId) { setTimeout(() => { wx.navigateBack({ delta: 1, fail: () => { wx.redirectTo({ url: '/pages/records/records?mode=pending&status=pending', }) }, }) }, 500) return } await this.load(this.data.page) } catch (error) { wx.showToast({ title: error.message, icon: 'none' }) } }, prevPage() { if (this.data.page > 1) { this.load(this.data.page - 1) } }, nextPage() { if (this.data.page < this.data.totalPages) { this.load(this.data.page + 1) } }, onPageInput(e) { this.setData({ pageInput: e.detail.value }) }, jumpToPage(e) { wx.hideKeyboard() const inputValue = e && e.detail && e.detail.value !== undefined ? e.detail.value : this.data.pageInput const target = Math.max(1, Math.min(Number(inputValue) || this.data.page, this.data.totalPages)) if (target === this.data.page) { this.setData({ pageInput: String(this.data.page || 1) }) return } this.load(target) }, toggleFilterVoided() { this.setData({ filterVoided: !this.data.filterVoided, page: 1 }) this.load(1) }, onFilterVoidedChange(e) { this.setData({ filterVoided: !!e.detail.value, page: 1 }) this.load(1) }, async voidReport(e) { const id = e.currentTarget.dataset.id wx.showModal({ title: '确认作废', content: '作废的记录将在30天后被系统彻底删除,彻底删除前可随时撤销作废,是否确定删除', cancelText: '否', confirmText: '是', confirmColor: '#c5221f', success: async res => { if (!res.confirm) { return } wx.showLoading({ title: '处理中' }) try { await api.voidReport(id) wx.showToast({ title: '已作废', icon: 'success' }) await this.load(this.data.page) } catch (error) { wx.showToast({ title: error.message || '作废失败', icon: 'none' }) } finally { wx.hideLoading() } }, }) }, async unvoidReport(e) { const id = e.currentTarget.dataset.id wx.showLoading({ title: '处理中' }) try { await api.unvoidReport(id) wx.showToast({ title: '已撤销作废', icon: 'success' }) await this.load(this.data.page) } catch (error) { wx.showToast({ title: error.message || '撤销失败', icon: 'none' }) } finally { wx.hideLoading() } }, })