const api = require('../../utils/api') const EXPANDED_MONTH_WIDTH = 390 const COLLAPSED_MONTH_WIDTH = 84 const END_SPACER_WIDTH = 120 const ALL_ATTENDANCE_POINTS = '__ALL_ATTENDANCE_POINTS__' const monthLabel = month => `${month}月` const formatQty = value => { const number = Number(value || 0) if (!Number.isFinite(number)) { return '0' } return Number.isInteger(number) ? String(number) : String(Math.round(number * 100) / 100) } const sanitizeQty = value => { const text = String(value || '').replace(/[^\d.]/g, '') const parts = text.split('.') if (parts.length <= 1) { return parts[0] } return `${parts[0]}.${parts.slice(1).join('').slice(0, 2)}` } Page({ data: { years: [], yearLabels: [], yearIndex: 0, selectedYear: '', currentYear: '', currentMonth: '', months: [], rawRows: [], rows: [], pointOptions: [{ label: '全部考勤点', value: ALL_ATTENDANCE_POINTS }], pointLabels: ['全部考勤点'], pointIndex: 0, selectedPointValue: ALL_ATTENDANCE_POINTS, selectedPointLabel: '全部考勤点', monthTableWidth: 0, monthEndSpacerWidth: 0, totalReported: 0, totalReconciled: 0, totalReturn: 0, monthScrollLeft: 0, loading: false, refreshing: false, savingKey: '', }, onLoad() { this.loadYears() }, async onPullDownRefresh() { this.setData({ refreshing: true }) try { await this.loadLedger() } finally { this.setData({ refreshing: false }) if (wx.stopPullDownRefresh) { wx.stopPullDownRefresh() } } }, async loadYears() { this.setData({ loading: true }) try { const result = await api.listReconciliationYears() const years = result.years && result.years.length ? result.years : [result.currentYear] const selectedYear = result.currentYear || years[years.length - 1] const yearIndex = Math.max(0, years.findIndex(year => Number(year) === Number(selectedYear))) this.setData({ years, yearLabels: years.map(year => `${year}年`), currentYear: result.currentYear, selectedYear, yearIndex, }) await this.loadLedger() } catch (error) { wx.showToast({ title: error.message || '加载年份失败', icon: 'none' }) } finally { this.setData({ loading: false }) } }, buildMonths(year, currentYear, currentMonth) { const visibleCount = Number(year) === Number(currentYear) ? Number(currentMonth || 1) : 12 return Array.from({ length: visibleCount }, (_, index) => { const month = index + 1 const expanded = Number(year) === Number(currentYear) ? month === Number(currentMonth) : true return { month, label: monthLabel(month), expanded, width: expanded ? EXPANDED_MONTH_WIDTH : COLLAPSED_MONTH_WIDTH, } }) }, buildRows(rawRows, months) { return (rawRows || []).map(row => ({ attendancePointName: row.attendancePointName || '', productName: row.productName, uniqueKey: row.uniqueKey || `${row.attendancePointName || ''}||${row.productName}`, months: months.map(monthMeta => { const source = (row.months || []).find(item => Number(item.month) === Number(monthMeta.month)) || {} const reported = Number(source.reportedGoodQty || 0) const reconciled = Number(source.reconciledGoodQty || 0) const returnQty = Number(source.returnQty || 0) return { ...monthMeta, reportedGoodQty: reported, reconciledGoodQty: reconciled, returnQty, reportedGoodQtyText: formatQty(reported), reconciledGoodQtyInput: formatQty(reconciled), returnQtyInput: formatQty(returnQty), } }), })) }, buildPointOptions(rawRows) { const seen = {} const options = [{ label: '全部考勤点', value: ALL_ATTENDANCE_POINTS }] ;(rawRows || []).forEach(row => { const value = row.attendancePointName || '' if (seen[value]) { return } seen[value] = true options.push({ label: value || '未设置考勤点', value, }) }) return options }, getPointFilterState(rawRows) { const pointOptions = this.buildPointOptions(rawRows) const currentValue = this.data.selectedPointValue || ALL_ATTENDANCE_POINTS const matchedIndex = pointOptions.findIndex(item => item.value === currentValue) const pointIndex = matchedIndex >= 0 ? matchedIndex : 0 return { pointOptions, pointLabels: pointOptions.map(item => item.label), pointIndex, selectedPointValue: pointOptions[pointIndex].value, selectedPointLabel: pointOptions[pointIndex].label, } }, getFilteredRows(rawRows) { const selectedPointValue = this.data.selectedPointValue || ALL_ATTENDANCE_POINTS if (selectedPointValue === ALL_ATTENDANCE_POINTS) { return rawRows || [] } return (rawRows || []).filter(row => (row.attendancePointName || '') === selectedPointValue) }, rpxToPx(value) { let windowWidth = 375 try { const info = wx.getWindowInfo ? wx.getWindowInfo() : wx.getSystemInfoSync() windowWidth = Number(info && info.windowWidth) || windowWidth } catch (error) { windowWidth = 375 } return Math.round(Number(value || 0) * windowWidth / 750) }, getCurrentMonthScrollLeft(months, year, currentYear, currentMonth) { if (Number(year) !== Number(currentYear)) { return 0 } const targetMonth = Number(currentMonth || 1) const offsetRpx = (months || []).reduce((sum, item) => { if (Number(item.month) >= targetMonth) { return sum } return sum + Number(item.width || 0) }, 0) return this.rpxToPx(offsetRpx) }, alignToCurrentMonth(months, year, currentYear, currentMonth) { const scrollLeft = this.getCurrentMonthScrollLeft(months, year, currentYear, currentMonth) if (!scrollLeft) { this.setData({ monthScrollLeft: 0 }) return } const currentScrollLeft = Number(this.data.monthScrollLeft || 0) this.setData({ monthScrollLeft: currentScrollLeft === scrollLeft ? scrollLeft - 1 : scrollLeft }) setTimeout(() => { this.setData({ monthScrollLeft: scrollLeft }) }, 80) }, applyMonthView(rawRows, months) { const rows = this.buildRows(this.getFilteredRows(rawRows), months) const monthEndSpacerWidth = Number(this.data.selectedYear) === Number(this.data.currentYear) ? END_SPACER_WIDTH : 0 const monthTableWidth = months.reduce((sum, item) => sum + Number(item.width || 0), 0) + monthEndSpacerWidth const totals = rows.reduce((result, row) => { row.months.forEach(month => { result.reported += Number(month.reportedGoodQty || 0) result.reconciled += Number(month.reconciledGoodQty || 0) result.returnQty += Number(month.returnQty || 0) }) return result }, { reported: 0, reconciled: 0, returnQty: 0 }) this.setData({ rows, months, monthTableWidth, monthEndSpacerWidth, totalReported: formatQty(totals.reported), totalReconciled: formatQty(totals.reconciled), totalReturn: formatQty(totals.returnQty), }) }, async loadLedger() { if (!this.data.selectedYear) { return } this.setData({ loading: true }) try { const result = await api.listReconciliationLedger(this.data.selectedYear) const months = this.buildMonths(result.year, result.currentYear, result.currentMonth) this.setData({ selectedYear: result.year, currentYear: result.currentYear, currentMonth: result.currentMonth, rawRows: result.rows || [], ...this.getPointFilterState(result.rows || []), }) this.applyMonthView(result.rows || [], months) this.alignToCurrentMonth(months, result.year, result.currentYear, result.currentMonth) } catch (error) { wx.showToast({ title: error.message || '加载账本失败', icon: 'none' }) } finally { this.setData({ loading: false }) } }, onYearChange(e) { const yearIndex = Number(e.detail.value) const selectedYear = this.data.years[yearIndex] this.setData({ yearIndex, selectedYear }) this.loadLedger() }, onPointChange(e) { const pointIndex = Number(e.detail.value) const selected = this.data.pointOptions[pointIndex] || this.data.pointOptions[0] this.setData({ pointIndex, selectedPointValue: selected ? selected.value : ALL_ATTENDANCE_POINTS, selectedPointLabel: selected ? selected.label : '全部考勤点', }) this.applyMonthView(this.data.rawRows, this.data.months) }, toggleMonth(e) { const index = Number(e.currentTarget.dataset.index) const months = this.data.months.map((item, itemIndex) => { if (itemIndex !== index) { return item } const expanded = !item.expanded return { ...item, expanded, width: expanded ? EXPANDED_MONTH_WIDTH : COLLAPSED_MONTH_WIDTH, } }) this.applyMonthView(this.data.rawRows, months) }, onQtyInput(e) { const rowIndex = Number(e.currentTarget.dataset.rowIndex) const monthIndex = Number(e.currentTarget.dataset.monthIndex) const field = e.currentTarget.dataset.field || 'reconciledGoodQty' const value = sanitizeQty(e.detail.value) this.setData({ [`rows[${rowIndex}].months[${monthIndex}].${field}Input`]: value, }) }, updateRawCell(attendancePointName, productName, month, field, value) { const rawRows = this.data.rawRows.map(row => { if (row.productName !== productName || row.attendancePointName !== attendancePointName) { return row } return { ...row, months: row.months.map(item => ( Number(item.month) === Number(month) ? { ...item, [field]: value } : item )), } }) this.setData({ rawRows }) return rawRows }, async saveCell(e) { const rowIndex = Number(e.currentTarget.dataset.rowIndex) const monthIndex = Number(e.currentTarget.dataset.monthIndex) const field = e.currentTarget.dataset.field || 'reconciledGoodQty' const inputField = `${field}Input` const row = this.data.rows[rowIndex] const month = row && row.months ? row.months[monthIndex] : null if (!row || !month) { return } const inputText = sanitizeQty(e.detail.value) const value = Number(inputText || 0) if (!Number.isFinite(value) || value < 0) { wx.showToast({ title: '只能填写数字', icon: 'none' }) return } const roundedValue = Math.round(value * 100) / 100 const key = `${row.attendancePointName}-${row.productName}-${month.month}-${field}` if (roundedValue === Number(month[field] || 0)) { this.setData({ [`rows[${rowIndex}].months[${monthIndex}].${inputField}`]: formatQty(roundedValue), }) return } this.setData({ savingKey: key }) try { await api.saveReconciliationEntry({ year: this.data.selectedYear, month: month.month, attendancePointName: row.attendancePointName, productName: row.productName, [field]: roundedValue, }) const rawRows = this.updateRawCell(row.attendancePointName, row.productName, month.month, field, roundedValue) this.applyMonthView(rawRows, this.data.months) } catch (error) { wx.showToast({ title: error.message || '保存失败', icon: 'none' }) } finally { this.setData({ savingKey: '' }) } }, })