From fc26a87bee2de16000b84e65a98f48b55db241b2 Mon Sep 17 00:00:00 2001 From: wintsa <770775984@qq.com> Date: Sat, 7 Mar 2026 17:52:33 +0800 Subject: [PATCH] =?UTF-8?q?=E7=B3=BB=E6=95=B0=E5=AD=97=E6=AE=B5=E4=BF=AE?= =?UTF-8?q?=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/基础参数及报表导出功能.js | 366 +++++++++++++++++- src/components/common/HtFeeGrid.vue | 12 +- src/components/common/XmFactorGrid.vue | 10 +- src/components/common/xmCommonAgGrid.vue | 10 +- .../views/pricingView/HourlyPricingPane.vue | 12 +- .../InvestmentScalePricingPane.vue | 157 ++++++-- .../pricingView/LandScalePricingPane.vue | 97 +++-- .../views/pricingView/WorkloadPricingPane.vue | 15 +- src/components/views/zxFw.vue | 43 +- src/lib/pricingMethodTotals.ts | 154 +++++++- src/main.ts | 8 +- src/sql.ts | 3 +- 12 files changed, 782 insertions(+), 105 deletions(-) diff --git a/public/基础参数及报表导出功能.js b/public/基础参数及报表导出功能.js index ebb8d6e..74e04b2 100644 --- a/public/基础参数及报表导出功能.js +++ b/public/基础参数及报表导出功能.js @@ -1843,4 +1843,368 @@ function numberFormatter(num, decimalNum) { } else { return big; } -} \ No newline at end of file +} + + + +let data1 = { + name: 'test001', + writer: '张三',// 编制人 + reviewer: '李四',// 复核人 + company: '测试公司',// 公司名称 + date: '2021-09-24',// 编制日期 + industry: 0,// 0为公路工程,1为铁路工程,2为水运工程 + fee: 10000, + scaleCost: 100000,// scale的cost的合计数 + scale: [// 规模信息 + { + major: 0, + cost: 100000, + area: 200, + }, + { + major: 1, + cost: 100000, + area: 200, + }, + ], + serviceCoes: [// 项目咨询分类系数 + { + serviceid: 0, + coe: 1.1, + remark: '',// 用户输入的说明 + }, + { + serviceid: 1, + coe: 1.2, + remark: '',// 用户输入的说明 + }, + ], + majorCoes: [// 项目工程专业系数 + { + majorid: 0, + coe: 1.1, + remark: '',// 用户输入的说明 + }, + { + majorid: 1, + coe: 1.2, + remark: '',// 用户输入的说明 + }, + ], + contracts: [// 合同段信息 + { + name: 'A合同段', + serviceFee: 100000, + addtionalFee: 0, + reserveFee: 0, + fee: 10000, + scale: [ + { + major: 0, + cost: 100000, + area: 200, + }, + { + major: 1, + cost: 100000, + area: 200, + }, + ], + serviceCoes: [// 合同段咨询分类系数 + { + serviceid: 0, + coe: 1.1, + remark: '',// 用户输入的说明 + }, + { + serviceid: 1, + coe: 1.2, + remark: '',// 用户输入的说明 + }, + ], + majorCoes: [// 合同段工程专业系数 + { + majorid: 0, + coe: 1.1, + remark: '',// 用户输入的说明 + }, + { + majorid: 1, + coe: 1.2, + remark: '',// 用户输入的说明 + }, + ], + services: [ + { + id: 0, + fee: 100000, + process: 0,// 工作环节,0为编制,1为审核 + method1: { // 投资规模法 + cost: 100000, + basicFee: 200, + basicFee_basic: 200, + basicFee_optional: 0, + fee: 250000, + proAmount: 3, + det: [ + { + proNum: 1, + major: 0, + cost: 100000, + basicFee: 200, + basicFormula: '856,000+(1,000,000,000-500,000,000)×1‰', + basicFee_basic: 200, + optionalFormula: '171,200+(1,000,000,000-500,000,000)×0.2‰', + basicFee_optional: 0, + serviceCoe: 1.1, + majorCoe: 1.2, + processCoe: 1,// 工作环节系数(编审系数) + proportion: 0.5,// 工作占比 + fee: 100000, + remark: '',// 用户输入的说明 + }, + ], + }, + method2: { // 用地规模法 + area: 1200, + basicFee: 200, + basicFee_basic: 200, + basicFee_optional: 0, + fee: 250000, + proAmount: 3, + det: [ + { + proNum: 1, + major: 0, + area: 1200, + basicFee: 200, + basicFormula: '106,000+(1,200-1,000)×60', + basicFee_basic: 200, + optionalFormula: '21,200+(1,200-1,000)×12', + basicFee_optional: 0, + serviceCoe: 1.1, + majorCoe: 1.2, + processCoe: 1,// 工作环节系数(编审系数) + proportion: 0.5,// 工作占比 + fee: 100000, + remark: '',// 用户输入的说明 + }, + ], + }, + method3: { // 工作量法 + basicFee: 200, + fee: 250000, + det: [ + { + task: 0, + price: 100000, + amount: 10, + basicFee: 200, + serviceCoe: 1.1, + fee: 100000, + remark: '',// 用户输入的说明 + }, + { + task: 1, + price: 100000, + amount: 10, + basicFee: 200, + serviceCoe: 1.1, + fee: 100000, + remark: '',// 用户输入的说明 + }, + ], + }, + method4: { // 工时法 + person_num: 10, + work_day: 10, + fee: 250000, + det: [ + { + expert: 0, + price: 100000, + person_num: 10, + work_day: 3, + fee: 100000, + remark: '',// 用户输入的说明 + }, + { + expert: 1, + price: 100000, + person_num: 10, + work_day: 3, + fee: 100000, + remark: '',// 用户输入的说明 + }, + ], + }, + }, + ], + addtional: {// 附加工作费 + ref: { richText: [{ font: { charset: 134, color: { theme: 1 }, italic: true, name: '宋体', size: 10 }, text: 'C' }, { font: { charset: 134, color: { theme: 1 }, italic: true, name: 'Calibri', size: 10, vertAlign: 'subscript' }, text: 'C' }] }, + name: '附加工作', + fee: 10000, + det: [ + { + id: 0, + ref: { richText: [{ font: { charset: 134, color: { theme: 1 }, italic: true, name: '宋体', size: 10 }, text: 'C' }, { font: { charset: 134, color: { theme: 1 }, italic: true, name: 'Calibri', size: 10, vertAlign: 'subscript' }, text: 'F' }] }, + name: '人员驻场服务及其他附加工作', + fee: 10000, + m4: { + person_num: 10, + work_day: 3, + fee: 10000, + det: [ + { + expert: 0, + price: 100000, + person_num: 10, + work_day: 3, + fee: 100000, + remark: '',// 用户输入的说明 + }, + { + expert: 1, + price: 100000, + person_num: 10, + work_day: 3, + fee: 100000, + remark: '',// 用户输入的说明 + }, + ], + }, + m5: { + fee: 10000, + det: [ + { + name: '×××项', + unit: '项', + amount: 10, + price: 100000, + fee: 100000, + remark: '',// 用户输入的说明 + }, + { + name: '×××项', + unit: '项', + amount: 10, + price: 100000, + fee: 100000, + remark: '',// 用户输入的说明 + }, + ], + }, + }, + { + id: 1, + ref: { richText: [{ font: { charset: 134, color: { theme: 1 }, italic: true, name: '宋体', size: 10 }, text: 'C' }, { font: { charset: 134, color: { theme: 1 }, italic: true, name: 'Calibri', size: 10, vertAlign: 'subscript' }, text: 'X' }] }, + name: '咨询服务协调工作', + fee: 10000, + m0: { + coe: 0.03, + fee: 10000, + }, + m4: { + person_num: 10, + work_day: 3, + fee: 10000, + det: [ + { + expert: 0, + price: 100000, + person_num: 10, + work_day: 3, + fee: 100000, + remark: '',// 用户输入的说明 + }, + { + expert: 1, + price: 100000, + person_num: 10, + work_day: 3, + fee: 100000, + remark: '',// 用户输入的说明 + }, + ], + }, + m5: { + fee: 10000, + det: [ + { + name: '×××项', + unit: '项', + amount: 10, + price: 100000, + fee: 100000, + remark: '',// 用户输入的说明 + }, + { + name: '×××项', + unit: '项', + amount: 10, + price: 100000, + fee: 100000, + remark: '',// 用户输入的说明 + }, + ], + }, + }, + ] + }, + reserve: {// 预备费 + ref: { 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' }] }, + name: '预备费', + fee: 10000, + m0: { + coe: 0.03, + fee: 10000, + }, + m4: { + person_num: 10, + work_day: 3, + fee: 10000, + det: [ + { + expert: 0, + price: 100000, + person_num: 10, + work_day: 3, + fee: 100000, + remark: '',// 用户输入的说明 + }, + { + expert: 1, + price: 100000, + person_num: 10, + work_day: 3, + fee: 100000, + remark: '',// 用户输入的说明 + }, + ], + }, + m5: { + fee: 10000, + det: [ + { + name: '×××项', + unit: '项', + amount: 10, + price: 100000, + fee: 100000, + remark: '',// 用户输入的说明 + }, + { + name: '×××项', + unit: '项', + amount: 10, + price: 100000, + fee: 100000, + remark: '',// 用户输入的说明 + }, + ], + } + }, + }, + ], +}; diff --git a/src/components/common/HtFeeGrid.vue b/src/components/common/HtFeeGrid.vue index 76a3606..9b2b6f7 100644 --- a/src/components/common/HtFeeGrid.vue +++ b/src/components/common/HtFeeGrid.vue @@ -6,7 +6,7 @@ import { AG_GRID_LOCALE_CN } from '@ag-grid-community/locale' import localforage from 'localforage' import { myTheme, gridOptions } from '@/lib/diyAgGridOptions' import { parseNumberOrNull } from '@/lib/number' -import { formatThousands, formatThousandsFlexible } from '@/lib/numberFormat' +import { formatThousandsFlexible } from '@/lib/numberFormat' import { roundTo, toDecimal } from '@/lib/decimal' interface FeeRow { @@ -50,17 +50,17 @@ const formatEditableText = (params: any) => { const formatEditableQuantity = (params: any) => { if (params.value == null || params.value === '') return '点击输入' - return formatThousandsFlexible(params.value, 4) + return formatThousandsFlexible(params.value, 3) } const formatEditableUnitPrice = (params: any) => { if (params.value == null || params.value === '') return '点击输入' - return formatThousands(params.value, 2) + return formatThousandsFlexible(params.value, 3) } const formatReadonlyBudgetFee = (params: any) => { if (params.value == null || params.value === '') return '' - return formatThousands(params.value, 2) + return formatThousandsFlexible(params.value, 3) } const syncComputedValuesToRows = () => { @@ -172,7 +172,7 @@ const columnDefs: ColDef[] = [ headerClass: 'ag-right-aligned-header', cellClass: 'ag-right-aligned-cell editable-cell-line', editable: true, - valueParser: params => parseNumberOrNull(params.newValue, { precision: 4 }), + valueParser: params => parseNumberOrNull(params.newValue, { precision: 3 }), valueFormatter: formatEditableQuantity, cellClassRules: { 'editable-cell-empty': params => params.value == null || params.value === '' @@ -186,7 +186,7 @@ const columnDefs: ColDef[] = [ headerClass: 'ag-right-aligned-header', cellClass: 'ag-right-aligned-cell editable-cell-line', editable: true, - valueParser: params => parseNumberOrNull(params.newValue, { precision: 2 }), + valueParser: params => parseNumberOrNull(params.newValue, { precision: 3 }), valueFormatter: formatEditableUnitPrice, cellClassRules: { 'editable-cell-empty': params => params.value == null || params.value === '' diff --git a/src/components/common/XmFactorGrid.vue b/src/components/common/XmFactorGrid.vue index 525b97b..f3b08fa 100644 --- a/src/components/common/XmFactorGrid.vue +++ b/src/components/common/XmFactorGrid.vue @@ -46,12 +46,16 @@ const gridApi = ref | null>(null) const formatReadonlyFactor = (value: unknown) => { if (value == null || value === '') return '' - return Number(value).toFixed(2) + const parsed = parseNumberOrNull(value, { precision: 3 }) + if (parsed == null) return '' + return String(Number(parsed)) } const formatEditableFactor = (params: any) => { if (params.value == null || params.value === '') return '点击输入' - return Number(params.value).toFixed(2) + const parsed = parseNumberOrNull(params.value, { precision: 3 }) + if (parsed == null) return '' + return String(Number(parsed)) } const sortedDictEntries = () => @@ -169,7 +173,7 @@ const columnDefs: ColDef[] = [ if (!props.disableBudgetEditWhenStandardNull) return true return params.data?.standardFactor != null }, - valueParser: params => parseNumberOrNull(params.newValue, { precision: 2 }), + valueParser: params => parseNumberOrNull(params.newValue, { precision: 3 }), valueFormatter: params => { const disabled = props.disableBudgetEditWhenStandardNull && params.data?.standardFactor == null if (disabled && (params.value == null || params.value === '')) return '' diff --git a/src/components/common/xmCommonAgGrid.vue b/src/components/common/xmCommonAgGrid.vue index 8f1ffc4..b41c369 100644 --- a/src/components/common/xmCommonAgGrid.vue +++ b/src/components/common/xmCommonAgGrid.vue @@ -6,7 +6,7 @@ import { AG_GRID_LOCALE_CN } from '@ag-grid-community/locale' import { myTheme, gridOptions } from '@/lib/diyAgGridOptions' import localforage from 'localforage' import { decimalAggSum, roundTo, sumByNumber } from '@/lib/decimal' -import { formatThousands } from '@/lib/numberFormat' +import { formatThousandsFlexible } from '@/lib/numberFormat' import { industryTypeList, getMajorDictEntries, isMajorIdInIndustryScope } from '@/sql' import { SwitchRoot, SwitchThumb } from 'reka-ui' @@ -295,13 +295,13 @@ const columnDefs: ColDef[] = [ valueParser: params => { if (params.newValue === '' || params.newValue == null) return null const v = Number(params.newValue) - return Number.isFinite(v) ? roundTo(v, 2) : null + return Number.isFinite(v) ? roundTo(v, 3) : null }, valueFormatter: params => { if (roughCalcEnabled.value) { if (!params.node?.rowPinned) return '' if (params.value == null || params.value === '') return '点击输入' - return formatThousands(params.value) + return formatThousandsFlexible(params.value, 3) } if (!params.node?.group && !params.node?.rowPinned && !params.data?.hasCost) { return '' @@ -310,7 +310,7 @@ const columnDefs: ColDef[] = [ return '点击输入' } if (params.value == null) return '' - return formatThousands(params.value) + return formatThousandsFlexible(params.value, 3) } }, { @@ -344,7 +344,7 @@ const columnDefs: ColDef[] = [ return '点击输入' } if (params.value == null) return '' - return formatThousands(params.value, 3) + return formatThousandsFlexible(params.value, 3) } } ] diff --git a/src/components/views/pricingView/HourlyPricingPane.vue b/src/components/views/pricingView/HourlyPricingPane.vue index dc2a47e..3235330 100644 --- a/src/components/views/pricingView/HourlyPricingPane.vue +++ b/src/components/views/pricingView/HourlyPricingPane.vue @@ -6,7 +6,7 @@ import localforage from 'localforage' import { expertList } from '@/sql' import { myTheme, gridOptions } from '@/lib/diyAgGridOptions' import { decimalAggSum, roundTo, sumByNumber, toDecimal } from '@/lib/decimal' -import { formatThousands } from '@/lib/numberFormat' +import { formatThousandsFlexible } from '@/lib/numberFormat' import { parseNumberOrNull } from '@/lib/number' import { syncPricingTotalToZxFw, ZXFW_RELOAD_SERVICE_KEY } from '@/lib/zxFwPricingSync' import { usePricingPaneReloadStore } from '@/pinia/pricingPaneReload' @@ -190,7 +190,7 @@ const formatEditableNumber = (params: any) => { return '点击输入' } if (params.value == null) return '' - return Number(params.value).toFixed(2) + return formatThousandsFlexible(params.value, 3) } const formatEditableInteger = (params: any) => { @@ -233,7 +233,7 @@ const editableNumberCol = ( 'editable-cell-empty': params => !params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '') }, - valueParser: params => parseNumberOrNull(params.newValue, { precision: 2 }), + valueParser: params => parseNumberOrNull(params.newValue, { precision: 3 }), valueFormatter: formatEditableNumber, ...extra }) @@ -254,13 +254,13 @@ const editableMoneyCol = ( 'editable-cell-empty': params => !params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '') }, - valueParser: params => parseNumberOrNull(params.newValue, { precision: 2 }), + valueParser: params => parseNumberOrNull(params.newValue, { precision: 3 }), valueFormatter: params => { if (!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')) { return '点击输入' } if (params.value == null) return '' - return formatThousands(params.value) + return formatThousandsFlexible(params.value, 3) }, ...extra }) @@ -325,7 +325,7 @@ const columnDefs: (ColDef | ColGroupDef)[] = [ valueGetter: params => (params.node?.rowPinned ? params.data?.serviceBudget ?? null : calcServiceBudget(params.data)), valueFormatter: params => { if (params.value == null || params.value === '') return '' - return formatThousands(params.value) + return formatThousandsFlexible(params.value, 3) } }, { diff --git a/src/components/views/pricingView/InvestmentScalePricingPane.vue b/src/components/views/pricingView/InvestmentScalePricingPane.vue index 0202479..1f9ebdb 100644 --- a/src/components/views/pricingView/InvestmentScalePricingPane.vue +++ b/src/components/views/pricingView/InvestmentScalePricingPane.vue @@ -5,8 +5,8 @@ import type { ColDef, ColGroupDef } from 'ag-grid-community' import localforage from 'localforage' import { getMajorDictEntries, getServiceDictItemById, industryTypeList, isMajorIdInIndustryScope } from '@/sql' import { myTheme, gridOptions } from '@/lib/diyAgGridOptions' -import { decimalAggSum, roundTo, sumByNumber } from '@/lib/decimal' -import { formatThousands } from '@/lib/numberFormat' +import { addNumbers, decimalAggSum, roundTo, sumByNumber } from '@/lib/decimal' +import { formatThousandsFlexible } from '@/lib/numberFormat' import { syncPricingTotalToZxFw, ZXFW_RELOAD_SERVICE_KEY } from '@/lib/zxFwPricingSync' import { usePricingPaneReloadStore } from '@/pinia/pricingPaneReload' import { loadConsultCategoryFactorMap, loadMajorFactorMap } from '@/lib/xmFactorDefaults' @@ -212,8 +212,9 @@ const detailDict: DictGroup[] = (() => { const hasCost = item.hasCost !== false const hasArea = item.hasArea !== false - // 特殊规则:投资规模法中,hasCost && hasArea 的专业不参与明细行 - if (hasCost && hasArea) continue + // 投资规模法仅保留可按造价计价且非用地规模的专业 + if (!hasCost) continue + if (hasArea) continue groupMap.get(parentCode)!.children.push({ id: key, @@ -292,7 +293,18 @@ const getOnlyCostScaleMajorFactorDefault = () => { const buildOnlyCostScaleRow = ( amount: number | null, - fromDb?: Partial> + fromDb?: Partial< + Pick< + DetailRow, + | 'consultCategoryFactor' + | 'majorFactor' + | 'workStageFactor' + | 'workRatio' + | 'remark' + | 'benchmarkBudgetBasicChecked' + | 'benchmarkBudgetOptionalChecked' + > + > ): DetailRow => ({ id: ONLY_COST_SCALE_ROW_ID, groupCode: 'TOTAL', @@ -305,8 +317,10 @@ const buildOnlyCostScaleRow = ( benchmarkBudget: null, benchmarkBudgetBasic: null, benchmarkBudgetOptional: null, - benchmarkBudgetBasicChecked: true, - benchmarkBudgetOptionalChecked: true, + benchmarkBudgetBasicChecked: + typeof fromDb?.benchmarkBudgetBasicChecked === 'boolean' ? fromDb.benchmarkBudgetBasicChecked : true, + benchmarkBudgetOptionalChecked: + typeof fromDb?.benchmarkBudgetOptionalChecked === 'boolean' ? fromDb.benchmarkBudgetOptionalChecked : true, basicFormula: '', optionalFormula: '', consultCategoryFactor: @@ -416,7 +430,7 @@ const formatEditableNumber = (params: any) => { return '请输入' } if (params.value == null) return '' - return Number(params.value).toFixed(2) + return formatThousandsFlexible(params.value, 3) } const formatConsultCategoryFactor = (params: any) => { @@ -431,7 +445,7 @@ const formatEditableMoney = (params: any) => { if (isOnlyCostScaleService.value) { if (!params.node?.rowPinned) return '' if (params.value == null || params.value === '') return '点击输入' - return formatThousands(params.value) + return formatThousandsFlexible(params.value, 3) } if (!params.node?.group && !params.node?.rowPinned && !params.data?.hasCost) { return '' @@ -440,12 +454,12 @@ const formatEditableMoney = (params: any) => { return '点击输入' } if (params.value == null) return '' - return formatThousands(params.value) + return formatThousandsFlexible(params.value, 3) } const formatReadonlyMoney = (params: any) => { if (params.value == null || params.value === '') return '' - return formatThousands(roundTo(params.value, 2)) + return formatThousandsFlexible(roundTo(params.value, 3), 3) } type BudgetCheckField = 'benchmarkBudgetBasicChecked' | 'benchmarkBudgetOptionalChecked' @@ -453,14 +467,14 @@ type BudgetCheckField = 'benchmarkBudgetBasicChecked' | 'benchmarkBudgetOptional const createBudgetCellRendererWithCheck = (checkField: BudgetCheckField) => (params: any) => { const valueText = formatReadonlyMoney(params) const hasValue = params.value != null && params.value !== '' - if (params.node?.group || params.node?.rowPinned || !params.data || !hasValue) { + if (params.node?.group || (params.node?.rowPinned && !isOnlyCostScaleService.value) || !params.data || !hasValue) { return valueText } const wrapper = document.createElement('div') wrapper.style.display = 'flex' wrapper.style.alignItems = 'center' - wrapper.style.justifyContent = 'flex-end' + wrapper.style.justifyContent = 'space-between' wrapper.style.gap = '6px' wrapper.style.width = '100%' @@ -471,8 +485,27 @@ const createBudgetCellRendererWithCheck = (checkField: BudgetCheckField) => (par checkbox.checked = params.data[checkField] !== false checkbox.addEventListener('click', event => event.stopPropagation()) checkbox.addEventListener('change', () => { - params.data[checkField] = checkbox.checked + const isOnlyCostScalePinned = isOnlyCostScaleService.value && Boolean(params.node?.rowPinned) + const targetRow = + isOnlyCostScalePinned + ? detailRows.value[0] + : (params.data as DetailRow | undefined) + if (!targetRow) return + + targetRow[checkField] = checkbox.checked + params.node?.setDataValue?.(checkField, checkbox.checked) + + if (!checkbox.checked) { + const budgetField = checkField === 'benchmarkBudgetBasicChecked' ? 'benchmarkBudgetBasic' : 'benchmarkBudgetOptional' + targetRow[budgetField] = 0 + params.node?.setDataValue?.(budgetField, 0) + } + handleCellValueChanged() + params.api?.refreshCells?.({ + rowNodes: params.node ? [params.node] : undefined, + force: true + }) }) const valueSpan = document.createElement('span') @@ -485,10 +518,34 @@ const createBudgetCellRendererWithCheck = (checkField: BudgetCheckField) => (par const getBenchmarkBudgetSplitByAmount = (row?: Pick) => getBenchmarkBudgetSplitByScale(row?.amount, 'cost') -const getBudgetFee = ( - row?: Pick +const getCheckedBenchmarkBudgetSplitByAmount = ( + row?: Pick ) => { - const benchmarkBudgetSplit = getBenchmarkBudgetSplitByAmount(row) + const split = getBenchmarkBudgetSplitByAmount(row) + if (!split) return null + const basic = row?.benchmarkBudgetBasicChecked === false ? 0 : split.basic + const optional = row?.benchmarkBudgetOptionalChecked === false ? 0 : split.optional + return { + ...split, + basic, + optional, + total: roundTo(addNumbers(basic, optional), 2) + } +} + +const getBudgetFee = ( + row?: Pick< + DetailRow, + | 'amount' + | 'benchmarkBudgetBasicChecked' + | 'benchmarkBudgetOptionalChecked' + | 'majorFactor' + | 'consultCategoryFactor' + | 'workStageFactor' + | 'workRatio' + > +) => { + const benchmarkBudgetSplit = getCheckedBenchmarkBudgetSplitByAmount(row) if (!benchmarkBudgetSplit) return null const splitBudgetFee = getScaleBudgetFeeSplit({ @@ -503,9 +560,18 @@ const getBudgetFee = ( } const getBudgetFeeSplit = ( - row?: Pick + row?: Pick< + DetailRow, + | 'amount' + | 'benchmarkBudgetBasicChecked' + | 'benchmarkBudgetOptionalChecked' + | 'majorFactor' + | 'consultCategoryFactor' + | 'workStageFactor' + | 'workRatio' + > ) => { - const benchmarkBudgetSplit = getBenchmarkBudgetSplitByAmount(row) + const benchmarkBudgetSplit = getCheckedBenchmarkBudgetSplitByAmount(row) if (!benchmarkBudgetSplit) return null return getScaleBudgetFeeSplit({ benchmarkBudgetBasic: benchmarkBudgetSplit.basic, @@ -517,6 +583,17 @@ const getBudgetFeeSplit = ( }) } +const getMergeColSpanBeforeTotal = (params: any) => { + if (!params.node?.group && !params.node?.rowPinned) return 1 + if (isOnlyCostScaleService.value && 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 +} + const columnDefs: Array | ColGroupDef> = [ { headerName: '造价金额(万元)', @@ -541,7 +618,7 @@ const columnDefs: Array | ColGroupDef> = [ ? params.value == null || params.value === '' : !params.node?.group && !params.node?.rowPinned && Boolean(params.data?.hasCost) && (params.value == null || params.value === '') }, - valueParser: params => parseNumberOrNull(params.newValue, { precision: 2 }), + valueParser: params => parseNumberOrNull(params.newValue, { precision: 3 }), valueFormatter: formatEditableMoney }, { @@ -558,8 +635,8 @@ const columnDefs: Array | ColGroupDef> = [ cellClass: 'ag-right-aligned-cell', valueGetter: params => params.node?.rowPinned - ? null - : getBenchmarkBudgetSplitByAmount(params.data)?.basic ?? null, + ? (isOnlyCostScaleService.value ? getCheckedBenchmarkBudgetSplitByAmount(params.data)?.basic ?? null : null) + : getCheckedBenchmarkBudgetSplitByAmount(params.data)?.basic ?? null, cellRenderer: createBudgetCellRendererWithCheck('benchmarkBudgetBasicChecked'), valueFormatter: formatReadonlyMoney }, @@ -573,8 +650,8 @@ const columnDefs: Array | ColGroupDef> = [ cellClass: 'ag-right-aligned-cell', valueGetter: params => params.node?.rowPinned - ? null - : getBenchmarkBudgetSplitByAmount(params.data)?.optional ?? null, + ? (isOnlyCostScaleService.value ? getCheckedBenchmarkBudgetSplitByAmount(params.data)?.optional ?? null : null) + : getCheckedBenchmarkBudgetSplitByAmount(params.data)?.optional ?? null, cellRenderer: createBudgetCellRendererWithCheck('benchmarkBudgetOptionalChecked'), valueFormatter: formatReadonlyMoney }, @@ -588,8 +665,8 @@ const columnDefs: Array | ColGroupDef> = [ cellClass: 'ag-right-aligned-cell', valueGetter: params => params.node?.rowPinned - ? null - : getBenchmarkBudgetSplitByAmount(params.data)?.total ?? null, + ? (isOnlyCostScaleService.value ? getCheckedBenchmarkBudgetSplitByAmount(params.data)?.total ?? null : null) + : getCheckedBenchmarkBudgetSplitByAmount(params.data)?.total ?? null, valueFormatter: formatReadonlyMoney } ] @@ -620,7 +697,7 @@ const columnDefs: Array | ColGroupDef> = [ ? params.value == null || params.value === '' : !params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '') }, - valueParser: params => parseNumberOrNull(params.newValue, { precision: 2 }), + valueParser: params => parseNumberOrNull(params.newValue, { precision: 3 }), valueFormatter: formatConsultCategoryFactor }, { @@ -645,7 +722,7 @@ const columnDefs: Array | ColGroupDef> = [ ? params.value == null || params.value === '' : !params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '') }, - valueParser: params => parseNumberOrNull(params.newValue, { precision: 2 }), + valueParser: params => parseNumberOrNull(params.newValue, { precision: 3 }), valueFormatter: formatMajorFactor }, { @@ -670,7 +747,7 @@ const columnDefs: Array | ColGroupDef> = [ ? params.value == null || params.value === '' : !params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '') }, - valueParser: params => parseNumberOrNull(params.newValue, { precision: 2 }), + valueParser: params => parseNumberOrNull(params.newValue, { precision: 3 }), valueFormatter: formatEditableNumber }, { @@ -695,7 +772,7 @@ const columnDefs: Array | ColGroupDef> = [ ? params.value == null || params.value === '' : !params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '') }, - valueParser: params => parseNumberOrNull(params.newValue, { precision: 2 }), + valueParser: params => parseNumberOrNull(params.newValue, { precision: 3 }), valueFormatter: formatEditableNumber }, { @@ -738,8 +815,8 @@ const columnDefs: Array | ColGroupDef> = [ const autoGroupColumnDef: ColDef = { headerName: '专业编码以及工程专业名称', minWidth: 250, - pinned: 'left', flex: 2, + // wrapText: true, // cellStyle: { whiteSpace: 'normal', lineHeight: '1.5', padding: '2px' }, @@ -747,6 +824,7 @@ const autoGroupColumnDef: ColDef = { cellRendererParams: { suppressCount: true }, + colSpan: getMergeColSpanBeforeTotal, valueFormatter: params => { if (params.node?.rowPinned) { return totalLabel.value @@ -766,9 +844,7 @@ const totalAmount = computed(() => sumByNumber(detailRows.value, row => row.amou const visibleDetailRows = computed(() => (isOnlyCostScaleService.value ? [] : detailRows.value)) const onlyCostScaleSourceRow = computed(() => detailRows.value[0] ?? buildOnlyCostScaleRow(null)) -const totalBenchmarkBudgetBasic = computed(() => sumByNumber(detailRows.value, row => getBenchmarkBudgetSplitByAmount(row)?.basic)) -const totalBenchmarkBudgetOptional = computed(() => sumByNumber(detailRows.value, row => getBenchmarkBudgetSplitByAmount(row)?.optional)) -const totalBenchmarkBudget = computed(() => sumByNumber(detailRows.value, row => getBenchmarkBudgetSplitByAmount(row)?.total)) + const totalBudgetFeeBasic = computed(() => sumByNumber(detailRows.value, row => getBudgetFeeSplit(row)?.basic)) const totalBudgetFeeOptional = computed(() => sumByNumber(detailRows.value, row => getBudgetFeeSplit(row)?.optional)) @@ -786,8 +862,8 @@ const pinnedTopRowData = computed(() => [ benchmarkBudget: null, benchmarkBudgetBasic: null, benchmarkBudgetOptional: null, - benchmarkBudgetBasicChecked: true, - benchmarkBudgetOptionalChecked: true, + benchmarkBudgetBasicChecked: isOnlyCostScaleService.value ? onlyCostScaleSourceRow.value.benchmarkBudgetBasicChecked !== false : true, + benchmarkBudgetOptionalChecked: isOnlyCostScaleService.value ? onlyCostScaleSourceRow.value.benchmarkBudgetOptionalChecked !== false : true, basicFormula: '', optionalFormula: '', consultCategoryFactor: isOnlyCostScaleService.value ? onlyCostScaleSourceRow.value.consultCategoryFactor : null, @@ -804,7 +880,8 @@ const pinnedTopRowData = computed(() => [ const syncComputedValuesToDetailRows = () => { for (const row of detailRows.value) { - const benchmarkBudgetSplit = getBenchmarkBudgetSplitByAmount(row) + const benchmarkBudgetRawSplit = getBenchmarkBudgetSplitByAmount(row) + const benchmarkBudgetSplit = getCheckedBenchmarkBudgetSplitByAmount(row) const budgetFeeSplit = benchmarkBudgetSplit ? getScaleBudgetFeeSplit({ benchmarkBudgetBasic: benchmarkBudgetSplit.basic, @@ -819,8 +896,8 @@ const syncComputedValuesToDetailRows = () => { row.benchmarkBudget = benchmarkBudgetSplit?.total ?? null row.benchmarkBudgetBasic = benchmarkBudgetSplit?.basic ?? null row.benchmarkBudgetOptional = benchmarkBudgetSplit?.optional ?? null - row.basicFormula = benchmarkBudgetSplit?.basicFormula ?? '' - row.optionalFormula = benchmarkBudgetSplit?.optionalFormula ?? '' + row.basicFormula = benchmarkBudgetRawSplit?.basicFormula ?? '' + row.optionalFormula = benchmarkBudgetRawSplit?.optionalFormula ?? '' row.budgetFee = budgetFeeSplit?.total ?? null row.budgetFeeBasic = budgetFeeSplit?.basic ?? null row.budgetFeeOptional = budgetFeeSplit?.optional ?? null @@ -951,7 +1028,7 @@ let persistTimer: ReturnType | null = null let gridPersistTimer: ReturnType | null = null const applyOnlyCostScalePinnedValue = (field: string, rawValue: unknown) => { - const parsedValue = parseNumberOrNull(rawValue, { precision: 2 }) + const parsedValue = parseNumberOrNull(rawValue, { precision: 3 }) const current = detailRows.value[0] if (!current) { detailRows.value = [buildOnlyCostScaleRow(field === 'amount' ? parsedValue : null)] diff --git a/src/components/views/pricingView/LandScalePricingPane.vue b/src/components/views/pricingView/LandScalePricingPane.vue index a0d15db..3187c94 100644 --- a/src/components/views/pricingView/LandScalePricingPane.vue +++ b/src/components/views/pricingView/LandScalePricingPane.vue @@ -5,8 +5,8 @@ import type { ColDef, ColGroupDef } from 'ag-grid-community' import localforage from 'localforage' import { getMajorDictEntries, industryTypeList, isMajorIdInIndustryScope } from '@/sql' import { myTheme, gridOptions } from '@/lib/diyAgGridOptions' -import { decimalAggSum, roundTo, sumByNumber } from '@/lib/decimal' -import { formatThousands } from '@/lib/numberFormat' +import { addNumbers, decimalAggSum, roundTo, sumByNumber } from '@/lib/decimal' +import { formatThousandsFlexible } from '@/lib/numberFormat' import { syncPricingTotalToZxFw, ZXFW_RELOAD_SERVICE_KEY } from '@/lib/zxFwPricingSync' import { usePricingPaneReloadStore } from '@/pinia/pricingPaneReload' import { loadConsultCategoryFactorMap, loadMajorFactorMap } from '@/lib/xmFactorDefaults' @@ -195,12 +195,15 @@ const detailDict: DictGroup[] = (() => { }) } + const hasArea = item.hasArea !== false + if (!hasArea) continue + groupMap.get(parentCode)!.children.push({ id: key, code, name: item.name, hasCost: item.hasCost !== false, - hasArea: item.hasArea !== false + hasArea }) } @@ -342,7 +345,7 @@ const formatEditableNumber = (params: any) => { return '请输入' } if (params.value == null) return '' - return Number(params.value).toFixed(2) + return formatThousandsFlexible(params.value, 3) } const formatConsultCategoryFactor = (params: any) => { @@ -355,7 +358,7 @@ const formatMajorFactor = (params: any) => { const formatReadonlyMoney = (params: any) => { if (params.value == null || params.value === '') return '' - return formatThousands(roundTo(params.value, 2)) + return formatThousandsFlexible(roundTo(params.value, 3), 3) } type BudgetCheckField = 'benchmarkBudgetBasicChecked' | 'benchmarkBudgetOptionalChecked' @@ -370,7 +373,7 @@ const createBudgetCellRendererWithCheck = (checkField: BudgetCheckField) => (par const wrapper = document.createElement('div') wrapper.style.display = 'flex' wrapper.style.alignItems = 'center' - wrapper.style.justifyContent = 'flex-end' + wrapper.style.justifyContent = 'space-between' wrapper.style.gap = '6px' wrapper.style.width = '100%' @@ -381,6 +384,10 @@ const createBudgetCellRendererWithCheck = (checkField: BudgetCheckField) => (par checkbox.addEventListener('click', event => event.stopPropagation()) checkbox.addEventListener('change', () => { params.data[checkField] = checkbox.checked + if (!checkbox.checked) { + const budgetField = checkField === 'benchmarkBudgetBasicChecked' ? 'benchmarkBudgetBasic' : 'benchmarkBudgetOptional' + params.data[budgetField] = 0 + } handleCellValueChanged() }) @@ -394,10 +401,34 @@ const createBudgetCellRendererWithCheck = (checkField: BudgetCheckField) => (par const getBenchmarkBudgetSplitByLandArea = (row?: Pick) => getBenchmarkBudgetSplitByScale(row?.landArea, 'area') -const getBudgetFee = ( - row?: Pick +const getCheckedBenchmarkBudgetSplitByLandArea = ( + row?: Pick ) => { - const benchmarkBudgetSplit = getBenchmarkBudgetSplitByLandArea(row) + const split = getBenchmarkBudgetSplitByLandArea(row) + if (!split) return null + const basic = row?.benchmarkBudgetBasicChecked === false ? 0 : split.basic + const optional = row?.benchmarkBudgetOptionalChecked === false ? 0 : split.optional + return { + ...split, + basic, + optional, + total: roundTo(addNumbers(basic, optional), 2) + } +} + +const getBudgetFee = ( + row?: Pick< + DetailRow, + | 'landArea' + | 'benchmarkBudgetBasicChecked' + | 'benchmarkBudgetOptionalChecked' + | 'majorFactor' + | 'consultCategoryFactor' + | 'workStageFactor' + | 'workRatio' + > +) => { + const benchmarkBudgetSplit = getCheckedBenchmarkBudgetSplitByLandArea(row) if (!benchmarkBudgetSplit) return null const splitBudgetFee = getScaleBudgetFeeSplit({ @@ -412,9 +443,18 @@ const getBudgetFee = ( } const getBudgetFeeSplit = ( - row?: Pick + row?: Pick< + DetailRow, + | 'landArea' + | 'benchmarkBudgetBasicChecked' + | 'benchmarkBudgetOptionalChecked' + | 'majorFactor' + | 'consultCategoryFactor' + | 'workStageFactor' + | 'workRatio' + > ) => { - const benchmarkBudgetSplit = getBenchmarkBudgetSplitByLandArea(row) + const benchmarkBudgetSplit = getCheckedBenchmarkBudgetSplitByLandArea(row) if (!benchmarkBudgetSplit) return null return getScaleBudgetFeeSplit({ benchmarkBudgetBasic: benchmarkBudgetSplit.basic, @@ -426,6 +466,16 @@ const getBudgetFeeSplit = ( }) } +const getMergeColSpanBeforeTotal = (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 +} + const formatEditableFlexibleNumber = (params: any) => { if (!params.node?.group && !params.node?.rowPinned && !params.data?.hasArea) { return '' @@ -434,7 +484,7 @@ const formatEditableFlexibleNumber = (params: any) => { return '点击输入' } if (params.value == null) return '' - return formatThousands(params.value, 3) + return formatThousandsFlexible(params.value, 3) } const columnDefs: Array | ColGroupDef> = [ @@ -476,7 +526,7 @@ const columnDefs: Array | ColGroupDef> = [ valueGetter: params => params.node?.rowPinned ? null - : getBenchmarkBudgetSplitByLandArea(params.data)?.basic ?? null, + : getCheckedBenchmarkBudgetSplitByLandArea(params.data)?.basic ?? null, cellRenderer: createBudgetCellRendererWithCheck('benchmarkBudgetBasicChecked'), valueFormatter: formatReadonlyMoney }, @@ -491,7 +541,7 @@ const columnDefs: Array | ColGroupDef> = [ valueGetter: params => params.node?.rowPinned ? null - : getBenchmarkBudgetSplitByLandArea(params.data)?.optional ?? null, + : getCheckedBenchmarkBudgetSplitByLandArea(params.data)?.optional ?? null, cellRenderer: createBudgetCellRendererWithCheck('benchmarkBudgetOptionalChecked'), valueFormatter: formatReadonlyMoney }, @@ -506,7 +556,7 @@ const columnDefs: Array | ColGroupDef> = [ valueGetter: params => params.node?.rowPinned ? null - : getBenchmarkBudgetSplitByLandArea(params.data)?.total ?? null, + : getCheckedBenchmarkBudgetSplitByLandArea(params.data)?.total ?? null, valueFormatter: formatReadonlyMoney } ] @@ -527,7 +577,7 @@ const columnDefs: Array | ColGroupDef> = [ 'editable-cell-empty': params => !params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '') }, - valueParser: params => parseNumberOrNull(params.newValue, { precision: 2 }), + valueParser: params => parseNumberOrNull(params.newValue, { precision: 3 }), valueFormatter: formatConsultCategoryFactor }, { @@ -542,7 +592,7 @@ const columnDefs: Array | ColGroupDef> = [ 'editable-cell-empty': params => !params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '') }, - valueParser: params => parseNumberOrNull(params.newValue, { precision: 2 }), + valueParser: params => parseNumberOrNull(params.newValue, { precision: 3 }), valueFormatter: formatMajorFactor }, { @@ -557,7 +607,7 @@ const columnDefs: Array | ColGroupDef> = [ 'editable-cell-empty': params => !params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '') }, - valueParser: params => parseNumberOrNull(params.newValue, { precision: 2 }), + valueParser: params => parseNumberOrNull(params.newValue, { precision: 3 }), valueFormatter: formatEditableNumber }, { @@ -572,7 +622,7 @@ const columnDefs: Array | ColGroupDef> = [ 'editable-cell-empty': params => !params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '') }, - valueParser: params => parseNumberOrNull(params.newValue, { precision: 2 }), + valueParser: params => parseNumberOrNull(params.newValue, { precision: 3 }), valueFormatter: formatEditableNumber }, { @@ -615,12 +665,12 @@ const columnDefs: Array | ColGroupDef> = [ const autoGroupColumnDef: ColDef = { headerName: '专业编码以及工程专业名称', minWidth: 250, - pinned: 'left', flex: 2, cellRendererParams: { suppressCount: true }, + colSpan: getMergeColSpanBeforeTotal, valueFormatter: params => { if (params.node?.rowPinned) { return totalLabel.value @@ -678,7 +728,8 @@ const pinnedTopRowData = computed(() => [ const syncComputedValuesToDetailRows = () => { for (const row of detailRows.value) { - const benchmarkBudgetSplit = getBenchmarkBudgetSplitByLandArea(row) + const benchmarkBudgetRawSplit = getBenchmarkBudgetSplitByLandArea(row) + const benchmarkBudgetSplit = getCheckedBenchmarkBudgetSplitByLandArea(row) const budgetFeeSplit = benchmarkBudgetSplit ? getScaleBudgetFeeSplit({ benchmarkBudgetBasic: benchmarkBudgetSplit.basic, @@ -693,8 +744,8 @@ const syncComputedValuesToDetailRows = () => { row.benchmarkBudget = benchmarkBudgetSplit?.total ?? null row.benchmarkBudgetBasic = benchmarkBudgetSplit?.basic ?? null row.benchmarkBudgetOptional = benchmarkBudgetSplit?.optional ?? null - row.basicFormula = benchmarkBudgetSplit?.basicFormula ?? '' - row.optionalFormula = benchmarkBudgetSplit?.optionalFormula ?? '' + row.basicFormula = benchmarkBudgetRawSplit?.basicFormula ?? '' + row.optionalFormula = benchmarkBudgetRawSplit?.optionalFormula ?? '' row.budgetFee = budgetFeeSplit?.total ?? null row.budgetFeeBasic = budgetFeeSplit?.basic ?? null row.budgetFeeOptional = budgetFeeSplit?.optional ?? null diff --git a/src/components/views/pricingView/WorkloadPricingPane.vue b/src/components/views/pricingView/WorkloadPricingPane.vue index b693a71..2f67442 100644 --- a/src/components/views/pricingView/WorkloadPricingPane.vue +++ b/src/components/views/pricingView/WorkloadPricingPane.vue @@ -6,7 +6,7 @@ import localforage from 'localforage' import { taskList } from '@/sql' import { myTheme, gridOptions } from '@/lib/diyAgGridOptions' import { decimalAggSum, roundTo, sumByNumber, toDecimal } from '@/lib/decimal' -import { formatThousands } from '@/lib/numberFormat' +import { formatThousands, formatThousandsFlexible } from '@/lib/numberFormat' import { parseNumberOrNull } from '@/lib/number' import { syncPricingTotalToZxFw, ZXFW_RELOAD_SERVICE_KEY } from '@/lib/zxFwPricingSync' import { usePricingPaneReloadStore } from '@/pinia/pricingPaneReload' @@ -187,7 +187,10 @@ const mergeWithDictRows = (rowsFromDb: DetailRow[] | undefined): DetailRow[] => } const parseSanitizedNumberOrNull = (value: unknown) => - parseNumberOrNull(value, { sanitize: true, precision: 2 }) + parseNumberOrNull(value, { sanitize: true, precision: 3 }) + +const parseSanitizedAdoptedPriceOrNull = (value: unknown) => + parseNumberOrNull(value, { sanitize: true, precision: 6 }) const calcBasicFee = (row: DetailRow | undefined) => { if (!row || isNoTaskRow(row)) return null @@ -227,7 +230,7 @@ const formatEditableNumber = (params: any) => { return '点击输入' } if (params.value == null) return '' - return Number(params.value).toFixed(2) + return formatThousandsFlexible(params.value, 3) } const spanRowsByTaskName = (params: any) => { @@ -294,7 +297,7 @@ const columnDefs: ColDef[] = [ !isNoTaskRow(params.data) && (params.value == null || params.value === '') }, - valueParser: params => parseSanitizedNumberOrNull(params.newValue), + valueParser: params => parseSanitizedAdoptedPriceOrNull(params.newValue), valueFormatter: params => { if (isNoTaskRow(params.data)) return '无' if (!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')) { @@ -302,7 +305,7 @@ const columnDefs: ColDef[] = [ } if (params.value == null) return '' const unit = params.data?.unit || '' - return `${formatThousands(params.value)}${unit}` + return `${formatThousandsFlexible(params.value, 6)}${unit}` } }, { @@ -354,7 +357,7 @@ const columnDefs: ColDef[] = [ valueFormatter: params => { if (isNoTaskRow(params.data)) return '无' if (params.value == null || params.value === '') return '' - return formatThousands(roundTo(params.value, 2)) + return formatThousandsFlexible(roundTo(params.value, 3), 3) } }, { diff --git a/src/components/views/zxFw.vue b/src/components/views/zxFw.vue index 04aabd9..07747a8 100644 --- a/src/components/views/zxFw.vue +++ b/src/components/views/zxFw.vue @@ -8,8 +8,9 @@ import { myTheme ,gridOptions} from '@/lib/diyAgGridOptions' import { AG_GRID_LOCALE_CN } from '@ag-grid-community/locale' import { addNumbers } from '@/lib/decimal' import { parseNumberOrNull } from '@/lib/number' -import { formatThousands, formatThousandsFlexible } from '@/lib/numberFormat' +import { formatThousandsFlexible } from '@/lib/numberFormat' import { + ensurePricingMethodDetailRowsForServices, getPricingMethodTotalsForService, getPricingMethodTotalsForServices, type PricingMethodTotals @@ -310,7 +311,7 @@ const dragRectStyle = computed(() => { }) const numericParser = (newValue: any): number | null => { - return parseNumberOrNull(newValue, { precision: 2 }) + return parseNumberOrNull(newValue, { precision: 3 }) } const valueOrZero = (v: number | null | undefined) => (typeof v === 'number' ? v : 0) @@ -395,6 +396,11 @@ const clearRowValues = async (row: DetailRow) => { // 若该服务编辑页已打开,先关闭,避免子页面卸载时把旧数据写回缓? tabStore.removeTab(`zxfw-edit-${props.contractId}-${row.id}`) await nextTick() await clearPricingPaneValues(row.id) + await ensurePricingMethodDetailRowsForServices({ + contractId: props.contractId, + serviceIds: [row.id], + options: PRICING_TOTALS_OPTIONS + }) const totals = await getPricingMethodTotalsForService({ contractId: props.contractId, serviceId: row.id, @@ -502,7 +508,7 @@ const columnDefs: ColDef[] = [ return params.data.investScale }, valueParser: params => numericParser(params.newValue), - valueFormatter: params => (params.value == null ? '' : formatThousands(params.value)) + valueFormatter: params => (params.value == null ? '' : formatThousandsFlexible(params.value, 3)) }, { headerName: '用地规模法', @@ -519,7 +525,7 @@ const columnDefs: ColDef[] = [ return params.data.landScale }, valueParser: params => numericParser(params.newValue), - valueFormatter: params => (params.value == null ? '' : formatThousandsFlexible(params.value, 2)) + valueFormatter: params => (params.value == null ? '' : formatThousandsFlexible(params.value, 3)) }, { headerName: '工作量法', @@ -537,7 +543,7 @@ const columnDefs: ColDef[] = [ }, // editable: params => !params.node?.rowPinned && !isFixedRow(params.data), valueParser: params => numericParser(params.newValue), - valueFormatter: params => (params.value == null ? '' : formatThousands(params.value)) + valueFormatter: params => (params.value == null ? '' : formatThousandsFlexible(params.value, 3)) }, { headerName: '工时法', @@ -555,7 +561,7 @@ const columnDefs: ColDef[] = [ }, // editable: params => !params.node?.rowPinned && !isFixedRow(params.data), valueParser: params => numericParser(params.newValue), - valueFormatter: params => (params.value == null ? '' : formatThousands(params.value)) + valueFormatter: params => (params.value == null ? '' : formatThousandsFlexible(params.value, 3)) }, { headerName: '小计', @@ -576,7 +582,7 @@ const columnDefs: ColDef[] = [ valueOrZero(params.data.hourly) ) }, - valueFormatter: params => (params.value == null ? '' : formatThousands(params.value)) + valueFormatter: params => (params.value == null ? '' : formatThousandsFlexible(params.value, 3)) }, { headerName: '操作', @@ -634,6 +640,21 @@ const applyFixedRowTotals = (rows: DetailRow[]) => { ) } +const getSelectedServiceIdsWithoutFixed = () => + detailRows.value + .filter(row => !isFixedRow(row)) + .map(row => String(row.id)) + +const ensurePricingDetailRowsForCurrentSelection = async () => { + const serviceIds = getSelectedServiceIdsWithoutFixed() + if (serviceIds.length === 0) return + await ensurePricingMethodDetailRowsForServices({ + contractId: props.contractId, + serviceIds, + options: PRICING_TOTALS_OPTIONS + }) +} + const fillPricingTotalsForServiceIds = async (serviceIds: string[]) => { const targetIds = Array.from( new Set( @@ -648,6 +669,12 @@ const fillPricingTotalsForServiceIds = async (serviceIds: string[]) => { return } + await ensurePricingMethodDetailRowsForServices({ + contractId: props.contractId, + serviceIds: targetIds, + options: PRICING_TOTALS_OPTIONS + }) + const totalsByServiceId = await getPricingMethodTotalsForServices({ contractId: props.contractId, serviceIds: targetIds, @@ -734,6 +761,7 @@ const handleServiceSelectionChange = async (ids: string[]) => { const nextSelectedSet = new Set(selectedIds.value) const addedIds = selectedIds.value.filter(id => !prevIds.includes(id) && nextSelectedSet.has(id)) await fillPricingTotalsForServiceIds(addedIds) + await ensurePricingDetailRowsForCurrentSelection() await saveToIndexedDB() } @@ -916,6 +944,7 @@ const loadFromIndexedDB = async () => { } }) detailRows.value = applyFixedRowTotals(detailRows.value) + await ensurePricingDetailRowsForCurrentSelection() } catch (error) { console.error('loadFromIndexedDB failed:', error) selectedIds.value = [] diff --git a/src/lib/pricingMethodTotals.ts b/src/lib/pricingMethodTotals.ts index 5b68507..55f7f3f 100644 --- a/src/lib/pricingMethodTotals.ts +++ b/src/lib/pricingMethodTotals.ts @@ -121,6 +121,20 @@ const getDefaultMajorFactorById = (id: string) => { return toFiniteNumberOrNull(major?.defCoe) } +const isCostMajorById = (id: string) => { + const resolvedId = majorById.has(id) ? id : majorIdAliasMap.get(id) || id + const major = majorById.get(resolvedId) + if (!major) return false + return major.hasCost !== false +} + +const isAreaMajorById = (id: string) => { + const resolvedId = majorById.has(id) ? id : majorIdAliasMap.get(id) || id + const major = majorById.get(resolvedId) + if (!major) return false + return major.hasArea !== false +} + const isDualScaleMajorById = (id: string) => { const resolvedId = majorById.has(id) ? id : majorIdAliasMap.get(id) || id const major = majorById.get(resolvedId) @@ -299,6 +313,44 @@ const getOnlyCostScaleBudgetFee = ( }) } +const buildOnlyCostScaleDetailRows = ( + serviceId: string, + rowsFromDb: Array> | undefined, + consultCategoryFactorMap?: Map, + majorFactorMap?: Map, + industryId?: string | null +) => { + const totalAmount = sumByNumber(rowsFromDb || [], row => + typeof row?.amount === 'number' && Number.isFinite(row.amount) ? row.amount : null + ) + const onlyRow = (rowsFromDb || []).find(row => String(row?.id || '') === ONLY_COST_SCALE_ROW_ID) + const consultCategoryFactor = + toFiniteNumberOrNull(onlyRow?.consultCategoryFactor) ?? + consultCategoryFactorMap?.get(String(serviceId)) ?? + getDefaultConsultCategoryFactor(serviceId) + const industryMajorEntry = getIndustryMajorEntryByIndustryId(industryId) + const majorFactor = + toFiniteNumberOrNull(onlyRow?.majorFactor) ?? + (industryMajorEntry ? majorFactorMap?.get(industryMajorEntry.id) ?? null : null) ?? + toFiniteNumberOrNull(industryMajorEntry?.item?.defCoe) ?? + 1 + const workStageFactor = toFiniteNumberOrNull(onlyRow?.workStageFactor) ?? 1 + const workRatio = toFiniteNumberOrNull(onlyRow?.workRatio) ?? 100 + + return [ + { + id: ONLY_COST_SCALE_ROW_ID, + amount: totalAmount, + consultCategoryFactor, + majorFactor, + workStageFactor, + workRatio, + benchmarkBudgetBasicChecked: typeof onlyRow?.benchmarkBudgetBasicChecked === 'boolean' ? onlyRow.benchmarkBudgetBasicChecked : true, + benchmarkBudgetOptionalChecked: typeof onlyRow?.benchmarkBudgetOptionalChecked === 'boolean' ? onlyRow.benchmarkBudgetOptionalChecked : true + } + ] +} + const getLandBudgetFee = (row: ScaleRow) => { return getScaleBudgetFee({ benchmarkBudget: getBenchmarkBudgetByLandArea(row.landArea), @@ -484,7 +536,7 @@ export const getPricingMethodTotalsForService = async (params: { majorFactorMap, industryId ) - : (() => { + : (() => { const investRows = resolveScaleRows( serviceId, investData, @@ -493,6 +545,7 @@ export const getPricingMethodTotalsForService = async (params: { majorFactorMap ) return sumByNumber(investRows, row => { + if (!isCostMajorById(row.id)) return null if (excludeInvestmentCostAndAreaRows && isDualScaleMajorById(row.id)) return null return getInvestmentBudgetFee(row) }) @@ -505,7 +558,7 @@ export const getPricingMethodTotalsForService = async (params: { consultCategoryFactorMap, majorFactorMap ) - const landScale = sumByNumber(landRows, row => getLandBudgetFee(row)) + const landScale = sumByNumber(landRows, row => (isAreaMajorById(row.id) ? getLandBudgetFee(row) : null)) const defaultWorkloadRows = buildDefaultWorkloadRows(serviceId, consultCategoryFactorMap) const workload = @@ -532,6 +585,103 @@ export const getPricingMethodTotalsForService = async (params: { } } +export const ensurePricingMethodDetailRowsForServices = async (params: { + contractId: string + serviceIds: Array + options?: PricingMethodTotalsOptions +}) => { + const uniqueServiceIds = Array.from(new Set(params.serviceIds.map(serviceId => String(serviceId)))) + if (uniqueServiceIds.length === 0) return + + const htDbKey = `ht-info-v3-${params.contractId}` + const consultFactorDbKey = `ht-consult-category-factor-v1-${params.contractId}` + const majorFactorDbKey = `ht-major-factor-v1-${params.contractId}` + const baseInfoDbKey = 'xm-base-info-v1' + + const [htData, consultFactorData, majorFactorData, baseInfo] = await Promise.all([ + localforage.getItem(htDbKey), + localforage.getItem(consultFactorDbKey), + localforage.getItem(majorFactorDbKey), + localforage.getItem(baseInfoDbKey) + ]) + + const consultCategoryFactorMap = buildConsultCategoryFactorMap(consultFactorData) + const majorFactorMap = buildMajorFactorMap(majorFactorData) + const industryId = typeof baseInfo?.projectIndustry === 'string' ? baseInfo.projectIndustry.trim() : '' + const excludeInvestmentCostAndAreaRows = params.options?.excludeInvestmentCostAndAreaRows === true + + await Promise.all( + uniqueServiceIds.map(async serviceId => { + const investDbKey = `tzGMF-${params.contractId}-${serviceId}` + const landDbKey = `ydGMF-${params.contractId}-${serviceId}` + const workloadDbKey = `gzlF-${params.contractId}-${serviceId}` + const hourlyDbKey = `hourlyPricing-${params.contractId}-${serviceId}` + const [investData, landData, workloadData, hourlyData] = await Promise.all([ + localforage.getItem(investDbKey), + localforage.getItem(landDbKey), + localforage.getItem(workloadDbKey), + localforage.getItem(hourlyDbKey) + ]) + + const shouldInitInvest = !Array.isArray(investData?.detailRows) || investData!.detailRows!.length === 0 + const shouldInitLand = !Array.isArray(landData?.detailRows) || landData!.detailRows!.length === 0 + const shouldInitWorkload = !Array.isArray(workloadData?.detailRows) || workloadData!.detailRows!.length === 0 + const shouldInitHourly = !Array.isArray(hourlyData?.detailRows) || hourlyData!.detailRows!.length === 0 + + const writeTasks: Promise[] = [] + + if (shouldInitInvest) { + const onlyCostScale = isOnlyCostScaleService(serviceId) + const investRows = onlyCostScale + ? buildOnlyCostScaleDetailRows( + serviceId, + (htData?.detailRows as Array> | undefined), + consultCategoryFactorMap, + majorFactorMap, + industryId + ) + : resolveScaleRows( + serviceId, + null, + htData, + consultCategoryFactorMap, + majorFactorMap + ).filter(row => { + if (!isCostMajorById(row.id)) return false + if (excludeInvestmentCostAndAreaRows && isDualScaleMajorById(row.id)) return false + return true + }) + writeTasks.push(localforage.setItem(investDbKey, { detailRows: investRows })) + } + + if (shouldInitLand) { + const landRows = resolveScaleRows( + serviceId, + null, + htData, + consultCategoryFactorMap, + majorFactorMap + ).filter(row => isAreaMajorById(row.id)) + writeTasks.push(localforage.setItem(landDbKey, { detailRows: landRows })) + } + + if (shouldInitWorkload) { + const workloadRows = buildDefaultWorkloadRows(serviceId, consultCategoryFactorMap) + writeTasks.push(localforage.setItem(workloadDbKey, { detailRows: workloadRows })) + } + + if (shouldInitHourly) { + const hourlyRows = buildDefaultHourlyRows() + writeTasks.push(localforage.setItem(hourlyDbKey, { detailRows: hourlyRows })) + } + + if (writeTasks.length > 0) { + await Promise.all(writeTasks) + } + }) + ) +} + export const getPricingMethodTotalsForServices = async (params: { contractId: string serviceIds: Array diff --git a/src/main.ts b/src/main.ts index 1dfd39d..680373b 100644 --- a/src/main.ts +++ b/src/main.ts @@ -10,7 +10,7 @@ import { RowAutoHeightModule, TextEditorModule, TooltipModule, - UndoRedoEditModule,RenderApiModule + UndoRedoEditModule,RenderApiModule ,ColumnApiModule ,CellSpanModule } from 'ag-grid-community' import { @@ -19,7 +19,7 @@ import { ClipboardModule, LicenseManager, RowGroupingModule, - TreeDataModule,ContextMenuModule + TreeDataModule,ContextMenuModule,ValidationModule } from 'ag-grid-enterprise' import { createPinia } from 'pinia' import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' @@ -40,14 +40,14 @@ const AG_GRID_MODULES = [ LargeTextEditorModule, UndoRedoEditModule, CellStyleModule, - PinnedRowModule,RenderApiModule , + PinnedRowModule,RenderApiModule ,ColumnApiModule , TooltipModule, TreeDataModule, AggregationModule, RowGroupingModule, CellSelectionModule, ClipboardModule, - LocaleModule, + LocaleModule,ValidationModule ,CellSpanModule ] const pinia = createPinia() diff --git a/src/sql.ts b/src/sql.ts index 457f432..809075a 100644 --- a/src/sql.ts +++ b/src/sql.ts @@ -501,7 +501,6 @@ export function getBasicFeeFromScale( * @returns 导出流程完成后的 Promise */ export async function exportFile(fileName: string, data: any): Promise { - console.log(data) if (window.showSaveFilePicker) { const handle = await window.showSaveFilePicker({ suggestedName: fileName, @@ -727,7 +726,7 @@ async function generateTemplate(data) { try { // 获取模板 let templateExcel = 'template20260226001test010'; - let templateUrl = `./public/${templateExcel}.xlsx`; + let templateUrl = `./${templateExcel}.xlsx`; let buf = await (await fetch(templateUrl)).arrayBuffer(); let workbook = new ExcelJS.Workbook(); await workbook.xlsx.load(buf);