Compare commits
No commits in common. "303f54bb71d2b429f9ae674e2416fac0b8aaa975" and "ad4e9cdee0a9aa013577b72c0b2a025bbc609e49" have entirely different histories.
303f54bb71
...
ad4e9cdee0
@ -123,23 +123,6 @@ 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',
|
||||||
|
|||||||
@ -1,55 +1,14 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onActivated, onMounted, ref } from 'vue'
|
import { serviceList } from '@/sql'
|
||||||
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 title="咨询分类系数明细" storage-key="xm-consult-category-factor-v1" :dict="filteredServiceDict"
|
<XmFactorGrid
|
||||||
:disable-budget-edit-when-standard-null="true" :exclude-notshow-by-zxflxs="true" />
|
title="咨询分类系数明细"
|
||||||
|
storage-key="xm-consult-category-factor-v1"
|
||||||
|
:dict="serviceList"
|
||||||
|
:disable-budget-edit-when-standard-null="true"
|
||||||
|
:exclude-notshow-by-zxflxs="true"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -5,29 +5,20 @@
|
|||||||
: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="defaultCategory"
|
default-category="investment-scale-method"
|
||||||
:categories="pricingCategories"
|
:categories="pricingCategories"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, defineAsyncComponent, defineComponent, h, markRaw, type Component } from 'vue'
|
import { 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 {
|
||||||
@ -36,22 +27,6 @@ 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({
|
||||||
@ -69,66 +44,15 @@ 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 investmentScaleUnavailableView = createMethodUnavailablePane(
|
const pricingCategories: PricingCategoryItem[] = [
|
||||||
'该服务不适用投资规模法',
|
{ key: 'investment-scale-method', label: '投资规模法', component: investmentScaleView },
|
||||||
'当前服务未启用规模法,投资规模法不可编辑。'
|
{ key: 'land-scale-method', label: '用地规模法', component: landScaleView },
|
||||||
)
|
{ key: 'workload-method', label: '工作量法', component: workloadView },
|
||||||
const landScaleUnavailableView = createMethodUnavailablePane(
|
{ key: 'hourly-method', label: '工时法', component: hourlyView }
|
||||||
'该服务不适用用地规模法',
|
]
|
||||||
'当前服务仅支持投资规模法,用地规模法不可编辑。'
|
|
||||||
)
|
|
||||||
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>
|
||||||
|
|||||||
@ -124,10 +124,9 @@ 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>
|
||||||
|
|||||||
@ -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, getServiceDictItemById, industryTypeList, isMajorIdInIndustryScope } from '@/sql'
|
import { getMajorDictEntries, 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,10 +78,6 @@ 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
|
||||||
@ -99,26 +95,11 @@ 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([
|
||||||
@ -168,14 +149,7 @@ const shouldForceDefaultLoad = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const detailRows = ref<DetailRow[]>([])
|
const detailRows = ref<DetailRow[]>([])
|
||||||
type majorLite = {
|
type majorLite = { code: string; name: string; defCoe: number | null; hasCost?: boolean; hasArea?: boolean }
|
||||||
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]))
|
||||||
|
|
||||||
@ -210,17 +184,12 @@ 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,
|
hasCost: item.hasCost !== false,
|
||||||
hasArea
|
hasArea: item.hasArea !== false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -272,63 +241,6 @@ 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<
|
||||||
@ -428,11 +340,6 @@ 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 ''
|
||||||
}
|
}
|
||||||
@ -525,22 +432,16 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
|
|||||||
minWidth: 90,
|
minWidth: 90,
|
||||||
flex: 2,
|
flex: 2,
|
||||||
|
|
||||||
editable: params => {
|
editable: params => !params.node?.group && !params.node?.rowPinned && Boolean(params.data?.hasCost),
|
||||||
if (isOnlyCostScaleService.value) return Boolean(params.node?.rowPinned)
|
|
||||||
return !params.node?.group && !params.node?.rowPinned && Boolean(params.data?.hasCost)
|
|
||||||
},
|
|
||||||
cellClass: params =>
|
cellClass: params =>
|
||||||
isOnlyCostScaleService.value && params.node?.rowPinned
|
!params.node?.group && !params.node?.rowPinned && params.data?.hasCost
|
||||||
? '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 =>
|
||||||
isOnlyCostScaleService.value && params.node?.rowPinned
|
!params.node?.group && !params.node?.rowPinned && Boolean(params.data?.hasCost) && (params.value == null || params.value === '')
|
||||||
? 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
|
||||||
},
|
},
|
||||||
@ -556,9 +457,10 @@ 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
|
||||||
? null
|
? params.data?.benchmarkBudgetBasic ?? null
|
||||||
: getBenchmarkBudgetSplitByAmount(params.data)?.basic ?? null,
|
: getBenchmarkBudgetSplitByAmount(params.data)?.basic ?? null,
|
||||||
cellRenderer: createBudgetCellRendererWithCheck('benchmarkBudgetBasicChecked'),
|
cellRenderer: createBudgetCellRendererWithCheck('benchmarkBudgetBasicChecked'),
|
||||||
valueFormatter: formatReadonlyMoney
|
valueFormatter: formatReadonlyMoney
|
||||||
@ -571,9 +473,10 @@ 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
|
||||||
? null
|
? params.data?.benchmarkBudgetOptional ?? null
|
||||||
: getBenchmarkBudgetSplitByAmount(params.data)?.optional ?? null,
|
: getBenchmarkBudgetSplitByAmount(params.data)?.optional ?? null,
|
||||||
cellRenderer: createBudgetCellRendererWithCheck('benchmarkBudgetOptionalChecked'),
|
cellRenderer: createBudgetCellRendererWithCheck('benchmarkBudgetOptionalChecked'),
|
||||||
valueFormatter: formatReadonlyMoney
|
valueFormatter: formatReadonlyMoney
|
||||||
@ -586,9 +489,10 @@ 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
|
||||||
? null
|
? params.data?.benchmarkBudget ?? null
|
||||||
: getBenchmarkBudgetSplitByAmount(params.data)?.total ?? null,
|
: getBenchmarkBudgetSplitByAmount(params.data)?.total ?? null,
|
||||||
valueFormatter: formatReadonlyMoney
|
valueFormatter: formatReadonlyMoney
|
||||||
}
|
}
|
||||||
@ -604,21 +508,11 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
|
|||||||
colId: 'consultCategoryFactor',
|
colId: 'consultCategoryFactor',
|
||||||
minWidth: 80,
|
minWidth: 80,
|
||||||
flex: 1,
|
flex: 1,
|
||||||
editable: params =>
|
editable: params => !params.node?.group && !params.node?.rowPinned,
|
||||||
isOnlyCostScaleService.value
|
cellClass: params => (!params.node?.group && !params.node?.rowPinned ? 'editable-cell-line' : ''),
|
||||||
? 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 =>
|
||||||
isOnlyCostScaleService.value && params.node?.rowPinned
|
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
|
||||||
? 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
|
||||||
@ -629,21 +523,11 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
|
|||||||
colId: 'majorFactor',
|
colId: 'majorFactor',
|
||||||
minWidth: 80,
|
minWidth: 80,
|
||||||
flex: 1,
|
flex: 1,
|
||||||
editable: params =>
|
editable: params => !params.node?.group && !params.node?.rowPinned,
|
||||||
isOnlyCostScaleService.value
|
cellClass: params => (!params.node?.group && !params.node?.rowPinned ? 'editable-cell-line' : ''),
|
||||||
? 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 =>
|
||||||
isOnlyCostScaleService.value && params.node?.rowPinned
|
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
|
||||||
? 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
|
||||||
@ -654,21 +538,11 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
|
|||||||
colId: 'workStageFactor',
|
colId: 'workStageFactor',
|
||||||
minWidth: 80,
|
minWidth: 80,
|
||||||
flex: 1,
|
flex: 1,
|
||||||
editable: params =>
|
editable: params => !params.node?.group && !params.node?.rowPinned,
|
||||||
isOnlyCostScaleService.value
|
cellClass: params => (!params.node?.group && !params.node?.rowPinned ? 'editable-cell-line' : ''),
|
||||||
? 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 =>
|
||||||
isOnlyCostScaleService.value && params.node?.rowPinned
|
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
|
||||||
? 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
|
||||||
@ -679,21 +553,11 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
|
|||||||
colId: 'workRatio',
|
colId: 'workRatio',
|
||||||
minWidth: 80,
|
minWidth: 80,
|
||||||
flex: 1,
|
flex: 1,
|
||||||
editable: params =>
|
editable: params => !params.node?.group && !params.node?.rowPinned,
|
||||||
isOnlyCostScaleService.value
|
cellClass: params => (!params.node?.group && !params.node?.rowPinned ? 'editable-cell-line' : ''),
|
||||||
? 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 =>
|
||||||
isOnlyCostScaleService.value && params.node?.rowPinned
|
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
|
||||||
? 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
|
||||||
@ -749,13 +613,13 @@ const autoGroupColumnDef: ColDef = {
|
|||||||
},
|
},
|
||||||
valueFormatter: params => {
|
valueFormatter: params => {
|
||||||
if (params.node?.rowPinned) {
|
if (params.node?.rowPinned) {
|
||||||
return totalLabel.value
|
return '总合计'
|
||||||
}
|
}
|
||||||
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 totalLabel.value
|
if (params.node?.rowPinned) return '总合计'
|
||||||
const nodeId = String(params.value || '')
|
const nodeId = String(params.value || '')
|
||||||
return idLabelMap.get(nodeId) || nodeId
|
return idLabelMap.get(nodeId) || nodeId
|
||||||
}
|
}
|
||||||
@ -763,8 +627,6 @@ 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))
|
||||||
@ -782,18 +644,18 @@ const pinnedTopRowData = computed(() => [
|
|||||||
majorName: '',
|
majorName: '',
|
||||||
hasCost: false,
|
hasCost: false,
|
||||||
hasArea: false,
|
hasArea: false,
|
||||||
amount: isOnlyCostScaleService.value ? onlyCostScaleSourceRow.value.amount : null,
|
amount: totalAmount.value,
|
||||||
benchmarkBudget: null,
|
benchmarkBudget: totalBenchmarkBudget.value,
|
||||||
benchmarkBudgetBasic: null,
|
benchmarkBudgetBasic: totalBenchmarkBudgetBasic.value,
|
||||||
benchmarkBudgetOptional: null,
|
benchmarkBudgetOptional: totalBenchmarkBudgetOptional.value,
|
||||||
benchmarkBudgetBasicChecked: true,
|
benchmarkBudgetBasicChecked: true,
|
||||||
benchmarkBudgetOptionalChecked: true,
|
benchmarkBudgetOptionalChecked: true,
|
||||||
basicFormula: '',
|
basicFormula: '',
|
||||||
optionalFormula: '',
|
optionalFormula: '',
|
||||||
consultCategoryFactor: isOnlyCostScaleService.value ? onlyCostScaleSourceRow.value.consultCategoryFactor : null,
|
consultCategoryFactor: null,
|
||||||
majorFactor: isOnlyCostScaleService.value ? onlyCostScaleSourceRow.value.majorFactor : null,
|
majorFactor: null,
|
||||||
workStageFactor: isOnlyCostScaleService.value ? onlyCostScaleSourceRow.value.workStageFactor : null,
|
workStageFactor: null,
|
||||||
workRatio: isOnlyCostScaleService.value ? onlyCostScaleSourceRow.value.workRatio : null,
|
workRatio: null,
|
||||||
budgetFee: totalBudgetFee.value,
|
budgetFee: totalBudgetFee.value,
|
||||||
budgetFeeBasic: totalBudgetFeeBasic.value,
|
budgetFeeBasic: totalBudgetFeeBasic.value,
|
||||||
budgetFeeOptional: totalBudgetFeeOptional.value,
|
budgetFeeOptional: totalBudgetFeeOptional.value,
|
||||||
@ -865,9 +727,6 @@ 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 => ({
|
||||||
@ -875,7 +734,6 @@ const loadFromIndexedDB = async () => {
|
|||||||
consultCategoryFactor: getDefaultConsultCategoryFactor(),
|
consultCategoryFactor: getDefaultConsultCategoryFactor(),
|
||||||
majorFactor: getDefaultMajorFactorById(row.id)
|
majorFactor: getDefaultMajorFactorById(row.id)
|
||||||
}))
|
}))
|
||||||
}
|
|
||||||
syncComputedValuesToDetailRows()
|
syncComputedValuesToDetailRows()
|
||||||
}
|
}
|
||||||
if (shouldForceDefaultLoad()) {
|
if (shouldForceDefaultLoad()) {
|
||||||
@ -885,9 +743,7 @@ 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 = isOnlyCostScaleService.value
|
detailRows.value = mergeWithDictRows(data.detailRows)
|
||||||
? buildOnlyCostScaleRows(data.detailRows as any)
|
|
||||||
: mergeWithDictRows(data.detailRows)
|
|
||||||
syncComputedValuesToDetailRows()
|
syncComputedValuesToDetailRows()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -895,7 +751,7 @@ const loadFromIndexedDB = async () => {
|
|||||||
await applyContractDefaultRows()
|
await applyContractDefaultRows()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('loadFromIndexedDB failed:', error)
|
console.error('loadFromIndexedDB failed:', error)
|
||||||
detailRows.value = isOnlyCostScaleService.value ? buildOnlyCostScaleRows() : buildDefaultRows()
|
detailRows.value = buildDefaultRows()
|
||||||
syncComputedValuesToDetailRows()
|
syncComputedValuesToDetailRows()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -910,9 +766,6 @@ 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 => ({
|
||||||
@ -920,7 +773,6 @@ 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)
|
||||||
@ -929,7 +781,7 @@ const importContractData = async () => {
|
|||||||
|
|
||||||
const clearAllData = async () => {
|
const clearAllData = async () => {
|
||||||
try {
|
try {
|
||||||
detailRows.value = isOnlyCostScaleService.value ? buildOnlyCostScaleRows() : buildDefaultRows()
|
detailRows.value = buildDefaultRows()
|
||||||
await saveToIndexedDB()
|
await saveToIndexedDB()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('clearAllData failed:', error)
|
console.error('clearAllData failed:', error)
|
||||||
@ -950,29 +802,7 @@ let persistTimer: ReturnType<typeof setTimeout> | null = null
|
|||||||
|
|
||||||
|
|
||||||
let gridPersistTimer: ReturnType<typeof setTimeout> | null = null
|
let gridPersistTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
const applyOnlyCostScalePinnedValue = (field: string, rawValue: unknown) => {
|
const handleCellValueChanged = () => {
|
||||||
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(() => {
|
||||||
@ -1069,7 +899,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="visibleDetailRows" :pinnedTopRowData="pinnedTopRowData"
|
<AgGridVue :style="{ height: '100%' }" :rowData="detailRows" :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"
|
||||||
|
|||||||
@ -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, industryTypeList, isMajorIdInIndustryScope } from '@/sql'
|
import { getMajorDictEntries, 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,16 +96,6 @@ 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 = () =>
|
||||||
@ -473,9 +463,10 @@ 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
|
||||||
? null
|
? params.data?.benchmarkBudgetBasic ?? null
|
||||||
: getBenchmarkBudgetSplitByLandArea(params.data)?.basic ?? null,
|
: getBenchmarkBudgetSplitByLandArea(params.data)?.basic ?? null,
|
||||||
cellRenderer: createBudgetCellRendererWithCheck('benchmarkBudgetBasicChecked'),
|
cellRenderer: createBudgetCellRendererWithCheck('benchmarkBudgetBasicChecked'),
|
||||||
valueFormatter: formatReadonlyMoney
|
valueFormatter: formatReadonlyMoney
|
||||||
@ -488,9 +479,10 @@ 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
|
||||||
? null
|
? params.data?.benchmarkBudgetOptional ?? null
|
||||||
: getBenchmarkBudgetSplitByLandArea(params.data)?.optional ?? null,
|
: getBenchmarkBudgetSplitByLandArea(params.data)?.optional ?? null,
|
||||||
cellRenderer: createBudgetCellRendererWithCheck('benchmarkBudgetOptionalChecked'),
|
cellRenderer: createBudgetCellRendererWithCheck('benchmarkBudgetOptionalChecked'),
|
||||||
valueFormatter: formatReadonlyMoney
|
valueFormatter: formatReadonlyMoney
|
||||||
@ -503,9 +495,10 @@ 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
|
||||||
? null
|
? params.data?.benchmarkBudget ?? null
|
||||||
: getBenchmarkBudgetSplitByLandArea(params.data)?.total ?? null,
|
: getBenchmarkBudgetSplitByLandArea(params.data)?.total ?? null,
|
||||||
valueFormatter: formatReadonlyMoney
|
valueFormatter: formatReadonlyMoney
|
||||||
}
|
}
|
||||||
@ -623,13 +616,13 @@ const autoGroupColumnDef: ColDef = {
|
|||||||
},
|
},
|
||||||
valueFormatter: params => {
|
valueFormatter: params => {
|
||||||
if (params.node?.rowPinned) {
|
if (params.node?.rowPinned) {
|
||||||
return totalLabel.value
|
return '总合计'
|
||||||
}
|
}
|
||||||
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 totalLabel.value
|
if (params.node?.rowPinned) return '总合计'
|
||||||
const nodeId = String(params.value || '')
|
const nodeId = String(params.value || '')
|
||||||
return idLabelMap.get(nodeId) || nodeId
|
return idLabelMap.get(nodeId) || nodeId
|
||||||
}
|
}
|
||||||
@ -655,11 +648,11 @@ const pinnedTopRowData = computed(() => [
|
|||||||
majorName: '',
|
majorName: '',
|
||||||
hasCost: false,
|
hasCost: false,
|
||||||
hasArea: false,
|
hasArea: false,
|
||||||
amount: null,
|
amount: totalAmount.value,
|
||||||
landArea: null,
|
landArea: null,
|
||||||
benchmarkBudget: null,
|
benchmarkBudget: totalBenchmarkBudget.value,
|
||||||
benchmarkBudgetBasic: null,
|
benchmarkBudgetBasic: totalBenchmarkBudgetBasic.value,
|
||||||
benchmarkBudgetOptional: null,
|
benchmarkBudgetOptional: totalBenchmarkBudgetOptional.value,
|
||||||
benchmarkBudgetBasicChecked: true,
|
benchmarkBudgetBasicChecked: true,
|
||||||
benchmarkBudgetOptionalChecked: true,
|
benchmarkBudgetOptionalChecked: true,
|
||||||
basicFormula: '',
|
basicFormula: '',
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, defineComponent, h, nextTick, onActivated, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
import { computed, defineComponent, h, nextTick, 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,11 +9,7 @@ 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 {
|
import { getPricingMethodTotalsForService, getPricingMethodTotalsForServices } from '@/lib/pricingMethodTotals'
|
||||||
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 {
|
||||||
@ -25,11 +21,18 @@ 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, isIndustryEnabledByType,getIndustryTypeValue } from '@/sql'
|
import { getServiceDictEntries } 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'
|
||||||
@ -38,7 +41,6 @@ interface ServiceItem {
|
|||||||
id: string
|
id: string
|
||||||
code: string
|
code: string
|
||||||
name: string
|
name: string
|
||||||
type: ServiceMethodType
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DetailRow {
|
interface DetailRow {
|
||||||
@ -59,17 +61,6 @@ 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
|
||||||
@ -77,66 +68,25 @@ 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 = {
|
type ServiceListItem = { code?: string; ref?: string; name: string; defCoe: number | null }
|
||||||
code?: string
|
const serviceDict: ServiceItem[] = getServiceDictEntries()
|
||||||
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 && isIndustryEnabledByType(item, getIndustryTypeValue(industry))
|
return Boolean(itemCode && item?.name) && item.defCoe !== null
|
||||||
|
|
||||||
})
|
})
|
||||||
return filteredEntries.map(({ id, item }) => ({
|
.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 = computed(() => new Map(serviceDict.value.map(item => [item.id, item])))
|
const serviceById = new Map(serviceDict.map(item => [item.id, item]))
|
||||||
const serviceIdByCode = computed(() => new Map(serviceDict.value.map(item => [item.code, item.id])))
|
const serviceIdByCode = new Map(serviceDict.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
|
||||||
|
|
||||||
@ -217,7 +167,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.value.get(id)?.name || '')
|
.map(id => serviceById.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} 项`
|
||||||
@ -227,7 +177,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.value.get(pendingClearServiceId.value)
|
const dict = serviceById.get(pendingClearServiceId.value)
|
||||||
if (dict) return `${dict.code}${dict.name}`
|
if (dict) return `${dict.code}${dict.name}`
|
||||||
return pendingClearServiceId.value
|
return pendingClearServiceId.value
|
||||||
})
|
})
|
||||||
@ -236,7 +186,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.value.get(pendingDeleteServiceId.value)
|
const dict = serviceById.get(pendingDeleteServiceId.value)
|
||||||
if (dict) return `${dict.code}${dict.name}`
|
if (dict) return `${dict.code}${dict.name}`
|
||||||
return pendingDeleteServiceId.value
|
return pendingDeleteServiceId.value
|
||||||
})
|
})
|
||||||
@ -291,8 +241,8 @@ const confirmDeleteRow = async () => {
|
|||||||
|
|
||||||
const filteredServiceDict = computed(() => {
|
const filteredServiceDict = computed(() => {
|
||||||
const keyword = pickerSearch.value.trim()
|
const keyword = pickerSearch.value.trim()
|
||||||
if (!keyword) return serviceDict.value
|
if (!keyword) return serviceDict
|
||||||
return serviceDict.value.filter(item => item.code.includes(keyword) || item.name.includes(keyword))
|
return serviceDict.filter(item => item.code.includes(keyword) || item.name.includes(keyword))
|
||||||
})
|
})
|
||||||
|
|
||||||
const dragRectStyle = computed(() => {
|
const dragRectStyle = computed(() => {
|
||||||
@ -315,40 +265,6 @@ 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'
|
||||||
@ -397,19 +313,17 @@ 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: sanitizedTotals.investScale,
|
investScale: totals.investScale,
|
||||||
landScale: sanitizedTotals.landScale,
|
landScale: totals.landScale,
|
||||||
workload: sanitizedTotals.workload,
|
workload: totals.workload,
|
||||||
hourly: sanitizedTotals.hourly
|
hourly: totals.hourly
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
const nextInvestScale = getMethodTotalFromRows(clearedRows, 'investScale')
|
const nextInvestScale = getMethodTotalFromRows(clearedRows, 'investScale')
|
||||||
@ -432,7 +346,6 @@ 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}`,
|
||||||
@ -441,8 +354,7 @@ 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
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -650,15 +562,13 @@ 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 totalsRaw = totalsByServiceId.get(String(row.id))
|
const totals = 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,
|
||||||
@ -675,35 +585,29 @@ 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.value.has(id) && id !== fixedBudgetRow.id
|
id => serviceById.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.value.get(id)
|
const dictItem = serviceById.get(id)
|
||||||
if (!dictItem) return null
|
if (!dictItem) return null
|
||||||
|
|
||||||
const old = existingMap.get(id)
|
const old = existingMap.get(id)
|
||||||
const nextValues = sanitizePricingFieldsByService(id, {
|
|
||||||
investScale: old?.investScale ?? null,
|
|
||||||
landScale: old?.landScale ?? null,
|
|
||||||
workload: old?.workload ?? null,
|
|
||||||
hourly: old?.hourly ?? null
|
|
||||||
})
|
|
||||||
return {
|
return {
|
||||||
id: old?.id || id,
|
id: old?.id || id,
|
||||||
code: dictItem.code,
|
code: dictItem.code,
|
||||||
name: dictItem.name,
|
name: dictItem.name,
|
||||||
investScale: nextValues.investScale,
|
investScale: old?.investScale ?? null,
|
||||||
landScale: nextValues.landScale,
|
landScale: old?.landScale ?? null,
|
||||||
workload: nextValues.workload,
|
workload: old?.workload ?? null,
|
||||||
hourly: nextValues.hourly
|
hourly: old?.hourly ?? null
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.filter((row): row is DetailRow => Boolean(row))
|
.filter((row): row is DetailRow => Boolean(row))
|
||||||
|
|
||||||
const orderMap = new Map(serviceDict.value.map((item, index) => [item.id, index]))
|
const orderMap = new Map(serviceDict.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)
|
||||||
@ -826,7 +730,6 @@ 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))
|
||||||
}
|
}
|
||||||
@ -894,25 +797,19 @@ const loadFromIndexedDB = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const idsFromStorage = data.selectedIds
|
const idsFromStorage = data.selectedIds
|
||||||
|| (data.selectedCodes || []).map(code => serviceIdByCode.value.get(code)).filter((id): id is string => Boolean(id))
|
|| (data.selectedCodes || []).map(code => serviceIdByCode.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
|
||||||
const nextValues = sanitizePricingFieldsByService(row.id, {
|
return {
|
||||||
|
...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)
|
||||||
@ -923,17 +820,6 @@ 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) => {
|
||||||
@ -942,15 +828,6 @@ 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)
|
||||||
@ -960,12 +837,6 @@ const handleCellValueChanged = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await loadProjectIndustry()
|
|
||||||
await loadFromIndexedDB()
|
|
||||||
})
|
|
||||||
|
|
||||||
onActivated(async () => {
|
|
||||||
await loadProjectIndustry()
|
|
||||||
await loadFromIndexedDB()
|
await loadFromIndexedDB()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -53,14 +53,10 @@ 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 {
|
||||||
@ -74,10 +70,6 @@ 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
|
||||||
@ -85,12 +77,6 @@ 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)
|
||||||
|
|
||||||
@ -107,11 +93,6 @@ 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()
|
||||||
|
|
||||||
@ -121,27 +102,6 @@ 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
|
||||||
@ -267,38 +227,6 @@ 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),
|
||||||
@ -445,46 +373,30 @@ 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, baseInfo] = await Promise.all([
|
const [investData, landData, workloadData, hourlyData, htData, consultFactorData, majorFactorData] = 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,
|
||||||
@ -492,11 +404,7 @@ export const getPricingMethodTotalsForService = async (params: {
|
|||||||
consultCategoryFactorMap,
|
consultCategoryFactorMap,
|
||||||
majorFactorMap
|
majorFactorMap
|
||||||
)
|
)
|
||||||
return sumByNumber(investRows, row => {
|
const investScale = sumByNumber(investRows, row => getInvestmentBudgetFee(row))
|
||||||
if (excludeInvestmentCostAndAreaRows && isDualScaleMajorById(row.id)) return null
|
|
||||||
return getInvestmentBudgetFee(row)
|
|
||||||
})
|
|
||||||
})()
|
|
||||||
|
|
||||||
const landRows = resolveScaleRows(
|
const landRows = resolveScaleRows(
|
||||||
serviceId,
|
serviceId,
|
||||||
@ -535,15 +443,13 @@ 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)
|
||||||
})
|
})
|
||||||
|
|||||||
24
src/sql.ts
24
src/sql.ts
@ -11,12 +11,6 @@ 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 },
|
||||||
@ -25,7 +19,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 ,industryId:'0' },
|
7: { code: 'E2', name: '公路工程专业', maxCoe: null, minCoe: null, defCoe: 1, desc: '适用于公路工程的全过程造价咨询、分阶段造价咨询、投资估算、初步设计概算、竣工决算和调整估算、调整概算(含征地拆迁和工程建设其他费)', isRoad: true, isRailway: false, isWaterway: false, order: 8, hasCost: false, 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 },
|
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 },
|
||||||
@ -36,7 +30,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 ,industryId:'1' },
|
18: { code: 'E3', name: '铁路工程专业', maxCoe: null, minCoe: null, defCoe: 1, desc: '适用于铁路工程的投资估算、初步设计概算、清理概算、竣工决算和调整估算、调整概算(含征地拆迁和工程建设其他费)', isRoad: false, isRailway: true, isWaterway: false, order: 19, hasCost: false, 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 },
|
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 },
|
||||||
@ -46,7 +40,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 ,industryId:'2' },
|
28: { code: 'E4', name: '水运工程专业', maxCoe: null, minCoe: null, defCoe: 1, desc: '适用于水运工程的投资估算、初步设计概算、竣工决算和调整估算、调整概算(含征地拆迁和工程建设其他费)', isRoad: false, isRailway: false, isWaterway: true, order: 29, hasCost: false, 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 },
|
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 },
|
||||||
@ -182,7 +176,11 @@ 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>
|
||||||
@ -316,6 +314,12 @@ 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 -> 专业项”映射。
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user