Compare commits

..

2 Commits

Author SHA1 Message Date
303f54bb71 if 2026-03-07 16:09:06 +08:00
043e1fc879 fix 2026-03-07 15:32:54 +08:00
9 changed files with 686 additions and 155 deletions

View File

@ -123,6 +123,23 @@ const loadFromIndexedDB = async () => {
} }
const columnDefs: ColDef<FeeRow>[] = [ const columnDefs: ColDef<FeeRow>[] = [
{
headerName: '序号',
colId: 'rowNo',
minWidth: 68,
maxWidth: 80,
flex: 0.6,
editable: false,
sortable: false,
filter: false,
cellStyle: { textAlign: 'center' },
valueGetter: params =>
params.node?.rowPinned
? ''
: typeof params.node?.rowIndex === 'number'
? params.node.rowIndex + 1
: ''
},
{ {
headerName: '费用项', headerName: '费用项',
field: 'feeItem', field: 'feeItem',

View File

@ -1,14 +1,55 @@
<script setup lang="ts"> <script setup lang="ts">
import { serviceList } from '@/sql' import { computed, onActivated, onMounted, ref } from 'vue'
import localforage from 'localforage'
import { getServiceDictEntries, isIndustryEnabledByType, getIndustryTypeValue } from '@/sql'
import XmFactorGrid from '@/components/common/XmFactorGrid.vue' import XmFactorGrid from '@/components/common/XmFactorGrid.vue'
interface XmBaseInfoState {
projectIndustry?: string
}
type ServiceItem = {
code: string
name: string
defCoe: number | null
desc?: string | null
notshowByzxflxs?: boolean
}
const PROJECT_INFO_KEY = 'xm-base-info-v1'
const projectIndustry = ref('')
const loadProjectIndustry = async () => {
try {
const data = await localforage.getItem<XmBaseInfoState>(PROJECT_INFO_KEY)
projectIndustry.value =
typeof data?.projectIndustry === 'string' ? data.projectIndustry.trim() : ''
} catch (error) {
console.error('loadProjectIndustry failed:', error)
projectIndustry.value = ''
}
}
const filteredServiceDict = computed<Record<string, ServiceItem>>(() => {
const industry = projectIndustry.value
if (!industry) return {}
const entries = getServiceDictEntries()
.filter(({ item }) => isIndustryEnabledByType(item, getIndustryTypeValue(industry))
)
.map(({ id, item }) => [id, item as ServiceItem] as const)
return Object.fromEntries(entries)
})
onMounted(() => {
void loadProjectIndustry()
})
onActivated(() => {
void loadProjectIndustry()
})
</script> </script>
<template> <template>
<XmFactorGrid <XmFactorGrid title="咨询分类系数明细" storage-key="xm-consult-category-factor-v1" :dict="filteredServiceDict"
title="咨询分类系数明细" :disable-budget-edit-when-standard-null="true" :exclude-notshow-by-zxflxs="true" />
storage-key="xm-consult-category-factor-v1"
:dict="serviceList"
:disable-budget-edit-when-standard-null="true"
:exclude-notshow-by-zxflxs="true"
/>
</template> </template>

View File

@ -5,20 +5,29 @@
:subtitle="`合同ID${contractId}`" :subtitle="`合同ID${contractId}`"
:copy-text="contractId" :copy-text="contractId"
:storage-key="`zxfw-pricing-active-cat-${contractId}-${serviceId}`" :storage-key="`zxfw-pricing-active-cat-${contractId}-${serviceId}`"
default-category="investment-scale-method" :default-category="defaultCategory"
:categories="pricingCategories" :categories="pricingCategories"
/> />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { defineAsyncComponent, defineComponent, h, markRaw, type Component } from 'vue' import { computed, defineAsyncComponent, defineComponent, h, markRaw, type Component } from 'vue'
import TypeLine from '@/layout/typeLine.vue' import TypeLine from '@/layout/typeLine.vue'
import MethodUnavailableNotice from '@/components/common/MethodUnavailableNotice.vue'
interface ServiceMethodType {
scale?: boolean | null
onlyCostScale?: boolean | null
amount?: boolean | null
workDay?: boolean | null
}
const props = defineProps<{ const props = defineProps<{
contractId: string contractId: string
contractName?: string contractName?: string
serviceId: string|number serviceId: string|number
fwName:string fwName:string
type?: ServiceMethodType
}>() }>()
interface PricingCategoryItem { interface PricingCategoryItem {
@ -27,6 +36,22 @@ interface PricingCategoryItem {
component: Component component: Component
} }
const resolveMethodEnabled = (value: boolean | null | undefined, fallback = true) =>
typeof value === 'boolean' ? value : fallback
const methodAvailability = computed(() => {
const scale = resolveMethodEnabled(props.type?.scale, true)
const onlyCostScale = resolveMethodEnabled(props.type?.onlyCostScale, false)
const amount = resolveMethodEnabled(props.type?.amount, true)
const workDay = resolveMethodEnabled(props.type?.workDay, true)
return {
investmentScale: scale,
landScale: scale && !onlyCostScale,
workload: amount,
hourly: workDay
}
})
const createPricingPane = (name: string) => const createPricingPane = (name: string) =>
markRaw( markRaw(
defineComponent({ defineComponent({
@ -44,15 +69,66 @@ const createPricingPane = (name: string) =>
}) })
) )
const createMethodUnavailablePane = (title: string, message: string) =>
markRaw(
defineComponent({
name: 'MethodUnavailablePane',
setup() {
return () => h(MethodUnavailableNotice, { title, message })
}
})
)
const investmentScaleView = createPricingPane('InvestmentScalePricingPane') const investmentScaleView = createPricingPane('InvestmentScalePricingPane')
const landScaleView = createPricingPane('LandScalePricingPane') const landScaleView = createPricingPane('LandScalePricingPane')
const workloadView = createPricingPane('WorkloadPricingPane') const workloadView = createPricingPane('WorkloadPricingPane')
const hourlyView = createPricingPane('HourlyPricingPane') const hourlyView = createPricingPane('HourlyPricingPane')
const pricingCategories: PricingCategoryItem[] = [ const investmentScaleUnavailableView = createMethodUnavailablePane(
{ key: 'investment-scale-method', label: '投资规模法', component: investmentScaleView }, '该服务不适用投资规模法',
{ key: 'land-scale-method', label: '用地规模法', component: landScaleView }, '当前服务未启用规模法,投资规模法不可编辑。'
{ key: 'workload-method', label: '工作量法', component: workloadView }, )
{ key: 'hourly-method', label: '工时法', component: hourlyView } const landScaleUnavailableView = createMethodUnavailablePane(
] '该服务不适用用地规模法',
'当前服务仅支持投资规模法,用地规模法不可编辑。'
)
const workloadUnavailableView = createMethodUnavailablePane(
'该服务不适用工作量法',
'当前服务未启用工作量法,工作量法不可编辑。'
)
const hourlyUnavailableView = createMethodUnavailablePane(
'该服务不适用工时法',
'当前服务未启用工时法,工时法不可编辑。'
)
const pricingCategories = computed<PricingCategoryItem[]>(() => [
{
key: 'investment-scale-method',
label: '投资规模法',
component: methodAvailability.value.investmentScale ? investmentScaleView : investmentScaleUnavailableView
},
{
key: 'land-scale-method',
label: '用地规模法',
component: methodAvailability.value.landScale ? landScaleView : landScaleUnavailableView
},
{
key: 'workload-method',
label: '工作量法',
component: methodAvailability.value.workload ? workloadView : workloadUnavailableView
},
{
key: 'hourly-method',
label: '工时法',
component: methodAvailability.value.hourly ? hourlyView : hourlyUnavailableView
}
])
const defaultCategory = computed(() => {
if (methodAvailability.value.investmentScale) return 'investment-scale-method'
if (methodAvailability.value.landScale) return 'land-scale-method'
if (methodAvailability.value.workload) return 'workload-method'
if (methodAvailability.value.hourly) return 'hourly-method'
return 'investment-scale-method'
})
</script> </script>

View File

@ -124,9 +124,10 @@ const xmCategories: XmCategoryItem[] = [
{ key: 'info', label: '规模信息', component: htView }, { key: 'info', label: '规模信息', component: htView },
{ key: 'consult-category-factor', label: '咨询分类系数', component: consultCategoryFactorView }, { key: 'consult-category-factor', label: '咨询分类系数', component: consultCategoryFactorView },
{ key: 'major-factor', label: '工程专业系数', component: majorFactorView }, { key: 'major-factor', label: '工程专业系数', component: majorFactorView },
{ key: 'contract', label: '咨询服务', component: zxfwView },
{ key: 'additional-work-fee', label: '附加工作费', component: additionalWorkFeeView }, { key: 'additional-work-fee', label: '附加工作费', component: additionalWorkFeeView },
{ key: 'reserve-fee', label: '预备费', component: reserveFeeView }, { key: 'reserve-fee', label: '预备费', component: reserveFeeView },
{ key: 'contract', label: '咨询服务', component: zxfwView },
]; ];
</script> </script>

View File

@ -3,7 +3,7 @@ import { computed, onActivated, onBeforeUnmount, onMounted, ref, watch } from 'v
import { AgGridVue } from 'ag-grid-vue3' import { AgGridVue } from 'ag-grid-vue3'
import type { ColDef, ColGroupDef } from 'ag-grid-community' import type { ColDef, ColGroupDef } from 'ag-grid-community'
import localforage from 'localforage' import localforage from 'localforage'
import { getMajorDictEntries, isMajorIdInIndustryScope } from '@/sql' import { getMajorDictEntries, getServiceDictItemById, industryTypeList, isMajorIdInIndustryScope } from '@/sql'
import { myTheme, gridOptions } from '@/lib/diyAgGridOptions' import { myTheme, gridOptions } from '@/lib/diyAgGridOptions'
import { decimalAggSum, roundTo, sumByNumber } from '@/lib/decimal' import { decimalAggSum, roundTo, sumByNumber } from '@/lib/decimal'
import { formatThousands } from '@/lib/numberFormat' import { formatThousands } from '@/lib/numberFormat'
@ -78,6 +78,10 @@ interface XmBaseInfoState {
projectIndustry?: string projectIndustry?: string
} }
interface ServiceLite {
onlyCostScale?: boolean | null
}
const props = defineProps<{ const props = defineProps<{
contractId: string, contractId: string,
serviceId: string | number serviceId: string | number
@ -95,11 +99,26 @@ const consultCategoryFactorMap = ref<Map<string, number | null>>(new Map())
const majorFactorMap = ref<Map<string, number | null>>(new Map()) const majorFactorMap = ref<Map<string, number | null>>(new Map())
let factorDefaultsLoaded = false let factorDefaultsLoaded = false
const paneInstanceCreatedAt = Date.now() const paneInstanceCreatedAt = Date.now()
const ONLY_COST_SCALE_ROW_ID = '__only-cost-scale-total__'
const industryNameMap = new Map(
industryTypeList.flatMap(item => [
[String(item.id).trim(), item.name],
[String(item.type).trim(), item.name]
])
)
const getDefaultConsultCategoryFactor = () => const getDefaultConsultCategoryFactor = () =>
consultCategoryFactorMap.value.get(String(props.serviceId)) ?? null consultCategoryFactorMap.value.get(String(props.serviceId)) ?? null
const getDefaultMajorFactorById = (id: string): number | null => majorFactorMap.value.get(id) ?? null const getDefaultMajorFactorById = (id: string): number | null => majorFactorMap.value.get(id) ?? null
const isOnlyCostScaleService = computed(() => {
const service = getServiceDictItemById(props.serviceId) as ServiceLite | undefined
return service?.onlyCostScale === true
})
const totalLabel = computed(() => {
const industryName = industryNameMap.get(activeIndustryCode.value.trim()) || ''
return industryName ? `${industryName}总投资` : '总投资'
})
const loadFactorDefaults = async () => { const loadFactorDefaults = async () => {
const [consultMap, majorMap] = await Promise.all([ const [consultMap, majorMap] = await Promise.all([
@ -149,7 +168,14 @@ const shouldForceDefaultLoad = () => {
} }
const detailRows = ref<DetailRow[]>([]) const detailRows = ref<DetailRow[]>([])
type majorLite = { code: string; name: string; defCoe: number | null; hasCost?: boolean; hasArea?: boolean } type majorLite = {
code: string
name: string
defCoe: number | null
hasCost?: boolean
hasArea?: boolean
industryId?: string | number | null
}
const serviceEntries = getMajorDictEntries().map(({ id, item }) => [id, item] as [string, majorLite]) const serviceEntries = getMajorDictEntries().map(({ id, item }) => [id, item] as [string, majorLite])
const majorIdAliasMap = new Map(getMajorDictEntries().map(({ rawId, id }) => [rawId, id])) const majorIdAliasMap = new Map(getMajorDictEntries().map(({ rawId, id }) => [rawId, id]))
@ -184,12 +210,17 @@ const detailDict: DictGroup[] = (() => {
}) })
} }
const hasCost = item.hasCost !== false
const hasArea = item.hasArea !== false
// hasCost && hasArea
if (hasCost && hasArea) continue
groupMap.get(parentCode)!.children.push({ groupMap.get(parentCode)!.children.push({
id: key, id: key,
code, code,
name: item.name, name: item.name,
hasCost: item.hasCost !== false, hasCost,
hasArea: item.hasArea !== false hasArea
}) })
} }
@ -241,6 +272,63 @@ const buildDefaultRows = (): DetailRow[] => {
return rows return rows
} }
const calcOnlyCostScaleAmountFromRows = (rows?: Array<{ amount?: unknown }>) =>
sumByNumber(rows || [], row => (typeof row?.amount === 'number' ? row.amount : null))
const getOnlyCostScaleMajorFactorDefault = () => {
const industryId = String(activeIndustryCode.value || '').trim()
if (!industryId) return 1
const industryMajor = serviceEntries.find(([, item]) => {
const majorIndustryId = String(item?.industryId ?? '').trim()
return majorIndustryId === industryId && !String(item?.code || '').includes('-')
})
if (!industryMajor) return 1
const [majorId, majorItem] = industryMajor
const fromMap = majorFactorMap.value.get(String(majorId))
if (typeof fromMap === 'number' && Number.isFinite(fromMap)) return fromMap
if (typeof majorItem?.defCoe === 'number' && Number.isFinite(majorItem.defCoe)) return majorItem.defCoe
return 1
}
const buildOnlyCostScaleRow = (
amount: number | null,
fromDb?: Partial<Pick<DetailRow, 'consultCategoryFactor' | 'majorFactor' | 'workStageFactor' | 'workRatio' | 'remark'>>
): DetailRow => ({
id: ONLY_COST_SCALE_ROW_ID,
groupCode: 'TOTAL',
groupName: '总投资',
majorCode: 'TOTAL',
majorName: '总投资',
hasCost: true,
hasArea: false,
amount,
benchmarkBudget: null,
benchmarkBudgetBasic: null,
benchmarkBudgetOptional: null,
benchmarkBudgetBasicChecked: true,
benchmarkBudgetOptionalChecked: true,
basicFormula: '',
optionalFormula: '',
consultCategoryFactor:
typeof fromDb?.consultCategoryFactor === 'number' ? fromDb.consultCategoryFactor : getDefaultConsultCategoryFactor(),
majorFactor: typeof fromDb?.majorFactor === 'number' ? fromDb.majorFactor : getOnlyCostScaleMajorFactorDefault(),
workStageFactor: typeof fromDb?.workStageFactor === 'number' ? fromDb.workStageFactor : 1,
workRatio: typeof fromDb?.workRatio === 'number' ? fromDb.workRatio : 100,
budgetFee: null,
budgetFeeBasic: null,
budgetFeeOptional: null,
remark: typeof fromDb?.remark === 'string' ? fromDb.remark : '',
path: [ONLY_COST_SCALE_ROW_ID]
})
const buildOnlyCostScaleRows = (
rowsFromDb?: Array<Partial<DetailRow> & Pick<DetailRow, 'id'>>
): DetailRow[] => {
const totalAmount = calcOnlyCostScaleAmountFromRows(rowsFromDb)
const onlyRow = rowsFromDb?.find(row => String(row.id) === ONLY_COST_SCALE_ROW_ID)
return [buildOnlyCostScaleRow(totalAmount, onlyRow)]
}
type SourceRow = Pick<DetailRow, 'id'> & type SourceRow = Pick<DetailRow, 'id'> &
Partial< Partial<
Pick< Pick<
@ -340,6 +428,11 @@ const formatMajorFactor = (params: any) => {
} }
const formatEditableMoney = (params: any) => { const formatEditableMoney = (params: any) => {
if (isOnlyCostScaleService.value) {
if (!params.node?.rowPinned) return ''
if (params.value == null || params.value === '') return '点击输入'
return formatThousands(params.value)
}
if (!params.node?.group && !params.node?.rowPinned && !params.data?.hasCost) { if (!params.node?.group && !params.node?.rowPinned && !params.data?.hasCost) {
return '' return ''
} }
@ -432,16 +525,22 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
minWidth: 90, minWidth: 90,
flex: 2, flex: 2,
editable: params => !params.node?.group && !params.node?.rowPinned && Boolean(params.data?.hasCost), editable: params => {
if (isOnlyCostScaleService.value) return Boolean(params.node?.rowPinned)
return !params.node?.group && !params.node?.rowPinned && Boolean(params.data?.hasCost)
},
cellClass: params => cellClass: params =>
!params.node?.group && !params.node?.rowPinned && params.data?.hasCost isOnlyCostScaleService.value && params.node?.rowPinned
? 'ag-right-aligned-cell editable-cell-line'
: !params.node?.group && !params.node?.rowPinned && params.data?.hasCost
? 'ag-right-aligned-cell editable-cell-line' ? 'ag-right-aligned-cell editable-cell-line'
: 'ag-right-aligned-cell', : 'ag-right-aligned-cell',
cellClassRules: { cellClassRules: {
'editable-cell-empty': params => 'editable-cell-empty': params =>
!params.node?.group && !params.node?.rowPinned && Boolean(params.data?.hasCost) && (params.value == null || params.value === '') isOnlyCostScaleService.value && params.node?.rowPinned
? params.value == null || params.value === ''
: !params.node?.group && !params.node?.rowPinned && Boolean(params.data?.hasCost) && (params.value == null || params.value === '')
}, },
aggFunc: decimalAggSum,
valueParser: params => parseNumberOrNull(params.newValue, { precision: 2 }), valueParser: params => parseNumberOrNull(params.newValue, { precision: 2 }),
valueFormatter: formatEditableMoney valueFormatter: formatEditableMoney
}, },
@ -457,10 +556,9 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
minWidth: 130, minWidth: 130,
flex: 1, flex: 1,
cellClass: 'ag-right-aligned-cell', cellClass: 'ag-right-aligned-cell',
aggFunc: decimalAggSum,
valueGetter: params => valueGetter: params =>
params.node?.rowPinned params.node?.rowPinned
? params.data?.benchmarkBudgetBasic ?? null ? null
: getBenchmarkBudgetSplitByAmount(params.data)?.basic ?? null, : getBenchmarkBudgetSplitByAmount(params.data)?.basic ?? null,
cellRenderer: createBudgetCellRendererWithCheck('benchmarkBudgetBasicChecked'), cellRenderer: createBudgetCellRendererWithCheck('benchmarkBudgetBasicChecked'),
valueFormatter: formatReadonlyMoney valueFormatter: formatReadonlyMoney
@ -473,10 +571,9 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
minWidth: 130, minWidth: 130,
flex: 1, flex: 1,
cellClass: 'ag-right-aligned-cell', cellClass: 'ag-right-aligned-cell',
aggFunc: decimalAggSum,
valueGetter: params => valueGetter: params =>
params.node?.rowPinned params.node?.rowPinned
? params.data?.benchmarkBudgetOptional ?? null ? null
: getBenchmarkBudgetSplitByAmount(params.data)?.optional ?? null, : getBenchmarkBudgetSplitByAmount(params.data)?.optional ?? null,
cellRenderer: createBudgetCellRendererWithCheck('benchmarkBudgetOptionalChecked'), cellRenderer: createBudgetCellRendererWithCheck('benchmarkBudgetOptionalChecked'),
valueFormatter: formatReadonlyMoney valueFormatter: formatReadonlyMoney
@ -489,10 +586,9 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
minWidth: 100, minWidth: 100,
flex: 1, flex: 1,
cellClass: 'ag-right-aligned-cell', cellClass: 'ag-right-aligned-cell',
aggFunc: decimalAggSum,
valueGetter: params => valueGetter: params =>
params.node?.rowPinned params.node?.rowPinned
? params.data?.benchmarkBudget ?? null ? null
: getBenchmarkBudgetSplitByAmount(params.data)?.total ?? null, : getBenchmarkBudgetSplitByAmount(params.data)?.total ?? null,
valueFormatter: formatReadonlyMoney valueFormatter: formatReadonlyMoney
} }
@ -508,11 +604,21 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
colId: 'consultCategoryFactor', colId: 'consultCategoryFactor',
minWidth: 80, minWidth: 80,
flex: 1, flex: 1,
editable: params => !params.node?.group && !params.node?.rowPinned, editable: params =>
cellClass: params => (!params.node?.group && !params.node?.rowPinned ? 'editable-cell-line' : ''), isOnlyCostScaleService.value
? Boolean(params.node?.rowPinned)
: !params.node?.group && !params.node?.rowPinned,
cellClass: params =>
isOnlyCostScaleService.value && params.node?.rowPinned
? 'editable-cell-line'
: !params.node?.group && !params.node?.rowPinned
? 'editable-cell-line'
: '',
cellClassRules: { cellClassRules: {
'editable-cell-empty': params => 'editable-cell-empty': params =>
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '') isOnlyCostScaleService.value && params.node?.rowPinned
? 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: 2 }),
valueFormatter: formatConsultCategoryFactor valueFormatter: formatConsultCategoryFactor
@ -523,11 +629,21 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
colId: 'majorFactor', colId: 'majorFactor',
minWidth: 80, minWidth: 80,
flex: 1, flex: 1,
editable: params => !params.node?.group && !params.node?.rowPinned, editable: params =>
cellClass: params => (!params.node?.group && !params.node?.rowPinned ? 'editable-cell-line' : ''), isOnlyCostScaleService.value
? Boolean(params.node?.rowPinned)
: !params.node?.group && !params.node?.rowPinned,
cellClass: params =>
isOnlyCostScaleService.value && params.node?.rowPinned
? 'editable-cell-line'
: !params.node?.group && !params.node?.rowPinned
? 'editable-cell-line'
: '',
cellClassRules: { cellClassRules: {
'editable-cell-empty': params => 'editable-cell-empty': params =>
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '') isOnlyCostScaleService.value && params.node?.rowPinned
? 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: 2 }),
valueFormatter: formatMajorFactor valueFormatter: formatMajorFactor
@ -538,11 +654,21 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
colId: 'workStageFactor', colId: 'workStageFactor',
minWidth: 80, minWidth: 80,
flex: 1, flex: 1,
editable: params => !params.node?.group && !params.node?.rowPinned, editable: params =>
cellClass: params => (!params.node?.group && !params.node?.rowPinned ? 'editable-cell-line' : ''), isOnlyCostScaleService.value
? Boolean(params.node?.rowPinned)
: !params.node?.group && !params.node?.rowPinned,
cellClass: params =>
isOnlyCostScaleService.value && params.node?.rowPinned
? 'editable-cell-line'
: !params.node?.group && !params.node?.rowPinned
? 'editable-cell-line'
: '',
cellClassRules: { cellClassRules: {
'editable-cell-empty': params => 'editable-cell-empty': params =>
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '') isOnlyCostScaleService.value && params.node?.rowPinned
? 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: 2 }),
valueFormatter: formatEditableNumber valueFormatter: formatEditableNumber
@ -553,11 +679,21 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
colId: 'workRatio', colId: 'workRatio',
minWidth: 80, minWidth: 80,
flex: 1, flex: 1,
editable: params => !params.node?.group && !params.node?.rowPinned, editable: params =>
cellClass: params => (!params.node?.group && !params.node?.rowPinned ? 'editable-cell-line' : ''), isOnlyCostScaleService.value
? Boolean(params.node?.rowPinned)
: !params.node?.group && !params.node?.rowPinned,
cellClass: params =>
isOnlyCostScaleService.value && params.node?.rowPinned
? 'editable-cell-line'
: !params.node?.group && !params.node?.rowPinned
? 'editable-cell-line'
: '',
cellClassRules: { cellClassRules: {
'editable-cell-empty': params => 'editable-cell-empty': params =>
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '') isOnlyCostScaleService.value && params.node?.rowPinned
? 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: 2 }),
valueFormatter: formatEditableNumber valueFormatter: formatEditableNumber
@ -613,13 +749,13 @@ const autoGroupColumnDef: ColDef = {
}, },
valueFormatter: params => { valueFormatter: params => {
if (params.node?.rowPinned) { if (params.node?.rowPinned) {
return '总合计' return totalLabel.value
} }
const nodeId = String(params.value || '') const nodeId = String(params.value || '')
return idLabelMap.get(nodeId) || nodeId return idLabelMap.get(nodeId) || nodeId
}, },
tooltipValueGetter: params => { tooltipValueGetter: params => {
if (params.node?.rowPinned) return '总合计' if (params.node?.rowPinned) return totalLabel.value
const nodeId = String(params.value || '') const nodeId = String(params.value || '')
return idLabelMap.get(nodeId) || nodeId return idLabelMap.get(nodeId) || nodeId
} }
@ -627,6 +763,8 @@ const autoGroupColumnDef: ColDef = {
const totalAmount = computed(() => sumByNumber(detailRows.value, row => row.amount)) const totalAmount = computed(() => sumByNumber(detailRows.value, row => row.amount))
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 totalBenchmarkBudgetBasic = computed(() => sumByNumber(detailRows.value, row => getBenchmarkBudgetSplitByAmount(row)?.basic))
const totalBenchmarkBudgetOptional = computed(() => sumByNumber(detailRows.value, row => getBenchmarkBudgetSplitByAmount(row)?.optional)) const totalBenchmarkBudgetOptional = computed(() => sumByNumber(detailRows.value, row => getBenchmarkBudgetSplitByAmount(row)?.optional))
@ -644,18 +782,18 @@ const pinnedTopRowData = computed(() => [
majorName: '', majorName: '',
hasCost: false, hasCost: false,
hasArea: false, hasArea: false,
amount: totalAmount.value, amount: isOnlyCostScaleService.value ? onlyCostScaleSourceRow.value.amount : null,
benchmarkBudget: totalBenchmarkBudget.value, benchmarkBudget: null,
benchmarkBudgetBasic: totalBenchmarkBudgetBasic.value, benchmarkBudgetBasic: null,
benchmarkBudgetOptional: totalBenchmarkBudgetOptional.value, benchmarkBudgetOptional: null,
benchmarkBudgetBasicChecked: true, benchmarkBudgetBasicChecked: true,
benchmarkBudgetOptionalChecked: true, benchmarkBudgetOptionalChecked: true,
basicFormula: '', basicFormula: '',
optionalFormula: '', optionalFormula: '',
consultCategoryFactor: null, consultCategoryFactor: isOnlyCostScaleService.value ? onlyCostScaleSourceRow.value.consultCategoryFactor : null,
majorFactor: null, majorFactor: isOnlyCostScaleService.value ? onlyCostScaleSourceRow.value.majorFactor : null,
workStageFactor: null, workStageFactor: isOnlyCostScaleService.value ? onlyCostScaleSourceRow.value.workStageFactor : null,
workRatio: null, workRatio: isOnlyCostScaleService.value ? onlyCostScaleSourceRow.value.workRatio : null,
budgetFee: totalBudgetFee.value, budgetFee: totalBudgetFee.value,
budgetFeeBasic: totalBudgetFeeBasic.value, budgetFeeBasic: totalBudgetFeeBasic.value,
budgetFeeOptional: totalBudgetFeeOptional.value, budgetFeeOptional: totalBudgetFeeOptional.value,
@ -727,6 +865,9 @@ const loadFromIndexedDB = async () => {
const applyContractDefaultRows = async () => { const applyContractDefaultRows = async () => {
const htData = await localforage.getItem<{ detailRows: SourceRow[] }>(HT_DB_KEY.value) const htData = await localforage.getItem<{ detailRows: SourceRow[] }>(HT_DB_KEY.value)
const hasContractRows = Array.isArray(htData?.detailRows) && htData.detailRows.length > 0 const hasContractRows = Array.isArray(htData?.detailRows) && htData.detailRows.length > 0
if (isOnlyCostScaleService.value) {
detailRows.value = hasContractRows ? buildOnlyCostScaleRows(htData!.detailRows as any) : buildOnlyCostScaleRows()
} else {
detailRows.value = hasContractRows detailRows.value = hasContractRows
? mergeWithDictRows(htData!.detailRows, { includeFactorValues: true }) ? mergeWithDictRows(htData!.detailRows, { includeFactorValues: true })
: buildDefaultRows().map(row => ({ : buildDefaultRows().map(row => ({
@ -734,6 +875,7 @@ const loadFromIndexedDB = async () => {
consultCategoryFactor: getDefaultConsultCategoryFactor(), consultCategoryFactor: getDefaultConsultCategoryFactor(),
majorFactor: getDefaultMajorFactorById(row.id) majorFactor: getDefaultMajorFactorById(row.id)
})) }))
}
syncComputedValuesToDetailRows() syncComputedValuesToDetailRows()
} }
if (shouldForceDefaultLoad()) { if (shouldForceDefaultLoad()) {
@ -743,7 +885,9 @@ const loadFromIndexedDB = async () => {
const data = await localforage.getItem<XmInfoState>(DB_KEY.value) const data = await localforage.getItem<XmInfoState>(DB_KEY.value)
if (data) { if (data) {
detailRows.value = mergeWithDictRows(data.detailRows) detailRows.value = isOnlyCostScaleService.value
? buildOnlyCostScaleRows(data.detailRows as any)
: mergeWithDictRows(data.detailRows)
syncComputedValuesToDetailRows() syncComputedValuesToDetailRows()
return return
} }
@ -751,7 +895,7 @@ const loadFromIndexedDB = async () => {
await applyContractDefaultRows() await applyContractDefaultRows()
} catch (error) { } catch (error) {
console.error('loadFromIndexedDB failed:', error) console.error('loadFromIndexedDB failed:', error)
detailRows.value = buildDefaultRows() detailRows.value = isOnlyCostScaleService.value ? buildOnlyCostScaleRows() : buildDefaultRows()
syncComputedValuesToDetailRows() syncComputedValuesToDetailRows()
} }
} }
@ -766,6 +910,9 @@ const importContractData = async () => {
await loadFactorDefaults() await loadFactorDefaults()
const htData = await localforage.getItem<{ detailRows: SourceRow[] }>(HT_DB_KEY.value) const htData = await localforage.getItem<{ detailRows: SourceRow[] }>(HT_DB_KEY.value)
const hasContractRows = Array.isArray(htData?.detailRows) && htData.detailRows.length > 0 const hasContractRows = Array.isArray(htData?.detailRows) && htData.detailRows.length > 0
if (isOnlyCostScaleService.value) {
detailRows.value = hasContractRows ? buildOnlyCostScaleRows(htData!.detailRows as any) : buildOnlyCostScaleRows()
} else {
detailRows.value = hasContractRows detailRows.value = hasContractRows
? mergeWithDictRows(htData!.detailRows, { includeFactorValues: true }) ? mergeWithDictRows(htData!.detailRows, { includeFactorValues: true })
: buildDefaultRows().map(row => ({ : buildDefaultRows().map(row => ({
@ -773,6 +920,7 @@ const importContractData = async () => {
consultCategoryFactor: getDefaultConsultCategoryFactor(), consultCategoryFactor: getDefaultConsultCategoryFactor(),
majorFactor: getDefaultMajorFactorById(row.id) majorFactor: getDefaultMajorFactorById(row.id)
})) }))
}
await saveToIndexedDB() await saveToIndexedDB()
} catch (error) { } catch (error) {
console.error('importContractData failed:', error) console.error('importContractData failed:', error)
@ -781,7 +929,7 @@ const importContractData = async () => {
const clearAllData = async () => { const clearAllData = async () => {
try { try {
detailRows.value = buildDefaultRows() detailRows.value = isOnlyCostScaleService.value ? buildOnlyCostScaleRows() : buildDefaultRows()
await saveToIndexedDB() await saveToIndexedDB()
} catch (error) { } catch (error) {
console.error('clearAllData failed:', error) console.error('clearAllData failed:', error)
@ -802,7 +950,29 @@ let persistTimer: ReturnType<typeof setTimeout> | null = null
let gridPersistTimer: ReturnType<typeof setTimeout> | null = null let gridPersistTimer: ReturnType<typeof setTimeout> | null = null
const handleCellValueChanged = () => { const applyOnlyCostScalePinnedValue = (field: string, rawValue: unknown) => {
const parsedValue = parseNumberOrNull(rawValue, { precision: 2 })
const current = detailRows.value[0]
if (!current) {
detailRows.value = [buildOnlyCostScaleRow(field === 'amount' ? parsedValue : null)]
return
}
if (
field !== 'amount' &&
field !== 'consultCategoryFactor' &&
field !== 'majorFactor' &&
field !== 'workStageFactor' &&
field !== 'workRatio'
) {
return
}
detailRows.value = [{ ...current, [field]: parsedValue }]
}
const handleCellValueChanged = (event?: any) => {
if (isOnlyCostScaleService.value && event?.node?.rowPinned && typeof event.colDef?.field === 'string') {
applyOnlyCostScalePinnedValue(event.colDef.field, event.newValue)
}
syncComputedValuesToDetailRows() syncComputedValuesToDetailRows()
if (gridPersistTimer) clearTimeout(gridPersistTimer) if (gridPersistTimer) clearTimeout(gridPersistTimer)
gridPersistTimer = setTimeout(() => { gridPersistTimer = setTimeout(() => {
@ -899,7 +1069,7 @@ const processCellFromClipboard = (params: any) => {
</div> </div>
<div class="ag-theme-quartz h-full min-h-0 w-full flex-1"> <div class="ag-theme-quartz h-full min-h-0 w-full flex-1">
<AgGridVue :style="{ height: '100%' }" :rowData="detailRows" :pinnedTopRowData="pinnedTopRowData" <AgGridVue :style="{ height: '100%' }" :rowData="visibleDetailRows" :pinnedTopRowData="pinnedTopRowData"
:columnDefs="columnDefs" :autoGroupColumnDef="autoGroupColumnDef" :gridOptions="gridOptions" :theme="myTheme" :columnDefs="columnDefs" :autoGroupColumnDef="autoGroupColumnDef" :gridOptions="gridOptions" :theme="myTheme"
@cell-value-changed="handleCellValueChanged" :suppressColumnVirtualisation="true" @cell-value-changed="handleCellValueChanged" :suppressColumnVirtualisation="true"
:suppressRowVirtualisation="true" :cellSelection="{ handle: { mode: 'range' } }" :enableClipboard="true" :suppressRowVirtualisation="true" :cellSelection="{ handle: { mode: 'range' } }" :enableClipboard="true"

View File

@ -3,7 +3,7 @@ import { computed, onActivated, onBeforeUnmount, onMounted, ref, watch } from 'v
import { AgGridVue } from 'ag-grid-vue3' import { AgGridVue } from 'ag-grid-vue3'
import type { ColDef, ColGroupDef } from 'ag-grid-community' import type { ColDef, ColGroupDef } from 'ag-grid-community'
import localforage from 'localforage' import localforage from 'localforage'
import { getMajorDictEntries, isMajorIdInIndustryScope } from '@/sql' import { getMajorDictEntries, industryTypeList, isMajorIdInIndustryScope } from '@/sql'
import { myTheme, gridOptions } from '@/lib/diyAgGridOptions' import { myTheme, gridOptions } from '@/lib/diyAgGridOptions'
import { decimalAggSum, roundTo, sumByNumber } from '@/lib/decimal' import { decimalAggSum, roundTo, sumByNumber } from '@/lib/decimal'
import { formatThousands } from '@/lib/numberFormat' import { formatThousands } from '@/lib/numberFormat'
@ -96,6 +96,16 @@ const consultCategoryFactorMap = ref<Map<string, number | null>>(new Map())
const majorFactorMap = ref<Map<string, number | null>>(new Map()) const majorFactorMap = ref<Map<string, number | null>>(new Map())
let factorDefaultsLoaded = false let factorDefaultsLoaded = false
const paneInstanceCreatedAt = Date.now() const paneInstanceCreatedAt = Date.now()
const industryNameMap = new Map(
industryTypeList.flatMap(item => [
[String(item.id).trim(), item.name],
[String(item.type).trim(), item.name]
])
)
const totalLabel = computed(() => {
const industryName = industryNameMap.get(activeIndustryCode.value.trim()) || ''
return industryName ? `${industryName}总投资` : '总投资'
})
const detailRows = ref<DetailRow[]>([]) const detailRows = ref<DetailRow[]>([])
const getDefaultConsultCategoryFactor = () => const getDefaultConsultCategoryFactor = () =>
@ -463,10 +473,9 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
minWidth: 130, minWidth: 130,
flex: 1, flex: 1,
cellClass: 'ag-right-aligned-cell', cellClass: 'ag-right-aligned-cell',
aggFunc: decimalAggSum,
valueGetter: params => valueGetter: params =>
params.node?.rowPinned params.node?.rowPinned
? params.data?.benchmarkBudgetBasic ?? null ? null
: getBenchmarkBudgetSplitByLandArea(params.data)?.basic ?? null, : getBenchmarkBudgetSplitByLandArea(params.data)?.basic ?? null,
cellRenderer: createBudgetCellRendererWithCheck('benchmarkBudgetBasicChecked'), cellRenderer: createBudgetCellRendererWithCheck('benchmarkBudgetBasicChecked'),
valueFormatter: formatReadonlyMoney valueFormatter: formatReadonlyMoney
@ -479,10 +488,9 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
minWidth: 130, minWidth: 130,
flex: 1, flex: 1,
cellClass: 'ag-right-aligned-cell', cellClass: 'ag-right-aligned-cell',
aggFunc: decimalAggSum,
valueGetter: params => valueGetter: params =>
params.node?.rowPinned params.node?.rowPinned
? params.data?.benchmarkBudgetOptional ?? null ? null
: getBenchmarkBudgetSplitByLandArea(params.data)?.optional ?? null, : getBenchmarkBudgetSplitByLandArea(params.data)?.optional ?? null,
cellRenderer: createBudgetCellRendererWithCheck('benchmarkBudgetOptionalChecked'), cellRenderer: createBudgetCellRendererWithCheck('benchmarkBudgetOptionalChecked'),
valueFormatter: formatReadonlyMoney valueFormatter: formatReadonlyMoney
@ -495,10 +503,9 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
minWidth: 100, minWidth: 100,
flex: 1, flex: 1,
cellClass: 'ag-right-aligned-cell', cellClass: 'ag-right-aligned-cell',
aggFunc: decimalAggSum,
valueGetter: params => valueGetter: params =>
params.node?.rowPinned params.node?.rowPinned
? params.data?.benchmarkBudget ?? null ? null
: getBenchmarkBudgetSplitByLandArea(params.data)?.total ?? null, : getBenchmarkBudgetSplitByLandArea(params.data)?.total ?? null,
valueFormatter: formatReadonlyMoney valueFormatter: formatReadonlyMoney
} }
@ -616,13 +623,13 @@ const autoGroupColumnDef: ColDef = {
}, },
valueFormatter: params => { valueFormatter: params => {
if (params.node?.rowPinned) { if (params.node?.rowPinned) {
return '总合计' return totalLabel.value
} }
const nodeId = String(params.value || '') const nodeId = String(params.value || '')
return idLabelMap.get(nodeId) || nodeId return idLabelMap.get(nodeId) || nodeId
}, },
tooltipValueGetter: params => { tooltipValueGetter: params => {
if (params.node?.rowPinned) return '总合计' if (params.node?.rowPinned) return totalLabel.value
const nodeId = String(params.value || '') const nodeId = String(params.value || '')
return idLabelMap.get(nodeId) || nodeId return idLabelMap.get(nodeId) || nodeId
} }
@ -648,11 +655,11 @@ const pinnedTopRowData = computed(() => [
majorName: '', majorName: '',
hasCost: false, hasCost: false,
hasArea: false, hasArea: false,
amount: totalAmount.value, amount: null,
landArea: null, landArea: null,
benchmarkBudget: totalBenchmarkBudget.value, benchmarkBudget: null,
benchmarkBudgetBasic: totalBenchmarkBudgetBasic.value, benchmarkBudgetBasic: null,
benchmarkBudgetOptional: totalBenchmarkBudgetOptional.value, benchmarkBudgetOptional: null,
benchmarkBudgetBasicChecked: true, benchmarkBudgetBasicChecked: true,
benchmarkBudgetOptionalChecked: true, benchmarkBudgetOptionalChecked: true,
basicFormula: '', basicFormula: '',

View File

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, defineComponent, h, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue' import { computed, defineComponent, h, nextTick, onActivated, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import type { ComponentPublicInstance, PropType } from 'vue' import type { ComponentPublicInstance, PropType } from 'vue'
import { AgGridVue } from 'ag-grid-vue3' import { AgGridVue } from 'ag-grid-vue3'
import type { ColDef, GridOptions, ICellRendererParams } from 'ag-grid-community' import type { ColDef, GridOptions, ICellRendererParams } from 'ag-grid-community'
@ -9,7 +9,11 @@ import { AG_GRID_LOCALE_CN } from '@ag-grid-community/locale'
import { addNumbers } from '@/lib/decimal' import { addNumbers } from '@/lib/decimal'
import { parseNumberOrNull } from '@/lib/number' import { parseNumberOrNull } from '@/lib/number'
import { formatThousands, formatThousandsFlexible } from '@/lib/numberFormat' import { formatThousands, formatThousandsFlexible } from '@/lib/numberFormat'
import { getPricingMethodTotalsForService, getPricingMethodTotalsForServices } from '@/lib/pricingMethodTotals' import {
getPricingMethodTotalsForService,
getPricingMethodTotalsForServices,
type PricingMethodTotals
} from '@/lib/pricingMethodTotals'
import { ZXFW_RELOAD_SERVICE_KEY } from '@/lib/zxFwPricingSync' import { ZXFW_RELOAD_SERVICE_KEY } from '@/lib/zxFwPricingSync'
import { Pencil, Eraser, Trash2 } from 'lucide-vue-next' import { Pencil, Eraser, Trash2 } from 'lucide-vue-next'
import { import {
@ -21,18 +25,11 @@ import {
AlertDialogPortal, AlertDialogPortal,
AlertDialogRoot, AlertDialogRoot,
AlertDialogTitle, AlertDialogTitle,
DialogClose,
DialogContent,
DialogDescription,
DialogOverlay,
DialogPortal,
DialogRoot,
DialogTitle,
DialogTrigger
} from 'reka-ui' } from 'reka-ui'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { TooltipProvider } from '@/components/ui/tooltip' import { TooltipProvider } from '@/components/ui/tooltip'
import { getServiceDictEntries } from '@/sql' import { getServiceDictEntries, isIndustryEnabledByType,getIndustryTypeValue } from '@/sql'
import { useTabStore } from '@/pinia/tab' import { useTabStore } from '@/pinia/tab'
import { usePricingPaneReloadStore } from '@/pinia/pricingPaneReload' import { usePricingPaneReloadStore } from '@/pinia/pricingPaneReload'
import ServiceCheckboxSelector from '@/components/views/ServiceCheckboxSelector.vue' import ServiceCheckboxSelector from '@/components/views/ServiceCheckboxSelector.vue'
@ -41,6 +38,7 @@ interface ServiceItem {
id: string id: string
code: string code: string
name: string name: string
type: ServiceMethodType
} }
interface DetailRow { interface DetailRow {
@ -61,6 +59,17 @@ interface ZxFwState {
detailRows: DetailRow[] detailRows: DetailRow[]
} }
interface XmBaseInfoState {
projectIndustry?: string
}
interface ServiceMethodType {
scale?: boolean | null
onlyCostScale?: boolean | null
amount?: boolean | null
workDay?: boolean | null
}
const props = defineProps<{ const props = defineProps<{
contractId: string contractId: string
contractName?: string contractName?: string
@ -68,25 +77,66 @@ const props = defineProps<{
const tabStore = useTabStore() const tabStore = useTabStore()
const pricingPaneReloadStore = usePricingPaneReloadStore() const pricingPaneReloadStore = usePricingPaneReloadStore()
const DB_KEY = computed(() => `zxFW-${props.contractId}`) const DB_KEY = computed(() => `zxFW-${props.contractId}`)
const PROJECT_INFO_KEY = 'xm-base-info-v1'
const PRICING_CLEAR_SKIP_PREFIX = 'pricing-clear-skip:' const PRICING_CLEAR_SKIP_PREFIX = 'pricing-clear-skip:'
const PRICING_FORCE_DEFAULT_PREFIX = 'pricing-force-default:' const PRICING_FORCE_DEFAULT_PREFIX = 'pricing-force-default:'
const PRICING_CLEAR_SKIP_TTL_MS = 5000 const PRICING_CLEAR_SKIP_TTL_MS = 5000
const PRICING_TOTALS_OPTIONS = { excludeInvestmentCostAndAreaRows: true } as const
const projectIndustry = ref('')
type ServiceListItem = { code?: string; ref?: string; name: string; defCoe: number | null } type ServiceListItem = {
const serviceDict: ServiceItem[] = getServiceDictEntries() code?: string
ref?: string
name: string
defCoe: number | null
isRoad?: boolean
isRailway?: boolean
isWaterway?: boolean
scale?: boolean | null
onlyCostScale?: boolean | null
amount?: boolean | null
workDay?: boolean | null
}
const toNullableBoolean = (value: unknown): boolean | null =>
typeof value === 'boolean' ? value : null
const resolveMethodEnabled = (value: boolean | null | undefined, fallback = true) =>
typeof value === 'boolean' ? value : fallback
const defaultServiceMethodType = {
scale: true,
onlyCostScale: false,
amount: true,
workDay: true
}
const serviceDict = computed<ServiceItem[]>(() => {
const industry = projectIndustry.value
if (!industry) return []
const filteredEntries = getServiceDictEntries()
.map(({ id, item }) => ({ id, item: item as ServiceListItem })) .map(({ id, item }) => ({ id, item: item as ServiceListItem }))
.filter(({ item }) => { .filter(({ item }) => {
const itemCode = item?.code || item?.ref const itemCode = item?.code || item?.ref
return Boolean(itemCode && item?.name) && item.defCoe !== null return Boolean(itemCode && item?.name) && item.defCoe !== null && isIndustryEnabledByType(item, getIndustryTypeValue(industry))
}) })
.map(({ id, item }) => ({ return filteredEntries.map(({ id, item }) => ({
id, id,
code: item.code || item.ref || '', code: item.code || item.ref || '',
name: item.name name: item.name,
type: {
scale: toNullableBoolean(item.scale),
onlyCostScale: toNullableBoolean(item.onlyCostScale),
amount: toNullableBoolean(item.amount),
workDay: toNullableBoolean(item.workDay)
}
})) }))
})
const serviceById = new Map(serviceDict.map(item => [item.id, item])) const serviceById = computed(() => new Map(serviceDict.value.map(item => [item.id, item])))
const serviceIdByCode = new Map(serviceDict.map(item => [item.code, item.id])) const serviceIdByCode = computed(() => new Map(serviceDict.value.map(item => [item.code, item.id])))
const serviceIdSignature = computed(() => serviceDict.value.map(item => item.id).join('|'))
const fixedBudgetRow: Pick<DetailRow, 'id' | 'code' | 'name'> = { id: 'fixed-budget-c', code: 'C', name: '合同预算' } const fixedBudgetRow: Pick<DetailRow, 'id' | 'code' | 'name'> = { id: 'fixed-budget-c', code: 'C', name: '合同预算' }
const isFixedRow = (row?: DetailRow | null) => row?.id === fixedBudgetRow.id const isFixedRow = (row?: DetailRow | null) => row?.id === fixedBudgetRow.id
@ -167,7 +217,7 @@ const startDragAutoScroll = () => {
const selectedServiceText = computed(() => { const selectedServiceText = computed(() => {
if (selectedIds.value.length === 0) return '' if (selectedIds.value.length === 0) return ''
const names = selectedIds.value const names = selectedIds.value
.map(id => serviceById.get(id)?.name || '') .map(id => serviceById.value.get(id)?.name || '')
.filter(Boolean) .filter(Boolean)
if (names.length <= 2) return names.join('、') if (names.length <= 2) return names.join('、')
return `${names.slice(0, 2).join('、')}${names.length}` return `${names.slice(0, 2).join('、')}${names.length}`
@ -177,7 +227,7 @@ const pendingClearServiceName = computed(() => {
if (!pendingClearServiceId.value) return '' if (!pendingClearServiceId.value) return ''
const row = detailRows.value.find(item => item.id === pendingClearServiceId.value) const row = detailRows.value.find(item => item.id === pendingClearServiceId.value)
if (row) return `${row.code}${row.name}` if (row) return `${row.code}${row.name}`
const dict = serviceById.get(pendingClearServiceId.value) const dict = serviceById.value.get(pendingClearServiceId.value)
if (dict) return `${dict.code}${dict.name}` if (dict) return `${dict.code}${dict.name}`
return pendingClearServiceId.value return pendingClearServiceId.value
}) })
@ -186,7 +236,7 @@ const pendingDeleteServiceName = computed(() => {
if (!pendingDeleteServiceId.value) return '' if (!pendingDeleteServiceId.value) return ''
const row = detailRows.value.find(item => item.id === pendingDeleteServiceId.value) const row = detailRows.value.find(item => item.id === pendingDeleteServiceId.value)
if (row) return `${row.code}${row.name}` if (row) return `${row.code}${row.name}`
const dict = serviceById.get(pendingDeleteServiceId.value) const dict = serviceById.value.get(pendingDeleteServiceId.value)
if (dict) return `${dict.code}${dict.name}` if (dict) return `${dict.code}${dict.name}`
return pendingDeleteServiceId.value return pendingDeleteServiceId.value
}) })
@ -241,8 +291,8 @@ const confirmDeleteRow = async () => {
const filteredServiceDict = computed(() => { const filteredServiceDict = computed(() => {
const keyword = pickerSearch.value.trim() const keyword = pickerSearch.value.trim()
if (!keyword) return serviceDict if (!keyword) return serviceDict.value
return serviceDict.filter(item => item.code.includes(keyword) || item.name.includes(keyword)) return serviceDict.value.filter(item => item.code.includes(keyword) || item.name.includes(keyword))
}) })
const dragRectStyle = computed(() => { const dragRectStyle = computed(() => {
@ -265,6 +315,40 @@ const numericParser = (newValue: any): number | null => {
const valueOrZero = (v: number | null | undefined) => (typeof v === 'number' ? v : 0) const valueOrZero = (v: number | null | undefined) => (typeof v === 'number' ? v : 0)
const getServiceMethodTypeById = (serviceId: string) => {
const type = serviceById.value.get(serviceId)?.type
const scale = resolveMethodEnabled(type?.scale, defaultServiceMethodType.scale)
const onlyCostScale = resolveMethodEnabled(type?.onlyCostScale, defaultServiceMethodType.onlyCostScale)
const amount = resolveMethodEnabled(type?.amount, defaultServiceMethodType.amount)
const workDay = resolveMethodEnabled(type?.workDay, defaultServiceMethodType.workDay)
return { scale, onlyCostScale, amount, workDay }
}
const sanitizePricingTotalsByService = (serviceId: string, totals: PricingMethodTotals): PricingMethodTotals => {
const methodType = getServiceMethodTypeById(serviceId)
const isScaleEnabled = methodType.scale
const isLandScaleEnabled = isScaleEnabled && !methodType.onlyCostScale
return {
investScale: isScaleEnabled ? totals.investScale : null,
landScale: isLandScaleEnabled ? totals.landScale : null,
workload: methodType.amount ? totals.workload : null,
hourly: methodType.workDay ? totals.hourly : null
}
}
const sanitizePricingFieldsByService = (
serviceId: string,
values: Pick<DetailRow, 'investScale' | 'landScale' | 'workload' | 'hourly'>
) => {
const sanitized = sanitizePricingTotalsByService(serviceId, values)
return {
investScale: sanitized.investScale,
landScale: sanitized.landScale,
workload: sanitized.workload,
hourly: sanitized.hourly
}
}
const getMethodTotalFromRows = ( const getMethodTotalFromRows = (
rows: DetailRow[], rows: DetailRow[],
field: 'investScale' | 'landScale' | 'workload' | 'hourly' field: 'investScale' | 'landScale' | 'workload' | 'hourly'
@ -313,17 +397,19 @@ const clearRowValues = async (row: DetailRow) => {
await clearPricingPaneValues(row.id) await clearPricingPaneValues(row.id)
const totals = await getPricingMethodTotalsForService({ const totals = await getPricingMethodTotalsForService({
contractId: props.contractId, contractId: props.contractId,
serviceId: row.id serviceId: row.id,
options: PRICING_TOTALS_OPTIONS
}) })
const sanitizedTotals = sanitizePricingTotalsByService(row.id, totals)
const clearedRows = detailRows.value.map(item => const clearedRows = detailRows.value.map(item =>
item.id !== row.id item.id !== row.id
? item ? item
: { : {
...item, ...item,
investScale: totals.investScale, investScale: sanitizedTotals.investScale,
landScale: totals.landScale, landScale: sanitizedTotals.landScale,
workload: totals.workload, workload: sanitizedTotals.workload,
hourly: totals.hourly hourly: sanitizedTotals.hourly
} }
) )
const nextInvestScale = getMethodTotalFromRows(clearedRows, 'investScale') const nextInvestScale = getMethodTotalFromRows(clearedRows, 'investScale')
@ -346,6 +432,7 @@ const clearRowValues = async (row: DetailRow) => {
} }
const openEditTab = (row: DetailRow) => { const openEditTab = (row: DetailRow) => {
const serviceType = serviceById.value.get(row.id)?.type
tabStore.openTab({ tabStore.openTab({
id: `zxfw-edit-${props.contractId}-${row.id}`, id: `zxfw-edit-${props.contractId}-${row.id}`,
title: `服务编辑-${row.code}${row.name}`, title: `服务编辑-${row.code}${row.name}`,
@ -354,7 +441,8 @@ const openEditTab = (row: DetailRow) => {
contractId: props.contractId, contractId: props.contractId,
contractName: props.contractName || '', contractName: props.contractName || '',
serviceId: row.id, serviceId: row.id,
fwName: row.code + row.name fwName: row.code + row.name,
type: serviceType ? { ...serviceType } : undefined
} }
}) })
} }
@ -562,13 +650,15 @@ const fillPricingTotalsForServiceIds = async (serviceIds: string[]) => {
const totalsByServiceId = await getPricingMethodTotalsForServices({ const totalsByServiceId = await getPricingMethodTotalsForServices({
contractId: props.contractId, contractId: props.contractId,
serviceIds: targetIds serviceIds: targetIds,
options: PRICING_TOTALS_OPTIONS
}) })
const targetSet = new Set(targetIds.map(id => String(id))) const targetSet = new Set(targetIds.map(id => String(id)))
const nextRows = detailRows.value.map(row => { const nextRows = detailRows.value.map(row => {
if (isFixedRow(row) || !targetSet.has(String(row.id))) return row if (isFixedRow(row) || !targetSet.has(String(row.id))) return row
const totals = totalsByServiceId.get(String(row.id)) const totalsRaw = totalsByServiceId.get(String(row.id))
const totals = totalsRaw ? sanitizePricingTotalsByService(String(row.id), totalsRaw) : null
if (!totals) return row if (!totals) return row
return { return {
...row, ...row,
@ -585,29 +675,35 @@ const fillPricingTotalsForServiceIds = async (serviceIds: string[]) => {
const applySelection = (codes: string[]) => { const applySelection = (codes: string[]) => {
const prevSelectedSet = new Set(selectedIds.value) const prevSelectedSet = new Set(selectedIds.value)
const uniqueIds = Array.from(new Set(codes)).filter( const uniqueIds = Array.from(new Set(codes)).filter(
id => serviceById.has(id) && id !== fixedBudgetRow.id id => serviceById.value.has(id) && id !== fixedBudgetRow.id
) )
const existingMap = new Map(detailRows.value.map(row => [row.id, row])) const existingMap = new Map(detailRows.value.map(row => [row.id, row]))
const baseRows: DetailRow[] = uniqueIds const baseRows: DetailRow[] = uniqueIds
.map(id => { .map(id => {
const dictItem = serviceById.get(id) const dictItem = serviceById.value.get(id)
if (!dictItem) return null if (!dictItem) return null
const old = existingMap.get(id) const old = existingMap.get(id)
return { const nextValues = sanitizePricingFieldsByService(id, {
id: old?.id || id,
code: dictItem.code,
name: dictItem.name,
investScale: old?.investScale ?? null, investScale: old?.investScale ?? null,
landScale: old?.landScale ?? null, landScale: old?.landScale ?? null,
workload: old?.workload ?? null, workload: old?.workload ?? null,
hourly: old?.hourly ?? null hourly: old?.hourly ?? null
})
return {
id: old?.id || id,
code: dictItem.code,
name: dictItem.name,
investScale: nextValues.investScale,
landScale: nextValues.landScale,
workload: nextValues.workload,
hourly: nextValues.hourly
} }
}) })
.filter((row): row is DetailRow => Boolean(row)) .filter((row): row is DetailRow => Boolean(row))
const orderMap = new Map(serviceDict.map((item, index) => [item.id, index])) const orderMap = new Map(serviceDict.value.map((item, index) => [item.id, index]))
baseRows.sort((a, b) => (orderMap.get(a.id) || 0) - (orderMap.get(b.id) || 0)) baseRows.sort((a, b) => (orderMap.get(a.id) || 0) - (orderMap.get(b.id) || 0))
const fixedOld = existingMap.get(fixedBudgetRow.id) const fixedOld = existingMap.get(fixedBudgetRow.id)
@ -730,6 +826,7 @@ const applyDragSelectionByRect = () => {
} }
pickerTempIds.value = serviceDict pickerTempIds.value = serviceDict
.value
.map(item => item.id) .map(item => item.id)
.filter(id => nextSelectedSet.has(id)) .filter(id => nextSelectedSet.has(id))
} }
@ -797,19 +894,25 @@ const loadFromIndexedDB = async () => {
} }
const idsFromStorage = data.selectedIds const idsFromStorage = data.selectedIds
|| (data.selectedCodes || []).map(code => serviceIdByCode.get(code)).filter((id): id is string => Boolean(id)) || (data.selectedCodes || []).map(code => serviceIdByCode.value.get(code)).filter((id): id is string => Boolean(id))
applySelection(idsFromStorage) applySelection(idsFromStorage)
const savedRowMap = new Map((data.detailRows || []).map(row => [row.id, row])) const savedRowMap = new Map((data.detailRows || []).map(row => [row.id, row]))
detailRows.value = detailRows.value.map(row => { detailRows.value = detailRows.value.map(row => {
const old = savedRowMap.get(row.id) const old = savedRowMap.get(row.id)
if (!old) return row if (!old) return row
return { const nextValues = sanitizePricingFieldsByService(row.id, {
...row,
investScale: typeof old.investScale === 'number' ? old.investScale : null, investScale: typeof old.investScale === 'number' ? old.investScale : null,
landScale: typeof old.landScale === 'number' ? old.landScale : null, landScale: typeof old.landScale === 'number' ? old.landScale : null,
workload: typeof old.workload === 'number' ? old.workload : null, workload: typeof old.workload === 'number' ? old.workload : null,
hourly: typeof old.hourly === 'number' ? old.hourly : null hourly: typeof old.hourly === 'number' ? old.hourly : null
})
return {
...row,
investScale: nextValues.investScale,
landScale: nextValues.landScale,
workload: nextValues.workload,
hourly: nextValues.hourly
} }
}) })
detailRows.value = applyFixedRowTotals(detailRows.value) detailRows.value = applyFixedRowTotals(detailRows.value)
@ -820,6 +923,17 @@ const loadFromIndexedDB = async () => {
} }
} }
const loadProjectIndustry = async () => {
try {
const data = await localforage.getItem<XmBaseInfoState>(PROJECT_INFO_KEY)
projectIndustry.value =
typeof data?.projectIndustry === 'string' ? data.projectIndustry.trim() : ''
} catch (error) {
console.error('loadProjectIndustry failed:', error)
projectIndustry.value = ''
}
}
watch( watch(
() => pricingPaneReloadStore.getReloadVersion(props.contractId, ZXFW_RELOAD_SERVICE_KEY), () => pricingPaneReloadStore.getReloadVersion(props.contractId, ZXFW_RELOAD_SERVICE_KEY),
(nextVersion, prevVersion) => { (nextVersion, prevVersion) => {
@ -828,6 +942,15 @@ watch(
} }
) )
watch(serviceIdSignature, () => {
const availableIds = new Set(serviceDict.value.map(item => item.id))
const nextSelectedIds = selectedIds.value.filter(id => availableIds.has(id))
if (nextSelectedIds.length !== selectedIds.value.length) {
applySelection(nextSelectedIds)
void saveToIndexedDB()
}
})
let gridPersistTimer: ReturnType<typeof setTimeout> | null = null let gridPersistTimer: ReturnType<typeof setTimeout> | null = null
const handleCellValueChanged = () => { const handleCellValueChanged = () => {
if (gridPersistTimer) clearTimeout(gridPersistTimer) if (gridPersistTimer) clearTimeout(gridPersistTimer)
@ -837,6 +960,12 @@ const handleCellValueChanged = () => {
} }
onMounted(async () => { onMounted(async () => {
await loadProjectIndustry()
await loadFromIndexedDB()
})
onActivated(async () => {
await loadProjectIndustry()
await loadFromIndexedDB() await loadFromIndexedDB()
}) })

View File

@ -53,10 +53,14 @@ interface HourlyRow {
interface MajorLite { interface MajorLite {
code: string code: string
defCoe: number | null defCoe: number | null
hasCost?: boolean
hasArea?: boolean
industryId?: string | number | null
} }
interface ServiceLite { interface ServiceLite {
defCoe: number | null defCoe: number | null
onlyCostScale?: boolean | null
} }
interface TaskLite { interface TaskLite {
@ -70,6 +74,10 @@ interface ExpertLite {
manageCoe: number | null manageCoe: number | null
} }
interface XmBaseInfoState {
projectIndustry?: string
}
export interface PricingMethodTotals { export interface PricingMethodTotals {
investScale: number | null investScale: number | null
landScale: number | null landScale: number | null
@ -77,6 +85,12 @@ export interface PricingMethodTotals {
hourly: number | null hourly: number | null
} }
interface PricingMethodTotalsOptions {
excludeInvestmentCostAndAreaRows?: boolean
}
const ONLY_COST_SCALE_ROW_ID = '__only-cost-scale-total__'
const hasOwn = (obj: unknown, key: string) => const hasOwn = (obj: unknown, key: string) =>
Object.prototype.hasOwnProperty.call(obj || {}, key) Object.prototype.hasOwnProperty.call(obj || {}, key)
@ -93,6 +107,11 @@ const getDefaultConsultCategoryFactor = (serviceId: string | number) => {
return toFiniteNumberOrNull(service?.defCoe) return toFiniteNumberOrNull(service?.defCoe)
} }
const isOnlyCostScaleService = (serviceId: string | number) => {
const service = (getServiceDictById() as Record<string, ServiceLite | undefined>)[String(serviceId)]
return service?.onlyCostScale === true
}
const majorById = new Map(getMajorDictEntries().map(({ id, item }) => [id, item as MajorLite])) const majorById = new Map(getMajorDictEntries().map(({ id, item }) => [id, item as MajorLite]))
const majorIdAliasMap = getMajorIdAliasMap() const majorIdAliasMap = getMajorIdAliasMap()
@ -102,6 +121,27 @@ const getDefaultMajorFactorById = (id: string) => {
return toFiniteNumberOrNull(major?.defCoe) return toFiniteNumberOrNull(major?.defCoe)
} }
const isDualScaleMajorById = (id: string) => {
const resolvedId = majorById.has(id) ? id : majorIdAliasMap.get(id) || id
const major = majorById.get(resolvedId)
if (!major) return false
const hasCost = major.hasCost !== false
const hasArea = major.hasArea !== false
return hasCost && hasArea
}
const getIndustryMajorEntryByIndustryId = (industryId: string | null | undefined) => {
const key = String(industryId || '').trim()
if (!key) return null
for (const [id, item] of majorById.entries()) {
const majorIndustryId = String(item?.industryId ?? '').trim()
if (majorIndustryId === key && !String(item?.code || '').includes('-')) {
return { id, item }
}
}
return null
}
const resolveFactorValue = ( const resolveFactorValue = (
row: { budgetValue?: number | null; standardFactor?: number | null } | undefined, row: { budgetValue?: number | null; standardFactor?: number | null } | undefined,
fallback: number | null fallback: number | null
@ -227,6 +267,38 @@ const getInvestmentBudgetFee = (row: ScaleRow) => {
}) })
} }
const getOnlyCostScaleBudgetFee = (
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 getScaleBudgetFee({
benchmarkBudget: getBenchmarkBudgetByAmount(totalAmount),
majorFactor,
consultCategoryFactor,
workStageFactor,
workRatio
})
}
const getLandBudgetFee = (row: ScaleRow) => { const getLandBudgetFee = (row: ScaleRow) => {
return getScaleBudgetFee({ return getScaleBudgetFee({
benchmarkBudget: getBenchmarkBudgetByLandArea(row.landArea), benchmarkBudget: getBenchmarkBudgetByLandArea(row.landArea),
@ -373,30 +445,46 @@ const resolveScaleRows = (
export const getPricingMethodTotalsForService = async (params: { export const getPricingMethodTotalsForService = async (params: {
contractId: string contractId: string
serviceId: string | number serviceId: string | number
options?: PricingMethodTotalsOptions
}): Promise<PricingMethodTotals> => { }): Promise<PricingMethodTotals> => {
const serviceId = String(params.serviceId) const serviceId = String(params.serviceId)
const htDbKey = `ht-info-v3-${params.contractId}` const htDbKey = `ht-info-v3-${params.contractId}`
const consultFactorDbKey = `ht-consult-category-factor-v1-${params.contractId}` const consultFactorDbKey = `ht-consult-category-factor-v1-${params.contractId}`
const majorFactorDbKey = `ht-major-factor-v1-${params.contractId}` const majorFactorDbKey = `ht-major-factor-v1-${params.contractId}`
const baseInfoDbKey = 'xm-base-info-v1'
const investDbKey = `tzGMF-${params.contractId}-${serviceId}` const investDbKey = `tzGMF-${params.contractId}-${serviceId}`
const landDbKey = `ydGMF-${params.contractId}-${serviceId}` const landDbKey = `ydGMF-${params.contractId}-${serviceId}`
const workloadDbKey = `gzlF-${params.contractId}-${serviceId}` const workloadDbKey = `gzlF-${params.contractId}-${serviceId}`
const hourlyDbKey = `hourlyPricing-${params.contractId}-${serviceId}` const hourlyDbKey = `hourlyPricing-${params.contractId}-${serviceId}`
const [investData, landData, workloadData, hourlyData, htData, consultFactorData, majorFactorData] = await Promise.all([ const [investData, landData, workloadData, hourlyData, htData, consultFactorData, majorFactorData, baseInfo] = await Promise.all([
localforage.getItem<StoredDetailRowsState>(investDbKey), localforage.getItem<StoredDetailRowsState>(investDbKey),
localforage.getItem<StoredDetailRowsState>(landDbKey), localforage.getItem<StoredDetailRowsState>(landDbKey),
localforage.getItem<StoredDetailRowsState>(workloadDbKey), localforage.getItem<StoredDetailRowsState>(workloadDbKey),
localforage.getItem<StoredDetailRowsState>(hourlyDbKey), localforage.getItem<StoredDetailRowsState>(hourlyDbKey),
localforage.getItem<StoredDetailRowsState>(htDbKey), localforage.getItem<StoredDetailRowsState>(htDbKey),
localforage.getItem<StoredFactorState>(consultFactorDbKey), localforage.getItem<StoredFactorState>(consultFactorDbKey),
localforage.getItem<StoredFactorState>(majorFactorDbKey) localforage.getItem<StoredFactorState>(majorFactorDbKey),
localforage.getItem<XmBaseInfoState>(baseInfoDbKey)
]) ])
const consultCategoryFactorMap = buildConsultCategoryFactorMap(consultFactorData) const consultCategoryFactorMap = buildConsultCategoryFactorMap(consultFactorData)
const majorFactorMap = buildMajorFactorMap(majorFactorData) const majorFactorMap = buildMajorFactorMap(majorFactorData)
const onlyCostScale = isOnlyCostScaleService(serviceId)
const industryId = typeof baseInfo?.projectIndustry === 'string' ? baseInfo.projectIndustry.trim() : ''
// 优先使用对应计费页的数据;不存在时回退合同段规模信息,再回退默认字典行。 // 优先使用对应计费页的数据;不存在时回退合同段规模信息,再回退默认字典行。
const excludeInvestmentCostAndAreaRows = params.options?.excludeInvestmentCostAndAreaRows === true
const investScale = onlyCostScale
? getOnlyCostScaleBudgetFee(
serviceId,
(investData?.detailRows as Array<Record<string, unknown>> | undefined) ||
(htData?.detailRows as Array<Record<string, unknown>> | undefined),
consultCategoryFactorMap,
majorFactorMap,
industryId
)
: (() => {
const investRows = resolveScaleRows( const investRows = resolveScaleRows(
serviceId, serviceId,
investData, investData,
@ -404,7 +492,11 @@ export const getPricingMethodTotalsForService = async (params: {
consultCategoryFactorMap, consultCategoryFactorMap,
majorFactorMap majorFactorMap
) )
const investScale = sumByNumber(investRows, row => getInvestmentBudgetFee(row)) return sumByNumber(investRows, row => {
if (excludeInvestmentCostAndAreaRows && isDualScaleMajorById(row.id)) return null
return getInvestmentBudgetFee(row)
})
})()
const landRows = resolveScaleRows( const landRows = resolveScaleRows(
serviceId, serviceId,
@ -443,13 +535,15 @@ export const getPricingMethodTotalsForService = async (params: {
export const getPricingMethodTotalsForServices = async (params: { export const getPricingMethodTotalsForServices = async (params: {
contractId: string contractId: string
serviceIds: Array<string | number> serviceIds: Array<string | number>
options?: PricingMethodTotalsOptions
}) => { }) => {
const result = new Map<string, PricingMethodTotals>() const result = new Map<string, PricingMethodTotals>()
await Promise.all( await Promise.all(
params.serviceIds.map(async serviceId => { params.serviceIds.map(async serviceId => {
const totals = await getPricingMethodTotalsForService({ const totals = await getPricingMethodTotalsForService({
contractId: params.contractId, contractId: params.contractId,
serviceId serviceId,
options: params.options
}) })
result.set(String(serviceId), totals) result.set(String(serviceId), totals)
}) })

View File

@ -11,6 +11,12 @@ const toFiniteNumber = (value: unknown) => {
const num = Number(value) const num = Number(value)
return Number.isFinite(num) ? num : 0 return Number.isFinite(num) ? num : 0
} }
export const industryTypeList = [
{id:'0', name: '公路工程', type: 'isRoad' },
{id:'1', name: '铁路工程', type: 'isRailway' },
{id:'2', name: '水运工程', type: 'isWaterway' }
] as const
export const majorList = { export const majorList = {
0: { code: 'E1', name: '交通运输工程通用专业',hideInIndustrySelector: true, maxCoe: null, minCoe: null, defCoe: null, desc: '', isRoad: true, isRailway: true, isWaterway: true, order: 1, hasCost: false, hasArea: false }, 0: { code: 'E1', name: '交通运输工程通用专业',hideInIndustrySelector: true, maxCoe: null, minCoe: null, defCoe: null, desc: '', isRoad: true, isRailway: true, isWaterway: true, order: 1, hasCost: false, hasArea: false },
1: { code: 'E1-1', name: '征地(用海)补偿', maxCoe: null, minCoe: null, defCoe: 1, desc: '适用于交通建设项目征地(用海)补偿的施工图预算、招标工程量清单及清单预算(或最高投标限价)、清理概算(仅限铁路工程)、合同(工程)结算和造价鉴定、计算工程量、工程变更费用咨询、工程成本测(核)算', isRoad: true, isRailway: true, isWaterway: true, order: 2, hasCost: true, hasArea: true }, 1: { code: 'E1-1', name: '征地(用海)补偿', maxCoe: null, minCoe: null, defCoe: 1, desc: '适用于交通建设项目征地(用海)补偿的施工图预算、招标工程量清单及清单预算(或最高投标限价)、清理概算(仅限铁路工程)、合同(工程)结算和造价鉴定、计算工程量、工程变更费用咨询、工程成本测(核)算', isRoad: true, isRailway: true, isWaterway: true, order: 2, hasCost: true, hasArea: true },
@ -19,7 +25,7 @@ export const majorList = {
4: { code: 'E1-4', name: '工程建设其他费', maxCoe: null, minCoe: null, defCoe: 1, desc: '适用于交通建设项目的工程建设其他费的施工图预算、招标工程量清单及清单预算(或最高投标限价)、清理概算(仅限铁路工程)和造价鉴定、计算工程量、工程变更费用咨询、工程成本测(核)算', isRoad: true, isRailway: true, isWaterway: true, order: 5, hasCost: true, hasArea: false }, 4: { code: 'E1-4', name: '工程建设其他费', maxCoe: null, minCoe: null, defCoe: 1, desc: '适用于交通建设项目的工程建设其他费的施工图预算、招标工程量清单及清单预算(或最高投标限价)、清理概算(仅限铁路工程)和造价鉴定、计算工程量、工程变更费用咨询、工程成本测(核)算', isRoad: true, isRailway: true, isWaterway: true, order: 5, hasCost: true, hasArea: false },
5: { code: 'E1-5', name: '预备费', maxCoe: null, minCoe: null, defCoe: 1, desc: '', isRoad: true, isRailway: true, isWaterway: true, order: 6, hasCost: true, hasArea: false }, 5: { code: 'E1-5', name: '预备费', maxCoe: null, minCoe: null, defCoe: 1, desc: '', isRoad: true, isRailway: true, isWaterway: true, order: 6, hasCost: true, hasArea: false },
6: { code: 'E1-6', name: '建设期贷款利息', maxCoe: null, minCoe: null, defCoe: 1, desc: '', isRoad: true, isRailway: true, isWaterway: true, order: 7, hasCost: true, hasArea: false }, 6: { code: 'E1-6', name: '建设期贷款利息', maxCoe: null, minCoe: null, defCoe: 1, desc: '', isRoad: true, isRailway: true, isWaterway: true, order: 7, hasCost: true, hasArea: false },
7: { code: 'E2', name: '公路工程专业', maxCoe: null, minCoe: null, defCoe: 1, desc: '适用于公路工程的全过程造价咨询、分阶段造价咨询、投资估算、初步设计概算、竣工决算和调整估算、调整概算(含征地拆迁和工程建设其他费)', isRoad: true, isRailway: false, isWaterway: false, order: 8, hasCost: false, hasArea: false }, 7: { code: 'E2', name: '公路工程专业', maxCoe: null, minCoe: null, defCoe: 1, desc: '适用于公路工程的全过程造价咨询、分阶段造价咨询、投资估算、初步设计概算、竣工决算和调整估算、调整概算(含征地拆迁和工程建设其他费)', isRoad: true, isRailway: false, isWaterway: false, order: 8, hasCost: false, hasArea: false ,industryId:'0' },
8: { code: 'E2-1', name: '临时工程', maxCoe: null, minCoe: null, defCoe: 1, desc: '适用于临时工程专业的施工图预算、招标工程量清单及清单预算(或最高投标限价)、合同(工程)结算和造价鉴定、计算工程量、工程变更费用咨询、工程成本测(核)算', isRoad: true, isRailway: false, isWaterway: false, order: 9, hasCost: true, hasArea: false }, 8: { code: 'E2-1', name: '临时工程', maxCoe: null, minCoe: null, defCoe: 1, desc: '适用于临时工程专业的施工图预算、招标工程量清单及清单预算(或最高投标限价)、合同(工程)结算和造价鉴定、计算工程量、工程变更费用咨询、工程成本测(核)算', isRoad: true, isRailway: false, isWaterway: false, order: 9, hasCost: true, hasArea: false },
9: { code: 'E2-2', name: '路基工程', maxCoe: null, minCoe: null, defCoe: 1.2, desc: '适用于路基工程专业的施工图预算、招标工程量清单及清单预算(或最高投标限价)、合同(工程)结算和造价鉴定、计算工程量、工程变更费用咨询、工程成本测(核)算', isRoad: true, isRailway: false, isWaterway: false, order: 10, hasCost: true, hasArea: false }, 9: { code: 'E2-2', name: '路基工程', maxCoe: null, minCoe: null, defCoe: 1.2, desc: '适用于路基工程专业的施工图预算、招标工程量清单及清单预算(或最高投标限价)、合同(工程)结算和造价鉴定、计算工程量、工程变更费用咨询、工程成本测(核)算', isRoad: true, isRailway: false, isWaterway: false, order: 10, hasCost: true, hasArea: false },
10: { code: 'E2-3', name: '路面工程', maxCoe: null, minCoe: null, defCoe: 0.8, desc: '适用于路面工程专业的施工图预算、招标工程量清单及清单预算(或最高投标限价)、合同(工程)结算和造价鉴定、计算工程量、工程变更费用咨询、工程成本测(核)算', isRoad: true, isRailway: false, isWaterway: false, order: 11, hasCost: true, hasArea: false }, 10: { code: 'E2-3', name: '路面工程', maxCoe: null, minCoe: null, defCoe: 0.8, desc: '适用于路面工程专业的施工图预算、招标工程量清单及清单预算(或最高投标限价)、合同(工程)结算和造价鉴定、计算工程量、工程变更费用咨询、工程成本测(核)算', isRoad: true, isRailway: false, isWaterway: false, order: 11, hasCost: true, hasArea: false },
@ -30,7 +36,7 @@ export const majorList = {
15: { code: 'E2-8', name: '交通安全设施工程', maxCoe: null, minCoe: null, defCoe: 1.2, desc: '适用于交通安全设施工程专业的施工图预算、招标工程量清单及清单预算(或最高投标限价)和造价鉴定、计算工程量、工程变更费用咨询、工程成本测(核)算', isRoad: true, isRailway: false, isWaterway: false, order: 16, hasCost: true, hasArea: false }, 15: { code: 'E2-8', name: '交通安全设施工程', maxCoe: null, minCoe: null, defCoe: 1.2, desc: '适用于交通安全设施工程专业的施工图预算、招标工程量清单及清单预算(或最高投标限价)和造价鉴定、计算工程量、工程变更费用咨询、工程成本测(核)算', isRoad: true, isRailway: false, isWaterway: false, order: 16, hasCost: true, hasArea: false },
16: { code: 'E2-9', name: '绿化及环境保护工程', maxCoe: null, minCoe: null, defCoe: 1.2, desc: '适用于绿化工程专业的施工图预算、招标工程量清单及清单预算(或最高投标限价)和造价鉴定、计算工程量、工程变更费用咨询、工程成本测(核)算', isRoad: true, isRailway: false, isWaterway: false, order: 17, hasCost: true, hasArea: false }, 16: { code: 'E2-9', name: '绿化及环境保护工程', maxCoe: null, minCoe: null, defCoe: 1.2, desc: '适用于绿化工程专业的施工图预算、招标工程量清单及清单预算(或最高投标限价)和造价鉴定、计算工程量、工程变更费用咨询、工程成本测(核)算', isRoad: true, isRailway: false, isWaterway: false, order: 17, hasCost: true, hasArea: false },
17: { code: 'E2-10', name: '房建工程', maxCoe: null, minCoe: null, defCoe: 2.5, desc: '适用于房建工程专业的施工图预算、招标工程量清单及清单预算(或最高投标限价)、合同(工程)结算和造价鉴定、计算工程量、工程变更费用咨询、工程成本测(核)算', isRoad: true, isRailway: false, isWaterway: false, order: 18, hasCost: true, hasArea: false }, 17: { code: 'E2-10', name: '房建工程', maxCoe: null, minCoe: null, defCoe: 2.5, desc: '适用于房建工程专业的施工图预算、招标工程量清单及清单预算(或最高投标限价)、合同(工程)结算和造价鉴定、计算工程量、工程变更费用咨询、工程成本测(核)算', isRoad: true, isRailway: false, isWaterway: false, order: 18, hasCost: true, hasArea: false },
18: { code: 'E3', name: '铁路工程专业', maxCoe: null, minCoe: null, defCoe: 1, desc: '适用于铁路工程的投资估算、初步设计概算、清理概算、竣工决算和调整估算、调整概算(含征地拆迁和工程建设其他费)', isRoad: false, isRailway: true, isWaterway: false, order: 19, hasCost: false, hasArea: false }, 18: { code: 'E3', name: '铁路工程专业', maxCoe: null, minCoe: null, defCoe: 1, desc: '适用于铁路工程的投资估算、初步设计概算、清理概算、竣工决算和调整估算、调整概算(含征地拆迁和工程建设其他费)', isRoad: false, isRailway: true, isWaterway: false, order: 19, hasCost: false, hasArea: false ,industryId:'1' },
19: { code: 'E3-1', name: '大型临时设施和过渡工程', maxCoe: null, minCoe: null, defCoe: 1, desc: '适用于大型临时设施和过渡工程专业的施工图预算、招标工程量清单及清单预算(或最高投标限价)、合同(工程)结算和造价鉴定、计算工程量、工程变更费用咨询、工程成本测(核)算', isRoad: false, isRailway: true, isWaterway: false, order: 20, hasCost: true, hasArea: false }, 19: { code: 'E3-1', name: '大型临时设施和过渡工程', maxCoe: null, minCoe: null, defCoe: 1, desc: '适用于大型临时设施和过渡工程专业的施工图预算、招标工程量清单及清单预算(或最高投标限价)、合同(工程)结算和造价鉴定、计算工程量、工程变更费用咨询、工程成本测(核)算', isRoad: false, isRailway: true, isWaterway: false, order: 20, hasCost: true, hasArea: false },
20: { code: 'E3-2', name: '路基工程', maxCoe: null, minCoe: null, defCoe: 1.2, desc: '适用于路基工程专业的施工图预算、招标工程量清单及清单预算(或最高投标限价)、合同(工程)结算和造价鉴定、计算工程量、工程变更费用咨询、工程成本测(核)算', isRoad: false, isRailway: true, isWaterway: false, order: 21, hasCost: true, hasArea: false }, 20: { code: 'E3-2', name: '路基工程', maxCoe: null, minCoe: null, defCoe: 1.2, desc: '适用于路基工程专业的施工图预算、招标工程量清单及清单预算(或最高投标限价)、合同(工程)结算和造价鉴定、计算工程量、工程变更费用咨询、工程成本测(核)算', isRoad: false, isRailway: true, isWaterway: false, order: 21, hasCost: true, hasArea: false },
21: { code: 'E3-3', name: '桥涵工程', maxCoe: null, minCoe: null, defCoe: 0.9, desc: '适用于桥涵工程专业的施工图预算、招标工程量清单及清单预算(或最高投标限价)、合同(工程)结算和造价鉴定、计算工程量、工程变更费用咨询、工程成本测(核)算', isRoad: false, isRailway: true, isWaterway: false, order: 22, hasCost: true, hasArea: false }, 21: { code: 'E3-3', name: '桥涵工程', maxCoe: null, minCoe: null, defCoe: 0.9, desc: '适用于桥涵工程专业的施工图预算、招标工程量清单及清单预算(或最高投标限价)、合同(工程)结算和造价鉴定、计算工程量、工程变更费用咨询、工程成本测(核)算', isRoad: false, isRailway: true, isWaterway: false, order: 22, hasCost: true, hasArea: false },
@ -40,7 +46,7 @@ export const majorList = {
25: { code: 'E3-7', name: '电力及电力牵引供电工程', maxCoe: null, minCoe: null, defCoe: 1.5, desc: '适用于电力及电力牵引供电工程专业的施工图预算、招标工程量清单及清单预算(或最高投标限价)、合同(工程)结算和造价鉴定、计算工程量、工程变更费用咨询、工程成本测(核)算', isRoad: false, isRailway: true, isWaterway: false, order: 26, hasCost: true, hasArea: false }, 25: { code: 'E3-7', name: '电力及电力牵引供电工程', maxCoe: null, minCoe: null, defCoe: 1.5, desc: '适用于电力及电力牵引供电工程专业的施工图预算、招标工程量清单及清单预算(或最高投标限价)、合同(工程)结算和造价鉴定、计算工程量、工程变更费用咨询、工程成本测(核)算', isRoad: false, isRailway: true, isWaterway: false, order: 26, hasCost: true, hasArea: false },
26: { code: 'E3-8', name: '房建工程(房屋建筑及附属工程)', maxCoe: null, minCoe: null, defCoe: 2.5, desc: '适用于房屋建筑及附属工程专业的施工图预算、招标工程量清单及清单预算(或最高投标限价)、合同(工程)结算和造价鉴定、计算工程量、工程变更费用咨询、工程成本测(核)算', isRoad: false, isRailway: true, isWaterway: false, order: 27, hasCost: true, hasArea: false }, 26: { code: 'E3-8', name: '房建工程(房屋建筑及附属工程)', maxCoe: null, minCoe: null, defCoe: 2.5, desc: '适用于房屋建筑及附属工程专业的施工图预算、招标工程量清单及清单预算(或最高投标限价)、合同(工程)结算和造价鉴定、计算工程量、工程变更费用咨询、工程成本测(核)算', isRoad: false, isRailway: true, isWaterway: false, order: 27, hasCost: true, hasArea: false },
27: { code: 'E3-9', name: '装饰装修工程', maxCoe: null, minCoe: null, defCoe: 2.7, desc: '适用于装饰装修工程专业的施工图预算、招标工程量清单及清单预算(或最高投标限价)、合同(工程)结算和造价鉴定、计算工程量、工程变更费用咨询、工程成本测(核)算', isRoad: false, isRailway: true, isWaterway: false, order: 28, hasCost: true, hasArea: false }, 27: { code: 'E3-9', name: '装饰装修工程', maxCoe: null, minCoe: null, defCoe: 2.7, desc: '适用于装饰装修工程专业的施工图预算、招标工程量清单及清单预算(或最高投标限价)、合同(工程)结算和造价鉴定、计算工程量、工程变更费用咨询、工程成本测(核)算', isRoad: false, isRailway: true, isWaterway: false, order: 28, hasCost: true, hasArea: false },
28: { code: 'E4', name: '水运工程专业', maxCoe: null, minCoe: null, defCoe: 1, desc: '适用于水运工程的投资估算、初步设计概算、竣工决算和调整估算、调整概算(含征地拆迁和工程建设其他费)', isRoad: false, isRailway: false, isWaterway: true, order: 29, hasCost: false, hasArea: false }, 28: { code: 'E4', name: '水运工程专业', maxCoe: null, minCoe: null, defCoe: 1, desc: '适用于水运工程的投资估算、初步设计概算、竣工决算和调整估算、调整概算(含征地拆迁和工程建设其他费)', isRoad: false, isRailway: false, isWaterway: true, order: 29, hasCost: false, hasArea: false ,industryId:'2' },
29: { code: 'E4-1', name: '临时工程', maxCoe: null, minCoe: null, defCoe: 1.1, desc: '适用于临时工程专业的施工图预算、招标工程量清单及清单预算(或最高投标限价)、合同(工程)结算、竣工决算和造价鉴定、计算工程量、工程变更费用咨询、工程成本测(核)算', isRoad: false, isRailway: false, isWaterway: true, order: 30, hasCost: true, hasArea: false }, 29: { code: 'E4-1', name: '临时工程', maxCoe: null, minCoe: null, defCoe: 1.1, desc: '适用于临时工程专业的施工图预算、招标工程量清单及清单预算(或最高投标限价)、合同(工程)结算、竣工决算和造价鉴定、计算工程量、工程变更费用咨询、工程成本测(核)算', isRoad: false, isRailway: false, isWaterway: true, order: 30, hasCost: true, hasArea: false },
30: { code: 'E4-2', name: '土建工程', maxCoe: null, minCoe: null, defCoe: 1, desc: '适用于土建工程专业的施工图预算、招标工程量清单及清单预算(或最高投标限价)、合同(工程)结算、竣工决算和造价鉴定、计算工程量、工程变更费用咨询、工程成本测(核)算', isRoad: false, isRailway: false, isWaterway: true, order: 31, hasCost: true, hasArea: false }, 30: { code: 'E4-2', name: '土建工程', maxCoe: null, minCoe: null, defCoe: 1, desc: '适用于土建工程专业的施工图预算、招标工程量清单及清单预算(或最高投标限价)、合同(工程)结算、竣工决算和造价鉴定、计算工程量、工程变更费用咨询、工程成本测(核)算', isRoad: false, isRailway: false, isWaterway: true, order: 31, hasCost: true, hasArea: false },
31: { code: 'E4-3', name: '机电与金属结构工程', maxCoe: null, minCoe: null, defCoe: 1.5, desc: '适用于机电与金属结构专业的施工图预算、招标工程量清单及清单预算(或最高投标限价)、合同(工程)结算和造价鉴定、计算工程量、工程变更费用咨询、工程成本测(核)算', isRoad: false, isRailway: false, isWaterway: true, order: 32, hasCost: true, hasArea: false }, 31: { code: 'E4-3', name: '机电与金属结构工程', maxCoe: null, minCoe: null, defCoe: 1.5, desc: '适用于机电与金属结构专业的施工图预算、招标工程量清单及清单预算(或最高投标限价)、合同(工程)结算和造价鉴定、计算工程量、工程变更费用咨询、工程成本测(核)算', isRoad: false, isRailway: false, isWaterway: true, order: 32, hasCost: true, hasArea: false },
@ -176,11 +182,7 @@ let areaScaleCal = [
export const industryTypeList = [
{id:'0', name: '公路工程', type: 'isRoad' },
{id:'1', name: '铁路工程', type: 'isRailway' },
{id:'2', name: '水运工程', type: 'isWaterway' }
] as const
export type IndustryType = (typeof industryTypeList)[number]['type'] export type IndustryType = (typeof industryTypeList)[number]['type']
type DictItem = Record<string, any> type DictItem = Record<string, any>
@ -314,12 +316,6 @@ export const getMajorDictEntries = (): DictEntry[] => buildDictEntries(majorList
*/ */
export const getServiceDictEntries = (): DictEntry[] => buildDictEntries(serviceList as Record<string, any>) export const getServiceDictEntries = (): DictEntry[] => buildDictEntries(serviceList as Record<string, any>)
/**
* isRoad/isRailway/isWaterway
* @returns
*/
export const isDictItemInIndustryScope = (item: DictItem | undefined, industryCode: unknown): boolean =>
isIndustryEnabledByType(item, getIndustryTypeValue(industryCode))
/** /**
* ID -> * ID ->