import { roundTo } from '@/lib/decimal' import { formatThousandsFlexible } from '@/lib/numberFormat' import type { GridApi } from 'ag-grid-community' import { nextTick } from 'vue' export type ScaleBudgetCheckField = 'benchmarkBudgetBasicChecked' | 'benchmarkBudgetOptionalChecked' export type ScaleBudgetHeaderCheckState = 'all' | 'none' | 'partial' type BudgetCheckRow = { id: string benchmarkBudgetBasicChecked: boolean benchmarkBudgetOptionalChecked: boolean benchmarkBudgetBasic?: number | null benchmarkBudgetOptional?: number | null } export interface ScaleBudgetToggleHeaderParams { displayName?: string field: ScaleBudgetCheckField getHeaderCheckState?: (field: ScaleBudgetCheckField) => ScaleBudgetHeaderCheckState onToggleAll?: (field: ScaleBudgetCheckField, checked: boolean) => void } export class ScaleBudgetToggleHeader { private params!: ScaleBudgetToggleHeaderParams private eGui!: HTMLDivElement private checkbox!: HTMLInputElement private label!: HTMLSpanElement init(params: ScaleBudgetToggleHeaderParams) { this.params = params const root = document.createElement('div') root.style.display = 'inline-flex' root.style.alignItems = 'center' root.style.gap = '6px' root.style.width = '100%' const checkbox = document.createElement('input') checkbox.type = 'checkbox' checkbox.className = 'cursor-pointer' checkbox.addEventListener('click', event => event.stopPropagation()) checkbox.addEventListener('mousedown', event => event.stopPropagation()) checkbox.addEventListener('change', event => { event.stopPropagation() this.params.onToggleAll?.(this.params.field, checkbox.checked) }) const label = document.createElement('span') label.textContent = String(params.displayName || '') label.style.userSelect = 'none' label.addEventListener('click', event => event.stopPropagation()) root.append(checkbox, label) this.eGui = root this.checkbox = checkbox this.label = label this.applyCheckState() } getGui() { return this.eGui } refresh(params: ScaleBudgetToggleHeaderParams) { this.params = params this.label.textContent = String(params.displayName || '') this.applyCheckState() return true } destroy() { // noop } private applyCheckState() { const state = this.params.getHeaderCheckState?.(this.params.field) || 'none' this.checkbox.indeterminate = state === 'partial' this.checkbox.checked = state === 'all' } } export const formatScaleEditableNumber = (params: any, precision = 3, emptyText = '请输入') => { if (!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')) { return emptyText } if (params.value == null) return '' return formatThousandsFlexible(params.value, precision) } export const formatScaleEditableConditionalNumber = ( params: any, options: { enabled: boolean; precision?: number; emptyText?: string } ) => { if (!params.node?.group && !params.node?.rowPinned && !options.enabled) { return '' } if (!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')) { return options.emptyText ?? '点击输入' } if (params.value == null) return '' return formatThousandsFlexible(params.value, options.precision ?? 3) } export const formatScaleReadonlyMoney = (params: any) => { if (params.value == null || params.value === '') return '' return formatThousandsFlexible(roundTo(params.value, 3), 3) } export const updateScaleBudgetCheckState = ( rows: TRow[], rowId: string, checkField: ScaleBudgetCheckField, checked: boolean ) => { for (const row of rows) { if (row.id !== rowId) continue if (checkField === 'benchmarkBudgetBasicChecked') { row.benchmarkBudgetBasicChecked = checked row.benchmarkBudgetBasic = checked ? row.benchmarkBudgetBasic : 0 return } row.benchmarkBudgetOptionalChecked = checked row.benchmarkBudgetOptional = checked ? row.benchmarkBudgetOptional : 0 return } } export const createScaleBudgetCellRendererWithCheck = >( checkField: ScaleBudgetCheckField, options: { formatValue: (params: any) => string onToggle: (row: TRow, checked: boolean) => void } ) => (params: any) => { const valueText = options.formatValue(params) const hasValue = params.value != null && params.value !== '' if (params.node?.group || params.node?.rowPinned || !params.data || !hasValue) { return valueText } const wrapper = document.createElement('div') wrapper.style.display = 'flex' wrapper.style.alignItems = 'center' wrapper.style.justifyContent = 'space-between' wrapper.style.gap = '6px' wrapper.style.width = '100%' wrapper.addEventListener('pointerdown', event => event.stopPropagation()) wrapper.addEventListener('mousedown', event => event.stopPropagation()) wrapper.addEventListener('click', event => event.stopPropagation()) wrapper.addEventListener('dblclick', event => event.stopPropagation()) const checkbox = document.createElement('input') checkbox.type = 'checkbox' checkbox.className = 'cursor-pointer' checkbox.checked = params.data[checkField] !== false checkbox.addEventListener('pointerdown', event => event.stopPropagation()) checkbox.addEventListener('mousedown', event => event.stopPropagation()) checkbox.addEventListener('click', event => event.stopPropagation()) checkbox.addEventListener('change', event => { event.stopPropagation() const targetRow = params.data as TRow | undefined if (!targetRow) return options.onToggle(targetRow, checkbox.checked) void nextTick(() => { params.api?.redrawRows?.({ rowNodes: params.node ? [params.node] : undefined }) params.api?.refreshCells?.({ rowNodes: params.node ? [params.node] : undefined, force: true }) }) }) const valueSpan = document.createElement('span') valueSpan.textContent = valueText valueSpan.addEventListener('pointerdown', event => event.stopPropagation()) valueSpan.addEventListener('mousedown', event => event.stopPropagation()) valueSpan.addEventListener('click', event => event.stopPropagation()) wrapper.append(checkbox, valueSpan) return wrapper } export const createScaleBudgetCellRendererToggleFactory = ( getRows: () => TRow[], onAfterToggle: () => void ) => (checkField: ScaleBudgetCheckField) => createScaleBudgetCellRendererWithCheck(checkField, { formatValue: formatScaleReadonlyMoney, onToggle: (targetRow: TRow, checked: boolean) => { updateScaleBudgetCheckState(getRows(), targetRow.id, checkField, checked) onAfterToggle() } }) export const getScaleMergeColSpanBeforeTotal = (params: any) => { if (!params.node?.group && !params.node?.rowPinned) return 1 const displayedColumns = params.api?.getAllDisplayedColumns?.() if (!Array.isArray(displayedColumns) || !params.column) return 1 const currentIndex = displayedColumns.findIndex((column: any) => column.getColId() === params.column.getColId()) const totalIndex = displayedColumns.findIndex((column: any) => column.getColId() === 'budgetFeeTotal') if (currentIndex < 0 || totalIndex <= currentIndex) return 1 return totalIndex - currentIndex } export const refreshScaleGridAfterColumnReset = async (gridApi: GridApi | null | undefined) => { await nextTick() gridApi?.refreshHeader() gridApi?.refreshCells({ force: true }) } export const restoreScaleColumnDefaults = async (options: { gridApi: GridApi | null | undefined rows: TRow[] getCurrentValue: (row: TRow) => number | null | undefined getNextValue: (row: TRow) => number | null | undefined isSameValue: (left: number | null | undefined, right: number | null | undefined) => boolean applyValue: (row: TRow, nextValue: number | null) => void afterApply: () => Promise }) => { options.gridApi?.stopEditing() let changed = false for (const row of options.rows) { const nextValue = options.getNextValue(row) ?? null if (options.isSameValue(options.getCurrentValue(row), nextValue)) continue options.applyValue(row, nextValue) changed = true } if (!changed) return false await options.afterApply() await refreshScaleGridAfterColumnReset(options.gridApi) return true }