1
This commit is contained in:
parent
35f06746fe
commit
be2662d579
@ -56,6 +56,7 @@ interface DetailRow {
|
|||||||
hourly: number | null
|
hourly: number | null
|
||||||
subtotal?: number | null
|
subtotal?: number | null
|
||||||
finalFee?: number | null
|
finalFee?: number | null
|
||||||
|
remark?: string
|
||||||
actions?: unknown
|
actions?: unknown
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -180,6 +181,7 @@ const normalizeDetailRow = (row: Partial<DetailRow>): DetailRow => ({
|
|||||||
hourly: typeof row.hourly === 'number' ? row.hourly : null,
|
hourly: typeof row.hourly === 'number' ? row.hourly : null,
|
||||||
subtotal: typeof row.subtotal === 'number' ? row.subtotal : null,
|
subtotal: typeof row.subtotal === 'number' ? row.subtotal : null,
|
||||||
finalFee: typeof row.finalFee === 'number' ? row.finalFee : null,
|
finalFee: typeof row.finalFee === 'number' ? row.finalFee : null,
|
||||||
|
remark: String(row.remark || ''),
|
||||||
actions: row.actions
|
actions: row.actions
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -408,11 +410,11 @@ const createMethodColumn = (
|
|||||||
): ColDef<DetailRow> => ({
|
): ColDef<DetailRow> => ({
|
||||||
headerName,
|
headerName,
|
||||||
field,
|
field,
|
||||||
headerClass: 'ag-right-aligned-header',
|
|
||||||
minWidth,
|
minWidth,
|
||||||
flex: 1.5,
|
flex: 1.5,
|
||||||
editable: false,
|
editable: false,
|
||||||
cellClassRules: {
|
cellClassRules: {
|
||||||
|
'zxfw-number-cell': () => true,
|
||||||
'ag-right-aligned-cell': () => true
|
'ag-right-aligned-cell': () => true
|
||||||
},
|
},
|
||||||
valueGetter: params => {
|
valueGetter: params => {
|
||||||
@ -666,11 +668,11 @@ const columnDefs: ColDef<DetailRow>[] = [
|
|||||||
{
|
{
|
||||||
headerName: t('htZxFw.columns.subtotal'),
|
headerName: t('htZxFw.columns.subtotal'),
|
||||||
field: 'subtotal',
|
field: 'subtotal',
|
||||||
headerClass: 'ag-right-aligned-header',
|
|
||||||
flex: 2,
|
flex: 2,
|
||||||
minWidth: 100,
|
minWidth: 100,
|
||||||
editable: false,
|
editable: false,
|
||||||
cellClassRules: {
|
cellClassRules: {
|
||||||
|
'zxfw-number-cell': () => true,
|
||||||
'ag-right-aligned-cell': () => true
|
'ag-right-aligned-cell': () => true
|
||||||
},
|
},
|
||||||
valueGetter: params => {
|
valueGetter: params => {
|
||||||
@ -682,12 +684,12 @@ const columnDefs: ColDef<DetailRow>[] = [
|
|||||||
{
|
{
|
||||||
headerName: t('htZxFw.columns.finalFee'),
|
headerName: t('htZxFw.columns.finalFee'),
|
||||||
field: 'finalFee',
|
field: 'finalFee',
|
||||||
headerClass: 'ag-right-aligned-header',
|
|
||||||
headerTooltip: t('htZxFw.columns.finalFeeTooltip'),
|
headerTooltip: t('htZxFw.columns.finalFeeTooltip'),
|
||||||
flex: 2,
|
flex: 2,
|
||||||
minWidth: 110,
|
minWidth: 110,
|
||||||
editable: params => !isFixedRow(params.data),
|
editable: params => !isFixedRow(params.data),
|
||||||
cellClassRules: {
|
cellClassRules: {
|
||||||
|
'zxfw-number-cell': () => true,
|
||||||
'ag-right-aligned-cell': () => true
|
'ag-right-aligned-cell': () => true
|
||||||
},
|
},
|
||||||
valueGetter: params => {
|
valueGetter: params => {
|
||||||
@ -707,6 +709,32 @@ const columnDefs: ColDef<DetailRow>[] = [
|
|||||||
},
|
},
|
||||||
valueFormatter: params => (params.value == null ? '' : formatThousands(params.value, 2))
|
valueFormatter: params => (params.value == null ? '' : formatThousands(params.value, 2))
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
headerName: t('htZxFw.columns.remark'),
|
||||||
|
field: 'remark',
|
||||||
|
minWidth: 160,
|
||||||
|
flex: 1.8,
|
||||||
|
editable: params => !isFixedRow(params.data),
|
||||||
|
cellEditor: 'agLargeTextCellEditor',
|
||||||
|
cellEditorPopup: true,
|
||||||
|
cellEditorParams: {
|
||||||
|
maxLength: 500,
|
||||||
|
rows: 8,
|
||||||
|
cols: 48
|
||||||
|
},
|
||||||
|
wrapText: true,
|
||||||
|
autoHeight: true,
|
||||||
|
cellStyle: { whiteSpace: 'normal', lineHeight: '1.4' },
|
||||||
|
valueGetter: params => {
|
||||||
|
if (!params.data || isFixedRow(params.data)) return ''
|
||||||
|
return String(params.data.remark || '')
|
||||||
|
},
|
||||||
|
valueFormatter: params => (params.value == null ? '' : String(params.value)),
|
||||||
|
cellClass: params => (!isFixedRow(params.data) ? 'remark-wrap-cell' : ''),
|
||||||
|
cellClassRules: {
|
||||||
|
'editable-cell-empty': params => !isFixedRow(params.data) && (params.value == null || String(params.value).trim() === '')
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
headerName: t('htZxFw.columns.actions'),
|
headerName: t('htZxFw.columns.actions'),
|
||||||
field: 'actions',
|
field: 'actions',
|
||||||
@ -931,7 +959,8 @@ const applySelection = async (codes: string[]) => {
|
|||||||
hourly: nextValues.hourly,
|
hourly: nextValues.hourly,
|
||||||
subtotal: typeof old?.subtotal === 'number' ? old.subtotal : null,
|
subtotal: typeof old?.subtotal === 'number' ? old.subtotal : null,
|
||||||
|
|
||||||
finalFee: typeof old?.finalFee === 'number' ? old.finalFee : null
|
finalFee: typeof old?.finalFee === 'number' ? old.finalFee : null,
|
||||||
|
remark: String(old?.remark || '')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.filter((row): row is DetailRow => row !== null)
|
.filter((row): row is DetailRow => row !== null)
|
||||||
@ -951,6 +980,7 @@ const applySelection = async (codes: string[]) => {
|
|||||||
hourly: typeof fixedOld?.hourly === 'number' ? fixedOld.hourly : null,
|
hourly: typeof fixedOld?.hourly === 'number' ? fixedOld.hourly : null,
|
||||||
subtotal: typeof fixedOld?.subtotal === 'number' ? fixedOld.subtotal : null,
|
subtotal: typeof fixedOld?.subtotal === 'number' ? fixedOld.subtotal : null,
|
||||||
finalFee: typeof fixedOld?.finalFee === 'number' ? fixedOld.finalFee : null,
|
finalFee: typeof fixedOld?.finalFee === 'number' ? fixedOld.finalFee : null,
|
||||||
|
remark: '',
|
||||||
actions: null
|
actions: null
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1013,6 +1043,7 @@ const initializeContractState = async () => {
|
|||||||
hourly: null,
|
hourly: null,
|
||||||
subtotal: null,
|
subtotal: null,
|
||||||
finalFee: null,
|
finalFee: null,
|
||||||
|
remark: '',
|
||||||
actions: null
|
actions: null
|
||||||
}])
|
}])
|
||||||
})
|
})
|
||||||
@ -1197,15 +1228,20 @@ const commitFinalFeeGridChanges = async () => {
|
|||||||
|
|
||||||
const handleCellValueChanged = async (event: any) => {
|
const handleCellValueChanged = async (event: any) => {
|
||||||
if (isBulkClipboardMutation) return
|
if (isBulkClipboardMutation) return
|
||||||
if (event.colDef?.field !== 'finalFee') return
|
|
||||||
const row = event.data as DetailRow | undefined
|
const row = event.data as DetailRow | undefined
|
||||||
if (!row || isFixedRow(row)) return
|
if (!row || isFixedRow(row)) return
|
||||||
const newValue = event.newValue != null ? roundTo(Number(event.newValue), 2) : null
|
const field = String(event.colDef?.field || '')
|
||||||
|
if (field !== 'finalFee' && field !== 'remark') return
|
||||||
const currentState = getCurrentContractState()
|
const currentState = getCurrentContractState()
|
||||||
|
|
||||||
const nextRows = currentState.detailRows.map(item =>
|
const nextRows = currentState.detailRows.map(item => {
|
||||||
item.id === row.id ? { ...item, finalFee: newValue } : item
|
if (item.id !== row.id) return item
|
||||||
)
|
if (field === 'remark') {
|
||||||
|
return { ...item, remark: String(event.newValue || '') }
|
||||||
|
}
|
||||||
|
const newValue = event.newValue != null ? roundTo(Number(event.newValue), 2) : null
|
||||||
|
return { ...item, finalFee: newValue }
|
||||||
|
})
|
||||||
const finalRows = applyFixedRowSummary(nextRows)
|
const finalRows = applyFixedRowSummary(nextRows)
|
||||||
await setCurrentContractState({
|
await setCurrentContractState({
|
||||||
...currentState,
|
...currentState,
|
||||||
@ -1256,7 +1292,7 @@ onActivated(async () => {
|
|||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<p class="text-[11px] text-muted-foreground leading-none leading-4 text-amber-700/90">{{ t('htZxFw.warning') }}</p>
|
<p class="text-[11px] leading-none leading-4 !text-[brown]">{{ t('htZxFw.warning') }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div :class="agGridWrapClass">
|
<div :class="agGridWrapClass">
|
||||||
@ -1319,6 +1355,14 @@ onActivated(async () => {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
:deep(.xmMx .ag-header-cell-label) {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.xmMx .ag-header-cell-text) {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
:deep(.zxfw-process-header .ag-header-cell-label) {
|
:deep(.zxfw-process-header .ag-header-cell-label) {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
@ -1332,6 +1376,11 @@ onActivated(async () => {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:deep(.zxfw-number-cell) {
|
||||||
|
justify-content: flex-end;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
:deep(.zxfw-name-cell.ag-cell-auto-height) {
|
:deep(.zxfw-name-cell.ag-cell-auto-height) {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@ -30,6 +30,9 @@ import {
|
|||||||
createScaleBudgetCellRendererToggleFactory,
|
createScaleBudgetCellRendererToggleFactory,
|
||||||
formatScaleEditableConditionalNumber,
|
formatScaleEditableConditionalNumber,
|
||||||
restoreScaleColumnDefaults,
|
restoreScaleColumnDefaults,
|
||||||
|
type ScaleBudgetCheckField,
|
||||||
|
type ScaleBudgetHeaderCheckState,
|
||||||
|
ScaleBudgetToggleHeader,
|
||||||
} from '@/lib/pricingScaleGrid'
|
} from '@/lib/pricingScaleGrid'
|
||||||
import {
|
import {
|
||||||
getCheckedBenchmarkBudgetSplitByRow,
|
getCheckedBenchmarkBudgetSplitByRow,
|
||||||
@ -538,6 +541,32 @@ const createBudgetCellRendererWithCheck = createScaleBudgetCellRendererToggleFac
|
|||||||
() => handleCellValueChanged()
|
() => handleCellValueChanged()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const getBenchmarkBudgetHeaderCheckState = (field: ScaleBudgetCheckField): ScaleBudgetHeaderCheckState => {
|
||||||
|
const targetRows = detailRows.value.filter(row => row.hasCost)
|
||||||
|
if (!targetRows.length) return 'all'
|
||||||
|
const checkedCount = targetRows.reduce((count, row) => (row[field] !== false ? count + 1 : count), 0)
|
||||||
|
if (checkedCount === 0) return 'none'
|
||||||
|
if (checkedCount === targetRows.length) return 'all'
|
||||||
|
return 'partial'
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleBenchmarkBudgetColumnChecked = (field: ScaleBudgetCheckField, checked: boolean) => {
|
||||||
|
const targetRows = detailRows.value.filter(row => row.hasCost)
|
||||||
|
if (!targetRows.length) return
|
||||||
|
for (const row of targetRows) {
|
||||||
|
row[field] = checked
|
||||||
|
}
|
||||||
|
gridApi.value?.refreshHeader()
|
||||||
|
gridApi.value?.refreshCells({ force: true })
|
||||||
|
handleCellValueChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
const getBenchmarkBudgetHeaderParams = (field: ScaleBudgetCheckField) => ({
|
||||||
|
field,
|
||||||
|
getHeaderCheckState: getBenchmarkBudgetHeaderCheckState,
|
||||||
|
onToggleAll: toggleBenchmarkBudgetColumnChecked
|
||||||
|
})
|
||||||
|
|
||||||
const getBenchmarkBudgetSplitByAmount = (row?: Pick<DetailRow, 'amount'>) =>
|
const getBenchmarkBudgetSplitByAmount = (row?: Pick<DetailRow, 'amount'>) =>
|
||||||
getCheckedBenchmarkBudgetSplitByRow(
|
getCheckedBenchmarkBudgetSplitByRow(
|
||||||
{
|
{
|
||||||
@ -661,7 +690,9 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
|
|||||||
}),
|
}),
|
||||||
createScaleBenchmarkBudgetColumnGroup<DetailRow>({
|
createScaleBenchmarkBudgetColumnGroup<DetailRow>({
|
||||||
getCheckedSplit: getCheckedBenchmarkBudgetSplitByAmount,
|
getCheckedSplit: getCheckedBenchmarkBudgetSplitByAmount,
|
||||||
createBudgetCellRendererWithCheck
|
createBudgetCellRendererWithCheck,
|
||||||
|
getHeaderComponent: () => ScaleBudgetToggleHeader,
|
||||||
|
getHeaderComponentParams: getBenchmarkBudgetHeaderParams
|
||||||
}),
|
}),
|
||||||
createScaleBudgetFeeColumnGroup<DetailRow>({
|
createScaleBudgetFeeColumnGroup<DetailRow>({
|
||||||
headerComponent: AgGridResetHeader,
|
headerComponent: AgGridResetHeader,
|
||||||
@ -877,7 +908,13 @@ const buildEmptyRows = (targetProjectCount: number) =>
|
|||||||
: buildDefaultRows(targetProjectCount)
|
: buildDefaultRows(targetProjectCount)
|
||||||
|
|
||||||
const applyDetailRows = (rows: DetailRow[]) => {
|
const applyDetailRows = (rows: DetailRow[]) => {
|
||||||
detailRows.value = rows
|
detailRows.value = rows.map(row => ({
|
||||||
|
...row,
|
||||||
|
benchmarkBudgetBasicChecked:
|
||||||
|
typeof row.benchmarkBudgetBasicChecked === 'boolean' ? row.benchmarkBudgetBasicChecked : true,
|
||||||
|
benchmarkBudgetOptionalChecked:
|
||||||
|
typeof row.benchmarkBudgetOptionalChecked === 'boolean' ? row.benchmarkBudgetOptionalChecked : true
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
const relabelDetailRowsFromDict = async () => {
|
const relabelDetailRowsFromDict = async () => {
|
||||||
@ -1039,9 +1076,11 @@ let isBulkClipboardMutation = false
|
|||||||
const commitGridChanges = async (source: string) => {
|
const commitGridChanges = async (source: string) => {
|
||||||
await nextTick()
|
await nextTick()
|
||||||
syncComputedValuesToDetailRows()
|
syncComputedValuesToDetailRows()
|
||||||
|
gridApi.value?.refreshHeader()
|
||||||
gridApi.value?.refreshCells({ force: true })
|
gridApi.value?.refreshCells({ force: true })
|
||||||
await saveToIndexedDB({ skipComputedSync: true })
|
await saveToIndexedDB({ skipComputedSync: true })
|
||||||
await nextTick()
|
await nextTick()
|
||||||
|
gridApi.value?.refreshHeader()
|
||||||
gridApi.value?.refreshCells({ force: true })
|
gridApi.value?.refreshCells({ force: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -30,6 +30,9 @@ import {
|
|||||||
createScaleBudgetCellRendererToggleFactory,
|
createScaleBudgetCellRendererToggleFactory,
|
||||||
formatScaleEditableConditionalNumber,
|
formatScaleEditableConditionalNumber,
|
||||||
restoreScaleColumnDefaults,
|
restoreScaleColumnDefaults,
|
||||||
|
type ScaleBudgetCheckField,
|
||||||
|
type ScaleBudgetHeaderCheckState,
|
||||||
|
ScaleBudgetToggleHeader,
|
||||||
} from '@/lib/pricingScaleGrid'
|
} from '@/lib/pricingScaleGrid'
|
||||||
import {
|
import {
|
||||||
getCheckedBenchmarkBudgetSplitByRow,
|
getCheckedBenchmarkBudgetSplitByRow,
|
||||||
@ -410,6 +413,32 @@ const createBudgetCellRendererWithCheck = createScaleBudgetCellRendererToggleFac
|
|||||||
() => handleCellValueChanged()
|
() => handleCellValueChanged()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const getBenchmarkBudgetHeaderCheckState = (field: ScaleBudgetCheckField): ScaleBudgetHeaderCheckState => {
|
||||||
|
const targetRows = detailRows.value.filter(row => row.hasArea)
|
||||||
|
if (!targetRows.length) return 'all'
|
||||||
|
const checkedCount = targetRows.reduce((count, row) => (row[field] !== false ? count + 1 : count), 0)
|
||||||
|
if (checkedCount === 0) return 'none'
|
||||||
|
if (checkedCount === targetRows.length) return 'all'
|
||||||
|
return 'partial'
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleBenchmarkBudgetColumnChecked = (field: ScaleBudgetCheckField, checked: boolean) => {
|
||||||
|
const targetRows = detailRows.value.filter(row => row.hasArea)
|
||||||
|
if (!targetRows.length) return
|
||||||
|
for (const row of targetRows) {
|
||||||
|
row[field] = checked
|
||||||
|
}
|
||||||
|
gridApi.value?.refreshHeader()
|
||||||
|
gridApi.value?.refreshCells({ force: true })
|
||||||
|
handleCellValueChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
const getBenchmarkBudgetHeaderParams = (field: ScaleBudgetCheckField) => ({
|
||||||
|
field,
|
||||||
|
getHeaderCheckState: getBenchmarkBudgetHeaderCheckState,
|
||||||
|
onToggleAll: toggleBenchmarkBudgetColumnChecked
|
||||||
|
})
|
||||||
|
|
||||||
const getBenchmarkBudgetSplitByLandArea = (row?: Pick<DetailRow, 'landArea'>) =>
|
const getBenchmarkBudgetSplitByLandArea = (row?: Pick<DetailRow, 'landArea'>) =>
|
||||||
getCheckedBenchmarkBudgetSplitByRow(
|
getCheckedBenchmarkBudgetSplitByRow(
|
||||||
{
|
{
|
||||||
@ -538,7 +567,9 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
|
|||||||
}),
|
}),
|
||||||
createScaleBenchmarkBudgetColumnGroup<DetailRow>({
|
createScaleBenchmarkBudgetColumnGroup<DetailRow>({
|
||||||
getCheckedSplit: getCheckedBenchmarkBudgetSplitByLandArea,
|
getCheckedSplit: getCheckedBenchmarkBudgetSplitByLandArea,
|
||||||
createBudgetCellRendererWithCheck
|
createBudgetCellRendererWithCheck,
|
||||||
|
getHeaderComponent: () => ScaleBudgetToggleHeader,
|
||||||
|
getHeaderComponentParams: getBenchmarkBudgetHeaderParams
|
||||||
}),
|
}),
|
||||||
createScaleBudgetFeeColumnGroup<DetailRow>({
|
createScaleBudgetFeeColumnGroup<DetailRow>({
|
||||||
headerComponent: AgGridResetHeader,
|
headerComponent: AgGridResetHeader,
|
||||||
@ -737,7 +768,13 @@ const buildRowsFromStoredState = (rows: DetailRow[]) =>
|
|||||||
const buildEmptyRows = (targetProjectCount: number) => buildDefaultRows(targetProjectCount)
|
const buildEmptyRows = (targetProjectCount: number) => buildDefaultRows(targetProjectCount)
|
||||||
|
|
||||||
const applyDetailRows = (rows: DetailRow[]) => {
|
const applyDetailRows = (rows: DetailRow[]) => {
|
||||||
detailRows.value = rows
|
detailRows.value = rows.map(row => ({
|
||||||
|
...row,
|
||||||
|
benchmarkBudgetBasicChecked:
|
||||||
|
typeof row.benchmarkBudgetBasicChecked === 'boolean' ? row.benchmarkBudgetBasicChecked : true,
|
||||||
|
benchmarkBudgetOptionalChecked:
|
||||||
|
typeof row.benchmarkBudgetOptionalChecked === 'boolean' ? row.benchmarkBudgetOptionalChecked : true
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
const relabelDetailRowsFromDict = async () => {
|
const relabelDetailRowsFromDict = async () => {
|
||||||
@ -897,9 +934,11 @@ let isBulkClipboardMutation = false
|
|||||||
const commitGridChanges = async (source: string) => {
|
const commitGridChanges = async (source: string) => {
|
||||||
await nextTick()
|
await nextTick()
|
||||||
syncComputedValuesToDetailRows()
|
syncComputedValuesToDetailRows()
|
||||||
|
gridApi.value?.refreshHeader()
|
||||||
gridApi.value?.refreshCells({ force: true })
|
gridApi.value?.refreshCells({ force: true })
|
||||||
await saveToIndexedDB({ skipComputedSync: true })
|
await saveToIndexedDB({ skipComputedSync: true })
|
||||||
await nextTick()
|
await nextTick()
|
||||||
|
gridApi.value?.refreshHeader()
|
||||||
gridApi.value?.refreshCells({ force: true })
|
gridApi.value?.refreshCells({ force: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -341,6 +341,7 @@ const handleCheckedToggle = (id: string, checked: boolean) => {
|
|||||||
if (!target || isAddTriggerRow(target)) return
|
if (!target || isAddTriggerRow(target)) return
|
||||||
target.checked = checked
|
target.checked = checked
|
||||||
gridApi.value?.refreshCells({ force: true })
|
gridApi.value?.refreshCells({ force: true })
|
||||||
|
gridApi.value?.redrawRows()
|
||||||
saveToStore()
|
saveToStore()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -167,6 +167,8 @@ const columnDefs: ColDef<FactorRow>[] = [
|
|||||||
{
|
{
|
||||||
headerName: t('xmFactorGrid.columns.standardFactor'),
|
headerName: t('xmFactorGrid.columns.standardFactor'),
|
||||||
field: 'standardFactor',
|
field: 'standardFactor',
|
||||||
|
type: 'numericColumn',
|
||||||
|
cellClass: 'ag-right-aligned-cell',
|
||||||
minWidth: 86,
|
minWidth: 86,
|
||||||
maxWidth: 100,
|
maxWidth: 100,
|
||||||
headerClass: 'ag-right-aligned-header',
|
headerClass: 'ag-right-aligned-header',
|
||||||
@ -493,3 +495,4 @@ onBeforeUnmount(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@ -328,6 +328,7 @@ export const enUS = {
|
|||||||
subtotal: 'Subtotal',
|
subtotal: 'Subtotal',
|
||||||
finalFee: 'Final Fee ✎',
|
finalFee: 'Final Fee ✎',
|
||||||
finalFeeTooltip: 'This column supports manual edits and will auto-sync to the fixed subtotal row.',
|
finalFeeTooltip: 'This column supports manual edits and will auto-sync to the fixed subtotal row.',
|
||||||
|
remark: 'Remark',
|
||||||
actions: 'Actions'
|
actions: 'Actions'
|
||||||
},
|
},
|
||||||
dialog: {
|
dialog: {
|
||||||
|
|||||||
@ -328,6 +328,7 @@ export const zhCN = {
|
|||||||
subtotal: '小计',
|
subtotal: '小计',
|
||||||
finalFee: '确认金额 ✎',
|
finalFee: '确认金额 ✎',
|
||||||
finalFeeTooltip: '该列支持手动修改,修改后会自动汇总到固定小计行',
|
finalFeeTooltip: '该列支持手动修改,修改后会自动汇总到固定小计行',
|
||||||
|
remark: '备注',
|
||||||
actions: '操作'
|
actions: '操作'
|
||||||
},
|
},
|
||||||
dialog: {
|
dialog: {
|
||||||
|
|||||||
@ -1048,6 +1048,28 @@ const createRichTextCode = (...parts: string[]): unknown => ({
|
|||||||
.map(text => ({ text }))
|
.map(text => ({ text }))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const resolveMethodEnabled = (value: unknown, fallback: boolean) =>
|
||||||
|
typeof value === 'boolean' ? value : fallback
|
||||||
|
|
||||||
|
const getServiceMethodAvailability = (serviceIdText: string) => {
|
||||||
|
const dict = getServiceDictItemById(serviceIdText) as {
|
||||||
|
scale?: boolean | null
|
||||||
|
onlyCostScale?: boolean | null
|
||||||
|
amount?: boolean | null
|
||||||
|
workDay?: boolean | null
|
||||||
|
} | null | undefined
|
||||||
|
const scale = resolveMethodEnabled(dict?.scale, true)
|
||||||
|
const onlyCostScale = resolveMethodEnabled(dict?.onlyCostScale, false)
|
||||||
|
const amount = resolveMethodEnabled(dict?.amount, true)
|
||||||
|
const workDay = resolveMethodEnabled(dict?.workDay, true)
|
||||||
|
return {
|
||||||
|
investmentScale: scale,
|
||||||
|
landScale: scale && !onlyCostScale,
|
||||||
|
workload: amount,
|
||||||
|
hourly: workDay
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const normalizeHtFeeMainRows = (rows: HtFeeMainRowLike[] | undefined) => {
|
const normalizeHtFeeMainRows = (rows: HtFeeMainRowLike[] | undefined) => {
|
||||||
if (!Array.isArray(rows)) return [] as Array<{ id: string; name: string; subtotal?: unknown }>
|
if (!Array.isArray(rows)) return [] as Array<{ id: string; name: string; subtotal?: unknown }>
|
||||||
const normalized = rows.map(row => {
|
const normalized = rows.map(row => {
|
||||||
@ -1148,11 +1170,20 @@ const buildReserveExport = async (contractId: string): Promise<ExportReserve | n
|
|||||||
const rows = normalizeHtFeeMainRows(mainState?.detailRows)
|
const rows = normalizeHtFeeMainRows(mainState?.detailRows)
|
||||||
if (rows.length === 0) return null
|
if (rows.length === 0) return null
|
||||||
|
|
||||||
|
const reserveCodeTemplate = getReserveListEntries(locale.value)?.[0]?.code
|
||||||
|
?? {
|
||||||
|
richText: [
|
||||||
|
{ font: { charset: 134, color: { theme: 1 }, italic: true, name: '宋体', size: 10 }, text: 'Y' },
|
||||||
|
{ font: { charset: 134, color: { theme: 1 }, italic: true, name: 'Calibri', size: 10, vertAlign: 'subscript' }, text: 'B' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
const methodPayload = await loadHtFeeMethodsByRow(storageKey, row.id)
|
const methodPayload = await loadHtFeeMethodsByRow(storageKey, row.id)
|
||||||
const rowSubtotal = getHtMainRowSubtotal(row)
|
const rowSubtotal = getHtMainRowSubtotal(row)
|
||||||
if (!methodPayload && rowSubtotal == null) continue
|
if (!methodPayload && rowSubtotal == null) continue
|
||||||
const reserve: ExportReserve = {
|
const reserve: ExportReserve = {
|
||||||
|
code: reserveCodeTemplate,
|
||||||
name: row.name || t('htSummary.reservePrefix'),
|
name: row.name || t('htSummary.reservePrefix'),
|
||||||
fee: toMoney(rowSubtotal ?? methodPayload?.fee ?? 0),
|
fee: toMoney(rowSubtotal ?? methodPayload?.fee ?? 0),
|
||||||
tasks: []
|
tasks: []
|
||||||
@ -1261,26 +1292,87 @@ const buildExportReportPayload = async (): Promise<ExportReportPayload> => {
|
|||||||
const serviceId = toSafeInteger(serviceIdText)
|
const serviceId = toSafeInteger(serviceIdText)
|
||||||
if (serviceId == null) return null
|
if (serviceId == null) return null
|
||||||
const sourceRow = serviceRowMap.get(serviceIdText)
|
const sourceRow = serviceRowMap.get(serviceIdText)
|
||||||
|
const methodAvailability = getServiceMethodAvailability(serviceIdText)
|
||||||
|
|
||||||
const [method1State, method2State, method3State, method4State] = await Promise.all([
|
const [method1State, method2State, method3State, method4State] = await Promise.all([
|
||||||
zxFwPricingStore.loadServicePricingMethodState<ScaleMethodRowLike>(contractId, serviceIdText, 'investScale'),
|
methodAvailability.investmentScale
|
||||||
zxFwPricingStore.loadServicePricingMethodState<ScaleMethodRowLike>(contractId, serviceIdText, 'landScale'),
|
? zxFwPricingStore.loadServicePricingMethodState<ScaleMethodRowLike>(contractId, serviceIdText, 'investScale')
|
||||||
zxFwPricingStore.loadServicePricingMethodState<WorkloadMethodRowLike>(contractId, serviceIdText, 'workload'),
|
: Promise.resolve(null),
|
||||||
zxFwPricingStore.loadServicePricingMethodState<HourlyMethodRowLike>(contractId, serviceIdText, 'hourly')
|
methodAvailability.landScale
|
||||||
|
? zxFwPricingStore.loadServicePricingMethodState<ScaleMethodRowLike>(contractId, serviceIdText, 'landScale')
|
||||||
|
: Promise.resolve(null),
|
||||||
|
methodAvailability.workload
|
||||||
|
? zxFwPricingStore.loadServicePricingMethodState<WorkloadMethodRowLike>(contractId, serviceIdText, 'workload')
|
||||||
|
: Promise.resolve(null),
|
||||||
|
methodAvailability.hourly
|
||||||
|
? zxFwPricingStore.loadServicePricingMethodState<HourlyMethodRowLike>(contractId, serviceIdText, 'hourly')
|
||||||
|
: Promise.resolve(null)
|
||||||
])
|
])
|
||||||
|
|
||||||
const method1Raw = method1State ? { detailRows: method1State.detailRows } : null
|
const method1Raw = method1State ? { detailRows: method1State.detailRows } : null
|
||||||
const method2Raw = method2State ? { detailRows: method2State.detailRows } : null
|
const method2Raw = method2State ? { detailRows: method2State.detailRows } : null
|
||||||
const method3Raw = method3State ? { detailRows: method3State.detailRows } : null
|
const method3Raw = method3State ? { detailRows: method3State.detailRows } : null
|
||||||
const method4Raw = method4State ? { detailRows: method4State.detailRows } : null
|
const method4Raw = method4State ? { detailRows: method4State.detailRows } : null
|
||||||
const method1 = buildMethod1(method1Raw?.detailRows)
|
const method1 = methodAvailability.investmentScale ? buildMethod1(method1Raw?.detailRows) : null
|
||||||
const method2 = buildMethod2(method2Raw?.detailRows)
|
const method2 = methodAvailability.landScale ? buildMethod2(method2Raw?.detailRows) : null
|
||||||
const method3 = buildMethod3(method3Raw?.detailRows)
|
const method3 = methodAvailability.workload ? buildMethod3(method3Raw?.detailRows) : null
|
||||||
const method4 = buildMethod4(method4Raw?.detailRows)
|
const method4 = methodAvailability.hourly ? buildMethod4(method4Raw?.detailRows) : null
|
||||||
const fee = buildServiceFee(sourceRow, method1, method2, method3, method4)
|
const sanitizedSourceRow = sourceRow
|
||||||
const finalFee = buildServiceFinalFee(sourceRow, method1, method2, method3, method4)
|
? {
|
||||||
|
...sourceRow,
|
||||||
|
investScale: methodAvailability.investmentScale ? sourceRow.investScale : null,
|
||||||
|
landScale: methodAvailability.landScale ? sourceRow.landScale : null,
|
||||||
|
workload: methodAvailability.workload ? sourceRow.workload : null,
|
||||||
|
hourly: methodAvailability.hourly ? sourceRow.hourly : null,
|
||||||
|
subtotal: sumNumbers([
|
||||||
|
methodAvailability.investmentScale ? toFiniteNumber(sourceRow.investScale) : 0,
|
||||||
|
methodAvailability.landScale ? toFiniteNumber(sourceRow.landScale) : 0,
|
||||||
|
methodAvailability.workload ? toFiniteNumber(sourceRow.workload) : 0,
|
||||||
|
methodAvailability.hourly ? toFiniteNumber(sourceRow.hourly) : 0
|
||||||
|
])
|
||||||
|
}
|
||||||
|
: sourceRow
|
||||||
|
const fee = buildServiceFee(sanitizedSourceRow, method1, method2, method3, method4)
|
||||||
|
const finalFee = buildServiceFinalFee(sanitizedSourceRow, method1, method2, method3, method4)
|
||||||
const tasks = await buildServiceTasks(contractId, serviceIdText)
|
const tasks = await buildServiceTasks(contractId, serviceIdText)
|
||||||
const process = Number(sourceRow?.process) === 1 ? 1 : 0
|
const process = Number(sourceRow?.process) === 1 ? 1 : 0
|
||||||
|
|
||||||
|
const disabledMethodLeak = {
|
||||||
|
investScale: !methodAvailability.investmentScale && (toFiniteNumber(sourceRow?.investScale) != null || Boolean(method1Raw)),
|
||||||
|
landScale: !methodAvailability.landScale && (toFiniteNumber(sourceRow?.landScale) != null || Boolean(method2Raw)),
|
||||||
|
workload: !methodAvailability.workload && (toFiniteNumber(sourceRow?.workload) != null || Boolean(method3Raw)),
|
||||||
|
hourly: !methodAvailability.hourly && (toFiniteNumber(sourceRow?.hourly) != null || Boolean(method4Raw))
|
||||||
|
}
|
||||||
|
if (disabledMethodLeak.investScale || disabledMethodLeak.landScale || disabledMethodLeak.workload || disabledMethodLeak.hourly) {
|
||||||
|
console.warn('[export][method-disabled-leak-detected]', {
|
||||||
|
contractId,
|
||||||
|
serviceId: serviceIdText,
|
||||||
|
methodAvailability,
|
||||||
|
sourceRow,
|
||||||
|
hasMethodPayload: {
|
||||||
|
method1: Boolean(method1Raw),
|
||||||
|
method2: Boolean(method2Raw),
|
||||||
|
method3: Boolean(method3Raw),
|
||||||
|
method4: Boolean(method4Raw)
|
||||||
|
},
|
||||||
|
disabledMethodLeak
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[export][service-methods]', {
|
||||||
|
contractId,
|
||||||
|
serviceId: serviceIdText,
|
||||||
|
methodAvailability,
|
||||||
|
exported: {
|
||||||
|
method1: Boolean(method1),
|
||||||
|
method2: Boolean(method2),
|
||||||
|
method3: Boolean(method3),
|
||||||
|
method4: Boolean(method4)
|
||||||
|
},
|
||||||
|
fee,
|
||||||
|
finalFee
|
||||||
|
})
|
||||||
|
|
||||||
const service: ExportService = {
|
const service: ExportService = {
|
||||||
id: serviceId,
|
id: serviceId,
|
||||||
process,
|
process,
|
||||||
|
|||||||
@ -2,6 +2,107 @@ import type { ColDef, ColGroupDef } from 'ag-grid-community'
|
|||||||
|
|
||||||
type AnyColDef<TRow> = ColDef<TRow> | ColGroupDef<TRow>
|
type AnyColDef<TRow> = ColDef<TRow> | ColGroupDef<TRow>
|
||||||
|
|
||||||
|
const numericFieldKeywords = [
|
||||||
|
'amount',
|
||||||
|
'area',
|
||||||
|
'cost',
|
||||||
|
'price',
|
||||||
|
'fee',
|
||||||
|
'budget',
|
||||||
|
'subtotal',
|
||||||
|
'total',
|
||||||
|
'ratio',
|
||||||
|
'rate',
|
||||||
|
'quantity',
|
||||||
|
'count',
|
||||||
|
'num',
|
||||||
|
'workday',
|
||||||
|
'workload',
|
||||||
|
'hourly',
|
||||||
|
'scale',
|
||||||
|
'value',
|
||||||
|
'coe',
|
||||||
|
'factor'
|
||||||
|
]
|
||||||
|
|
||||||
|
const isFiniteNumber = (value: unknown) => typeof value === 'number' && Number.isFinite(value)
|
||||||
|
|
||||||
|
const isNumericLikeString = (value: unknown) => {
|
||||||
|
if (typeof value !== 'string') return false
|
||||||
|
const normalized = value.replace(/[,\s]/g, '').replace(/%$/, '')
|
||||||
|
if (!normalized) return false
|
||||||
|
const asNumber = Number(normalized)
|
||||||
|
return Number.isFinite(asNumber)
|
||||||
|
}
|
||||||
|
|
||||||
|
const looksNumericByColumnDef = <TRow>(col: ColDef<TRow>) => {
|
||||||
|
const fieldLike = String(col.field ?? col.colId ?? '').toLowerCase()
|
||||||
|
const hasNumericFieldHint = fieldLike
|
||||||
|
? numericFieldKeywords.some(keyword => fieldLike.includes(keyword))
|
||||||
|
: false
|
||||||
|
const type = col.type
|
||||||
|
const typeList = Array.isArray(type) ? type : type ? [type] : []
|
||||||
|
const hasNumericType = typeList.some(item => {
|
||||||
|
const normalized = String(item).toLowerCase()
|
||||||
|
return normalized.includes('numeric') || normalized.includes('rightaligned')
|
||||||
|
})
|
||||||
|
return hasNumericFieldHint || col.cellDataType === 'number' || hasNumericType
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasRightAlignedHeaderClass = (value: unknown) => {
|
||||||
|
if (typeof value === 'string') return value.includes('ag-right-aligned-header')
|
||||||
|
if (Array.isArray(value)) return value.some(item => String(item).includes('ag-right-aligned-header'))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const mergeHeaderClass = <TRow>(col: ColDef<TRow>, mustRightAlign: boolean): ColDef<TRow>['headerClass'] => {
|
||||||
|
if (!mustRightAlign) return col.headerClass
|
||||||
|
|
||||||
|
const base = col.headerClass
|
||||||
|
if (!base) return 'ag-right-aligned-header'
|
||||||
|
if (hasRightAlignedHeaderClass(base)) return base
|
||||||
|
|
||||||
|
if (typeof base === 'function') {
|
||||||
|
return params => {
|
||||||
|
const result = base(params)
|
||||||
|
if (!result) return 'ag-right-aligned-header'
|
||||||
|
if (typeof result === 'string') {
|
||||||
|
return result.includes('ag-right-aligned-header')
|
||||||
|
? result
|
||||||
|
: `${result} ag-right-aligned-header`
|
||||||
|
}
|
||||||
|
if (Array.isArray(result)) {
|
||||||
|
return hasRightAlignedHeaderClass(result)
|
||||||
|
? result
|
||||||
|
: [...result, 'ag-right-aligned-header']
|
||||||
|
}
|
||||||
|
return 'ag-right-aligned-header'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof base === 'string') return `${base} ag-right-aligned-header`
|
||||||
|
if (Array.isArray(base)) return [...base, 'ag-right-aligned-header']
|
||||||
|
return base
|
||||||
|
}
|
||||||
|
|
||||||
|
const mergeNumericCellClassRules = <TRow>(
|
||||||
|
col: ColDef<TRow>,
|
||||||
|
mustRightAlign: boolean
|
||||||
|
): ColDef<TRow>['cellClassRules'] => {
|
||||||
|
if (!mustRightAlign) return col.cellClassRules
|
||||||
|
|
||||||
|
const baseRules = col.cellClassRules ? { ...col.cellClassRules } : {}
|
||||||
|
const existing = baseRules['ag-right-aligned-cell']
|
||||||
|
if (existing) return baseRules
|
||||||
|
|
||||||
|
baseRules['ag-right-aligned-cell'] = params =>
|
||||||
|
looksNumericByColumnDef(params.colDef as ColDef<TRow>) ||
|
||||||
|
isFiniteNumber(params.value) ||
|
||||||
|
isNumericLikeString(params.value)
|
||||||
|
|
||||||
|
return baseRules
|
||||||
|
}
|
||||||
|
|
||||||
const mergeCellStyle = (cellStyle: ColDef['cellStyle']): ColDef['cellStyle'] => {
|
const mergeCellStyle = (cellStyle: ColDef['cellStyle']): ColDef['cellStyle'] => {
|
||||||
const baseStyle = { whiteSpace: 'normal', lineHeight: '1.4' }
|
const baseStyle = { whiteSpace: 'normal', lineHeight: '1.4' }
|
||||||
if (!cellStyle) return baseStyle
|
if (!cellStyle) return baseStyle
|
||||||
@ -29,9 +130,18 @@ const mergeCellStyle = (cellStyle: ColDef['cellStyle']): ColDef['cellStyle'] =>
|
|||||||
const enhanceLeafColumn = <TRow>(col: ColDef<TRow>): ColDef<TRow> => {
|
const enhanceLeafColumn = <TRow>(col: ColDef<TRow>): ColDef<TRow> => {
|
||||||
const editable = col.editable
|
const editable = col.editable
|
||||||
const isReadonlyColumn = editable == null || editable === false
|
const isReadonlyColumn = editable == null || editable === false
|
||||||
if (!isReadonlyColumn) return { ...col }
|
const shouldRightAlign = looksNumericByColumnDef(col)
|
||||||
|
if (!isReadonlyColumn) {
|
||||||
return {
|
return {
|
||||||
...col,
|
...col,
|
||||||
|
headerClass: mergeHeaderClass(col, shouldRightAlign),
|
||||||
|
cellClassRules: mergeNumericCellClassRules(col, shouldRightAlign)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...col,
|
||||||
|
headerClass: mergeHeaderClass(col, shouldRightAlign),
|
||||||
|
cellClassRules: mergeNumericCellClassRules(col, shouldRightAlign),
|
||||||
wrapText: true,
|
wrapText: true,
|
||||||
autoHeight: true,
|
autoHeight: true,
|
||||||
cellStyle: mergeCellStyle(col.cellStyle)
|
cellStyle: mergeCellStyle(col.cellStyle)
|
||||||
|
|||||||
@ -36,6 +36,41 @@ export const agGridWrapClass = 'ag-theme-quartz h-full min-h-0 w-full flex-1'
|
|||||||
// AG Grid 组件通用 style(撑满容器 div)
|
// AG Grid 组件通用 style(撑满容器 div)
|
||||||
export const agGridStyle = { height: '100%' }
|
export const agGridStyle = { height: '100%' }
|
||||||
|
|
||||||
|
const numericFieldKeywords = [
|
||||||
|
'amount',
|
||||||
|
'area',
|
||||||
|
'cost',
|
||||||
|
'price',
|
||||||
|
'fee',
|
||||||
|
'budget',
|
||||||
|
'subtotal',
|
||||||
|
'total',
|
||||||
|
'ratio',
|
||||||
|
'rate',
|
||||||
|
'quantity',
|
||||||
|
'count',
|
||||||
|
'num',
|
||||||
|
'workday',
|
||||||
|
'workload',
|
||||||
|
'hourly',
|
||||||
|
'investscale',
|
||||||
|
'landscale',
|
||||||
|
'scale',
|
||||||
|
'finalfee',
|
||||||
|
'value',
|
||||||
|
'coe',
|
||||||
|
'factor'
|
||||||
|
]
|
||||||
|
|
||||||
|
const isLikelyNumericColumn = (params: any) => {
|
||||||
|
const value = params?.value
|
||||||
|
if (typeof value === 'number' && Number.isFinite(value)) return true
|
||||||
|
|
||||||
|
const field = String(params?.colDef?.field || params?.column?.getColId?.() || '').toLowerCase()
|
||||||
|
if (!field) return false
|
||||||
|
return numericFieldKeywords.some(keyword => field.includes(keyword))
|
||||||
|
}
|
||||||
|
|
||||||
const isPlainEnterKey = (event: KeyboardEvent) =>
|
const isPlainEnterKey = (event: KeyboardEvent) =>
|
||||||
event.key === 'Enter' && !event.altKey && !event.ctrlKey && !event.metaKey
|
event.key === 'Enter' && !event.altKey && !event.ctrlKey && !event.metaKey
|
||||||
|
|
||||||
@ -112,7 +147,7 @@ export const agGridDefaultColDef: ColDef = {
|
|||||||
suppressKeyboardEvent: suppressExcelLikeEnter,
|
suppressKeyboardEvent: suppressExcelLikeEnter,
|
||||||
// 默认把数值型单元格右对齐,减少每个列重复配置。
|
// 默认把数值型单元格右对齐,减少每个列重复配置。
|
||||||
cellClassRules: {
|
cellClassRules: {
|
||||||
'ag-right-aligned-cell': params => typeof params.value === 'number' && Number.isFinite(params.value)
|
'ag-right-aligned-cell': params => isLikelyNumericColumn(params)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -52,6 +52,8 @@ export const createScaleValueColumn = <TRow>(options: {
|
|||||||
export const createScaleBenchmarkBudgetColumnGroup = <TRow>(options: {
|
export const createScaleBenchmarkBudgetColumnGroup = <TRow>(options: {
|
||||||
getCheckedSplit: (row: TRow | undefined) => { basic?: number | null; optional?: number | null; total?: number | null } | null
|
getCheckedSplit: (row: TRow | undefined) => { basic?: number | null; optional?: number | null; total?: number | null } | null
|
||||||
createBudgetCellRendererWithCheck: (field: 'benchmarkBudgetBasicChecked' | 'benchmarkBudgetOptionalChecked') => any
|
createBudgetCellRendererWithCheck: (field: 'benchmarkBudgetBasicChecked' | 'benchmarkBudgetOptionalChecked') => any
|
||||||
|
getHeaderComponent?: (field: 'benchmarkBudgetBasicChecked' | 'benchmarkBudgetOptionalChecked') => any
|
||||||
|
getHeaderComponentParams?: (field: 'benchmarkBudgetBasicChecked' | 'benchmarkBudgetOptionalChecked') => Record<string, unknown>
|
||||||
}) : ColGroupDef<TRow> => ({
|
}) : ColGroupDef<TRow> => ({
|
||||||
headerName: scaleT('columns.benchmarkBudget'),
|
headerName: scaleT('columns.benchmarkBudget'),
|
||||||
marryChildren: true,
|
marryChildren: true,
|
||||||
@ -61,6 +63,8 @@ export const createScaleBenchmarkBudgetColumnGroup = <TRow>(options: {
|
|||||||
field: 'benchmarkBudgetBasic' as any,
|
field: 'benchmarkBudgetBasic' as any,
|
||||||
colId: 'benchmarkBudgetBasic',
|
colId: 'benchmarkBudgetBasic',
|
||||||
headerClass: 'ag-right-aligned-header',
|
headerClass: 'ag-right-aligned-header',
|
||||||
|
headerComponent: options.getHeaderComponent?.('benchmarkBudgetBasicChecked'),
|
||||||
|
headerComponentParams: options.getHeaderComponentParams?.('benchmarkBudgetBasicChecked'),
|
||||||
minWidth: 130,
|
minWidth: 130,
|
||||||
flex: 1,
|
flex: 1,
|
||||||
cellClassRules: {
|
cellClassRules: {
|
||||||
@ -78,6 +82,8 @@ export const createScaleBenchmarkBudgetColumnGroup = <TRow>(options: {
|
|||||||
field: 'benchmarkBudgetOptional' as any,
|
field: 'benchmarkBudgetOptional' as any,
|
||||||
colId: 'benchmarkBudgetOptional',
|
colId: 'benchmarkBudgetOptional',
|
||||||
headerClass: 'ag-right-aligned-header',
|
headerClass: 'ag-right-aligned-header',
|
||||||
|
headerComponent: options.getHeaderComponent?.('benchmarkBudgetOptionalChecked'),
|
||||||
|
headerComponentParams: options.getHeaderComponentParams?.('benchmarkBudgetOptionalChecked'),
|
||||||
minWidth: 130,
|
minWidth: 130,
|
||||||
flex: 1,
|
flex: 1,
|
||||||
cellClassRules: {
|
cellClassRules: {
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import type { GridApi } from 'ag-grid-community'
|
|||||||
import { nextTick } from 'vue'
|
import { nextTick } from 'vue'
|
||||||
|
|
||||||
export type ScaleBudgetCheckField = 'benchmarkBudgetBasicChecked' | 'benchmarkBudgetOptionalChecked'
|
export type ScaleBudgetCheckField = 'benchmarkBudgetBasicChecked' | 'benchmarkBudgetOptionalChecked'
|
||||||
|
export type ScaleBudgetHeaderCheckState = 'all' | 'none' | 'partial'
|
||||||
|
|
||||||
type BudgetCheckRow = {
|
type BudgetCheckRow = {
|
||||||
id: string
|
id: string
|
||||||
@ -13,6 +14,71 @@ type BudgetCheckRow = {
|
|||||||
benchmarkBudgetOptional?: 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 = '请输入') => {
|
export const formatScaleEditableNumber = (params: any, precision = 3, emptyText = '请输入') => {
|
||||||
if (!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')) {
|
if (!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')) {
|
||||||
return emptyText
|
return emptyText
|
||||||
|
|||||||
@ -22,6 +22,7 @@ export interface ZxFwDetailRow {
|
|||||||
id: string
|
id: string
|
||||||
code?: string
|
code?: string
|
||||||
name?: string
|
name?: string
|
||||||
|
remark?: string
|
||||||
process?: number | null
|
process?: number | null
|
||||||
investScale: number | null
|
investScale: number | null
|
||||||
landScale: number | null
|
landScale: number | null
|
||||||
@ -126,6 +127,7 @@ const normalizeRows = (rows: unknown): ZxFwDetailRow[] =>
|
|||||||
id: rowId,
|
id: rowId,
|
||||||
code: typeof row.code === 'string' ? row.code : '',
|
code: typeof row.code === 'string' ? row.code : '',
|
||||||
name: typeof row.name === 'string' ? row.name : '',
|
name: typeof row.name === 'string' ? row.name : '',
|
||||||
|
remark: typeof row.remark === 'string' ? row.remark : '',
|
||||||
process: normalizeProcessValue(row.process, rowId),
|
process: normalizeProcessValue(row.process, rowId),
|
||||||
investScale: toFiniteNumberOrNull(row.investScale),
|
investScale: toFiniteNumberOrNull(row.investScale),
|
||||||
landScale: toFiniteNumberOrNull(row.landScale),
|
landScale: toFiniteNumberOrNull(row.landScale),
|
||||||
@ -214,6 +216,7 @@ const isSameRows = (a: ZxFwDetailRow[] | undefined, b: ZxFwDetailRow[] | undefin
|
|||||||
if (l.id !== r.id) return false
|
if (l.id !== r.id) return false
|
||||||
if ((l.code || '') !== (r.code || '')) return false
|
if ((l.code || '') !== (r.code || '')) return false
|
||||||
if ((l.name || '') !== (r.name || '')) return false
|
if ((l.name || '') !== (r.name || '')) return false
|
||||||
|
if ((l.remark || '') !== (r.remark || '')) return false
|
||||||
if (normalizeProcessValue(l.process, l.id) !== normalizeProcessValue(r.process, r.id)) return false
|
if (normalizeProcessValue(l.process, l.id) !== normalizeProcessValue(r.process, r.id)) return false
|
||||||
if (!isSameNullableNumber(l.investScale, r.investScale)) return false
|
if (!isSameNullableNumber(l.investScale, r.investScale)) return false
|
||||||
if (!isSameNullableNumber(l.landScale, r.landScale)) return false
|
if (!isSameNullableNumber(l.landScale, r.landScale)) return false
|
||||||
|
|||||||
@ -175,10 +175,23 @@ input[inputmode='numeric'] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.ag-theme-quartz .ag-cell.ag-right-aligned-cell .ag-cell-value {
|
.ag-theme-quartz .ag-cell.ag-right-aligned-cell .ag-cell-value {
|
||||||
|
display: flex;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
justify-content: flex-end;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Global AG Grid header alignment: center all header text. */
|
||||||
|
.ag-theme-quartz .ag-header-cell-label,
|
||||||
|
.ag-theme-quartz .ag-header-group-cell-label {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ag-theme-quartz .ag-header-cell-text,
|
||||||
|
.ag-theme-quartz .ag-header-group-text {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
.app-toolbar-btn {
|
.app-toolbar-btn {
|
||||||
height: var(--app-toolbar-btn-h) !important;
|
height: var(--app-toolbar-btn-h) !important;
|
||||||
min-height: var(--app-toolbar-btn-h) !important;
|
min-height: var(--app-toolbar-btn-h) !important;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user