系数字段修改

This commit is contained in:
wintsa 2026-03-07 17:52:33 +08:00
parent 21d3f0379c
commit fc26a87bee
12 changed files with 782 additions and 105 deletions

View File

@ -1844,3 +1844,367 @@ function numberFormatter(num, decimalNum) {
return big;
}
}
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: '',// 用户输入的说明
},
],
}
},
},
],
};

View File

@ -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<FeeRow>[] = [
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<FeeRow>[] = [
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 === ''

View File

@ -46,12 +46,16 @@ const gridApi = ref<GridApi<FactorRow> | 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<FactorRow>[] = [
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 ''

View File

@ -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<DetailRow>[] = [
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<DetailRow>[] = [
return '点击输入'
}
if (params.value == null) return ''
return formatThousands(params.value)
return formatThousandsFlexible(params.value, 3)
}
},
{
@ -344,7 +344,7 @@ const columnDefs: ColDef<DetailRow>[] = [
return '点击输入'
}
if (params.value == null) return ''
return formatThousands(params.value, 3)
return formatThousandsFlexible(params.value, 3)
}
}
]

View File

@ -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 = <K extends keyof DetailRow>(
'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 = <K extends keyof DetailRow>(
'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<DetailRow> | ColGroupDef<DetailRow>)[] = [
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)
}
},
{

View File

@ -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<Pick<DetailRow, 'consultCategoryFactor' | 'majorFactor' | 'workStageFactor' | 'workRatio' | 'remark'>>
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<DetailRow, 'amount'>) =>
getBenchmarkBudgetSplitByScale(row?.amount, 'cost')
const getBudgetFee = (
row?: Pick<DetailRow, 'amount' | 'majorFactor' | 'consultCategoryFactor' | 'workStageFactor' | 'workRatio'>
const getCheckedBenchmarkBudgetSplitByAmount = (
row?: Pick<DetailRow, 'amount' | 'benchmarkBudgetBasicChecked' | 'benchmarkBudgetOptionalChecked'>
) => {
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<DetailRow, 'amount' | 'majorFactor' | 'consultCategoryFactor' | 'workStageFactor' | 'workRatio'>
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<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
{
headerName: '造价金额(万元)',
@ -541,7 +618,7 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
? 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<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
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<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
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<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
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<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
? 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<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
? 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<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
? 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<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
? 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<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
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<typeof setTimeout> | null = null
let gridPersistTimer: ReturnType<typeof setTimeout> | 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)]

View File

@ -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<DetailRow, 'landArea'>) =>
getBenchmarkBudgetSplitByScale(row?.landArea, 'area')
const getBudgetFee = (
row?: Pick<DetailRow, 'landArea' | 'majorFactor' | 'consultCategoryFactor' | 'workStageFactor' | 'workRatio'>
const getCheckedBenchmarkBudgetSplitByLandArea = (
row?: Pick<DetailRow, 'landArea' | 'benchmarkBudgetBasicChecked' | 'benchmarkBudgetOptionalChecked'>
) => {
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<DetailRow, 'landArea' | 'majorFactor' | 'consultCategoryFactor' | 'workStageFactor' | 'workRatio'>
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<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
@ -476,7 +526,7 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
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<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
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<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
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<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
'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<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
'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<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
'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<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
'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<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
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

View File

@ -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<DetailRow>[] = [
!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<DetailRow>[] = [
}
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<DetailRow>[] = [
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)
}
},
{

View File

@ -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<DetailRow>[] = [
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<DetailRow>[] = [
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<DetailRow>[] = [
},
// 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<DetailRow>[] = [
},
// 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<DetailRow>[] = [
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 = []

View File

@ -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<Record<string, unknown>> | undefined,
consultCategoryFactorMap?: Map<string, number | null>,
majorFactorMap?: Map<string, number | null>,
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<string | number>
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<StoredDetailRowsState>(htDbKey),
localforage.getItem<StoredFactorState>(consultFactorDbKey),
localforage.getItem<StoredFactorState>(majorFactorDbKey),
localforage.getItem<XmBaseInfoState>(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<StoredDetailRowsState>(investDbKey),
localforage.getItem<StoredDetailRowsState>(landDbKey),
localforage.getItem<StoredDetailRowsState>(workloadDbKey),
localforage.getItem<StoredDetailRowsState>(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<unknown>[] = []
if (shouldInitInvest) {
const onlyCostScale = isOnlyCostScaleService(serviceId)
const investRows = onlyCostScale
? buildOnlyCostScaleDetailRows(
serviceId,
(htData?.detailRows as Array<Record<string, unknown>> | 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<string | number>

View File

@ -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()

View File

@ -501,7 +501,6 @@ export function getBasicFeeFromScale(
* @returns Promise
*/
export async function exportFile(fileName: string, data: any): Promise<void> {
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);