calculator2026/src/lib/pricingScaleGrid.ts
2026-04-07 16:27:49 +08:00

235 lines
8.3 KiB
TypeScript

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 = <TRow extends BudgetCheckRow>(
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 = <TRow extends Record<string, any>>(
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 = <TRow extends BudgetCheckRow>(
getRows: () => TRow[],
onAfterToggle: () => void
) => (checkField: ScaleBudgetCheckField) =>
createScaleBudgetCellRendererWithCheck<TRow>(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 <TRow>(gridApi: GridApi<TRow> | null | undefined) => {
await nextTick()
gridApi?.refreshHeader()
gridApi?.refreshCells({ force: true })
}
export const restoreScaleColumnDefaults = async <TRow>(options: {
gridApi: GridApi<TRow> | 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<void>
}) => {
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
}