235 lines
8.3 KiB
TypeScript
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
|
|
}
|