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>[] = [
|
||||
{
|
||||
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: '费用项',
|
||||
field: 'feeItem',
|
||||
|
||||
@ -1,55 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onActivated, onMounted, ref } from 'vue'
|
||||
import localforage from 'localforage'
|
||||
import { getServiceDictEntries, isIndustryEnabledByType, getIndustryTypeValue } from '@/sql'
|
||||
import { serviceList } from '@/sql'
|
||||
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>
|
||||
|
||||
<template>
|
||||
<XmFactorGrid title="咨询分类系数明细" storage-key="xm-consult-category-factor-v1" :dict="filteredServiceDict"
|
||||
:disable-budget-edit-when-standard-null="true" :exclude-notshow-by-zxflxs="true" />
|
||||
<XmFactorGrid
|
||||
title="咨询分类系数明细"
|
||||
storage-key="xm-consult-category-factor-v1"
|
||||
:dict="serviceList"
|
||||
:disable-budget-edit-when-standard-null="true"
|
||||
:exclude-notshow-by-zxflxs="true"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@ -5,29 +5,20 @@
|
||||
:subtitle="`合同ID:${contractId}`"
|
||||
:copy-text="contractId"
|
||||
:storage-key="`zxfw-pricing-active-cat-${contractId}-${serviceId}`"
|
||||
:default-category="defaultCategory"
|
||||
default-category="investment-scale-method"
|
||||
:categories="pricingCategories"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<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 MethodUnavailableNotice from '@/components/common/MethodUnavailableNotice.vue'
|
||||
|
||||
interface ServiceMethodType {
|
||||
scale?: boolean | null
|
||||
onlyCostScale?: boolean | null
|
||||
amount?: boolean | null
|
||||
workDay?: boolean | null
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
contractId: string
|
||||
contractName?: string
|
||||
serviceId: string|number
|
||||
fwName:string
|
||||
type?: ServiceMethodType
|
||||
}>()
|
||||
|
||||
interface PricingCategoryItem {
|
||||
@ -36,22 +27,6 @@ interface PricingCategoryItem {
|
||||
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) =>
|
||||
markRaw(
|
||||
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 landScaleView = createPricingPane('LandScalePricingPane')
|
||||
const workloadView = createPricingPane('WorkloadPricingPane')
|
||||
const hourlyView = createPricingPane('HourlyPricingPane')
|
||||
|
||||
const investmentScaleUnavailableView = createMethodUnavailablePane(
|
||||
'该服务不适用投资规模法',
|
||||
'当前服务未启用规模法,投资规模法不可编辑。'
|
||||
)
|
||||
const landScaleUnavailableView = createMethodUnavailablePane(
|
||||
'该服务不适用用地规模法',
|
||||
'当前服务仅支持投资规模法,用地规模法不可编辑。'
|
||||
)
|
||||
const workloadUnavailableView = createMethodUnavailablePane(
|
||||
'该服务不适用工作量法',
|
||||
'当前服务未启用工作量法,工作量法不可编辑。'
|
||||
)
|
||||
const hourlyUnavailableView = createMethodUnavailablePane(
|
||||
'该服务不适用工时法',
|
||||
'当前服务未启用工时法,工时法不可编辑。'
|
||||
)
|
||||
|
||||
const pricingCategories = computed<PricingCategoryItem[]>(() => [
|
||||
{
|
||||
key: 'investment-scale-method',
|
||||
label: '投资规模法',
|
||||
component: methodAvailability.value.investmentScale ? investmentScaleView : investmentScaleUnavailableView
|
||||
},
|
||||
{
|
||||
key: 'land-scale-method',
|
||||
label: '用地规模法',
|
||||
component: methodAvailability.value.landScale ? landScaleView : landScaleUnavailableView
|
||||
},
|
||||
{
|
||||
key: 'workload-method',
|
||||
label: '工作量法',
|
||||
component: methodAvailability.value.workload ? workloadView : workloadUnavailableView
|
||||
},
|
||||
{
|
||||
key: 'hourly-method',
|
||||
label: '工时法',
|
||||
component: methodAvailability.value.hourly ? hourlyView : hourlyUnavailableView
|
||||
}
|
||||
])
|
||||
|
||||
const defaultCategory = computed(() => {
|
||||
if (methodAvailability.value.investmentScale) return 'investment-scale-method'
|
||||
if (methodAvailability.value.landScale) return 'land-scale-method'
|
||||
if (methodAvailability.value.workload) return 'workload-method'
|
||||
if (methodAvailability.value.hourly) return 'hourly-method'
|
||||
return 'investment-scale-method'
|
||||
})
|
||||
const pricingCategories: PricingCategoryItem[] = [
|
||||
{ key: 'investment-scale-method', label: '投资规模法', component: investmentScaleView },
|
||||
{ key: 'land-scale-method', label: '用地规模法', component: landScaleView },
|
||||
{ key: 'workload-method', label: '工作量法', component: workloadView },
|
||||
{ key: 'hourly-method', label: '工时法', component: hourlyView }
|
||||
]
|
||||
</script>
|
||||
|
||||
@ -124,10 +124,9 @@ const xmCategories: XmCategoryItem[] = [
|
||||
{ key: 'info', label: '规模信息', component: htView },
|
||||
{ key: 'consult-category-factor', label: '咨询分类系数', component: consultCategoryFactorView },
|
||||
{ key: 'major-factor', label: '工程专业系数', component: majorFactorView },
|
||||
{ key: 'contract', label: '咨询服务', component: zxfwView },
|
||||
|
||||
{ key: 'additional-work-fee', label: '附加工作费', component: additionalWorkFeeView },
|
||||
{ key: 'reserve-fee', label: '预备费', component: reserveFeeView },
|
||||
{ key: 'contract', label: '咨询服务', component: zxfwView },
|
||||
|
||||
];
|
||||
</script>
|
||||
|
||||
@ -3,7 +3,7 @@ import { computed, onActivated, onBeforeUnmount, onMounted, ref, watch } from 'v
|
||||
import { AgGridVue } from 'ag-grid-vue3'
|
||||
import type { ColDef, ColGroupDef } from 'ag-grid-community'
|
||||
import localforage from 'localforage'
|
||||
import { getMajorDictEntries, getServiceDictItemById, industryTypeList, isMajorIdInIndustryScope } from '@/sql'
|
||||
import { getMajorDictEntries, isMajorIdInIndustryScope } from '@/sql'
|
||||
import { myTheme, gridOptions } from '@/lib/diyAgGridOptions'
|
||||
import { decimalAggSum, roundTo, sumByNumber } from '@/lib/decimal'
|
||||
import { formatThousands } from '@/lib/numberFormat'
|
||||
@ -78,10 +78,6 @@ interface XmBaseInfoState {
|
||||
projectIndustry?: string
|
||||
}
|
||||
|
||||
interface ServiceLite {
|
||||
onlyCostScale?: boolean | null
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
contractId: string,
|
||||
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())
|
||||
let factorDefaultsLoaded = false
|
||||
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 = () =>
|
||||
consultCategoryFactorMap.value.get(String(props.serviceId)) ?? 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 [consultMap, majorMap] = await Promise.all([
|
||||
@ -168,14 +149,7 @@ const shouldForceDefaultLoad = () => {
|
||||
}
|
||||
|
||||
const detailRows = ref<DetailRow[]>([])
|
||||
type majorLite = {
|
||||
code: string
|
||||
name: string
|
||||
defCoe: number | null
|
||||
hasCost?: boolean
|
||||
hasArea?: boolean
|
||||
industryId?: string | number | null
|
||||
}
|
||||
type majorLite = { code: string; name: string; defCoe: number | null; hasCost?: boolean; hasArea?: boolean }
|
||||
const serviceEntries = getMajorDictEntries().map(({ id, item }) => [id, item] as [string, majorLite])
|
||||
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({
|
||||
id: key,
|
||||
code,
|
||||
name: item.name,
|
||||
hasCost,
|
||||
hasArea
|
||||
hasCost: item.hasCost !== false,
|
||||
hasArea: item.hasArea !== false
|
||||
})
|
||||
}
|
||||
|
||||
@ -272,63 +241,6 @@ const buildDefaultRows = (): DetailRow[] => {
|
||||
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'> &
|
||||
Partial<
|
||||
Pick<
|
||||
@ -428,11 +340,6 @@ const formatMajorFactor = (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) {
|
||||
return ''
|
||||
}
|
||||
@ -525,22 +432,16 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
|
||||
minWidth: 90,
|
||||
flex: 2,
|
||||
|
||||
editable: params => {
|
||||
if (isOnlyCostScaleService.value) return Boolean(params.node?.rowPinned)
|
||||
return !params.node?.group && !params.node?.rowPinned && Boolean(params.data?.hasCost)
|
||||
},
|
||||
editable: params => !params.node?.group && !params.node?.rowPinned && Boolean(params.data?.hasCost),
|
||||
cellClass: params =>
|
||||
isOnlyCostScaleService.value && params.node?.rowPinned
|
||||
? 'ag-right-aligned-cell editable-cell-line'
|
||||
: !params.node?.group && !params.node?.rowPinned && params.data?.hasCost
|
||||
!params.node?.group && !params.node?.rowPinned && params.data?.hasCost
|
||||
? 'ag-right-aligned-cell editable-cell-line'
|
||||
: 'ag-right-aligned-cell',
|
||||
cellClassRules: {
|
||||
'editable-cell-empty': params =>
|
||||
isOnlyCostScaleService.value && params.node?.rowPinned
|
||||
? params.value == null || params.value === ''
|
||||
: !params.node?.group && !params.node?.rowPinned && Boolean(params.data?.hasCost) && (params.value == null || params.value === '')
|
||||
!params.node?.group && !params.node?.rowPinned && Boolean(params.data?.hasCost) && (params.value == null || params.value === '')
|
||||
},
|
||||
aggFunc: decimalAggSum,
|
||||
valueParser: params => parseNumberOrNull(params.newValue, { precision: 2 }),
|
||||
valueFormatter: formatEditableMoney
|
||||
},
|
||||
@ -556,9 +457,10 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
|
||||
minWidth: 130,
|
||||
flex: 1,
|
||||
cellClass: 'ag-right-aligned-cell',
|
||||
aggFunc: decimalAggSum,
|
||||
valueGetter: params =>
|
||||
params.node?.rowPinned
|
||||
? null
|
||||
? params.data?.benchmarkBudgetBasic ?? null
|
||||
: getBenchmarkBudgetSplitByAmount(params.data)?.basic ?? null,
|
||||
cellRenderer: createBudgetCellRendererWithCheck('benchmarkBudgetBasicChecked'),
|
||||
valueFormatter: formatReadonlyMoney
|
||||
@ -571,9 +473,10 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
|
||||
minWidth: 130,
|
||||
flex: 1,
|
||||
cellClass: 'ag-right-aligned-cell',
|
||||
aggFunc: decimalAggSum,
|
||||
valueGetter: params =>
|
||||
params.node?.rowPinned
|
||||
? null
|
||||
? params.data?.benchmarkBudgetOptional ?? null
|
||||
: getBenchmarkBudgetSplitByAmount(params.data)?.optional ?? null,
|
||||
cellRenderer: createBudgetCellRendererWithCheck('benchmarkBudgetOptionalChecked'),
|
||||
valueFormatter: formatReadonlyMoney
|
||||
@ -586,9 +489,10 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
|
||||
minWidth: 100,
|
||||
flex: 1,
|
||||
cellClass: 'ag-right-aligned-cell',
|
||||
aggFunc: decimalAggSum,
|
||||
valueGetter: params =>
|
||||
params.node?.rowPinned
|
||||
? null
|
||||
? params.data?.benchmarkBudget ?? null
|
||||
: getBenchmarkBudgetSplitByAmount(params.data)?.total ?? null,
|
||||
valueFormatter: formatReadonlyMoney
|
||||
}
|
||||
@ -604,21 +508,11 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
|
||||
colId: 'consultCategoryFactor',
|
||||
minWidth: 80,
|
||||
flex: 1,
|
||||
editable: params =>
|
||||
isOnlyCostScaleService.value
|
||||
? Boolean(params.node?.rowPinned)
|
||||
: !params.node?.group && !params.node?.rowPinned,
|
||||
cellClass: params =>
|
||||
isOnlyCostScaleService.value && params.node?.rowPinned
|
||||
? 'editable-cell-line'
|
||||
: !params.node?.group && !params.node?.rowPinned
|
||||
? 'editable-cell-line'
|
||||
: '',
|
||||
editable: params => !params.node?.group && !params.node?.rowPinned,
|
||||
cellClass: params => (!params.node?.group && !params.node?.rowPinned ? 'editable-cell-line' : ''),
|
||||
cellClassRules: {
|
||||
'editable-cell-empty': params =>
|
||||
isOnlyCostScaleService.value && params.node?.rowPinned
|
||||
? params.value == null || params.value === ''
|
||||
: !params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
|
||||
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
|
||||
},
|
||||
valueParser: params => parseNumberOrNull(params.newValue, { precision: 2 }),
|
||||
valueFormatter: formatConsultCategoryFactor
|
||||
@ -629,21 +523,11 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
|
||||
colId: 'majorFactor',
|
||||
minWidth: 80,
|
||||
flex: 1,
|
||||
editable: params =>
|
||||
isOnlyCostScaleService.value
|
||||
? Boolean(params.node?.rowPinned)
|
||||
: !params.node?.group && !params.node?.rowPinned,
|
||||
cellClass: params =>
|
||||
isOnlyCostScaleService.value && params.node?.rowPinned
|
||||
? 'editable-cell-line'
|
||||
: !params.node?.group && !params.node?.rowPinned
|
||||
? 'editable-cell-line'
|
||||
: '',
|
||||
editable: params => !params.node?.group && !params.node?.rowPinned,
|
||||
cellClass: params => (!params.node?.group && !params.node?.rowPinned ? 'editable-cell-line' : ''),
|
||||
cellClassRules: {
|
||||
'editable-cell-empty': params =>
|
||||
isOnlyCostScaleService.value && params.node?.rowPinned
|
||||
? params.value == null || params.value === ''
|
||||
: !params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
|
||||
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
|
||||
},
|
||||
valueParser: params => parseNumberOrNull(params.newValue, { precision: 2 }),
|
||||
valueFormatter: formatMajorFactor
|
||||
@ -654,21 +538,11 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
|
||||
colId: 'workStageFactor',
|
||||
minWidth: 80,
|
||||
flex: 1,
|
||||
editable: params =>
|
||||
isOnlyCostScaleService.value
|
||||
? Boolean(params.node?.rowPinned)
|
||||
: !params.node?.group && !params.node?.rowPinned,
|
||||
cellClass: params =>
|
||||
isOnlyCostScaleService.value && params.node?.rowPinned
|
||||
? 'editable-cell-line'
|
||||
: !params.node?.group && !params.node?.rowPinned
|
||||
? 'editable-cell-line'
|
||||
: '',
|
||||
editable: params => !params.node?.group && !params.node?.rowPinned,
|
||||
cellClass: params => (!params.node?.group && !params.node?.rowPinned ? 'editable-cell-line' : ''),
|
||||
cellClassRules: {
|
||||
'editable-cell-empty': params =>
|
||||
isOnlyCostScaleService.value && params.node?.rowPinned
|
||||
? params.value == null || params.value === ''
|
||||
: !params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
|
||||
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
|
||||
},
|
||||
valueParser: params => parseNumberOrNull(params.newValue, { precision: 2 }),
|
||||
valueFormatter: formatEditableNumber
|
||||
@ -679,21 +553,11 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
|
||||
colId: 'workRatio',
|
||||
minWidth: 80,
|
||||
flex: 1,
|
||||
editable: params =>
|
||||
isOnlyCostScaleService.value
|
||||
? Boolean(params.node?.rowPinned)
|
||||
: !params.node?.group && !params.node?.rowPinned,
|
||||
cellClass: params =>
|
||||
isOnlyCostScaleService.value && params.node?.rowPinned
|
||||
? 'editable-cell-line'
|
||||
: !params.node?.group && !params.node?.rowPinned
|
||||
? 'editable-cell-line'
|
||||
: '',
|
||||
editable: params => !params.node?.group && !params.node?.rowPinned,
|
||||
cellClass: params => (!params.node?.group && !params.node?.rowPinned ? 'editable-cell-line' : ''),
|
||||
cellClassRules: {
|
||||
'editable-cell-empty': params =>
|
||||
isOnlyCostScaleService.value && params.node?.rowPinned
|
||||
? params.value == null || params.value === ''
|
||||
: !params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
|
||||
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
|
||||
},
|
||||
valueParser: params => parseNumberOrNull(params.newValue, { precision: 2 }),
|
||||
valueFormatter: formatEditableNumber
|
||||
@ -749,13 +613,13 @@ const autoGroupColumnDef: ColDef = {
|
||||
},
|
||||
valueFormatter: params => {
|
||||
if (params.node?.rowPinned) {
|
||||
return totalLabel.value
|
||||
return '总合计'
|
||||
}
|
||||
const nodeId = String(params.value || '')
|
||||
return idLabelMap.get(nodeId) || nodeId
|
||||
},
|
||||
tooltipValueGetter: params => {
|
||||
if (params.node?.rowPinned) return totalLabel.value
|
||||
if (params.node?.rowPinned) return '总合计'
|
||||
const nodeId = String(params.value || '')
|
||||
return idLabelMap.get(nodeId) || nodeId
|
||||
}
|
||||
@ -763,8 +627,6 @@ const autoGroupColumnDef: ColDef = {
|
||||
|
||||
|
||||
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 totalBenchmarkBudgetOptional = computed(() => sumByNumber(detailRows.value, row => getBenchmarkBudgetSplitByAmount(row)?.optional))
|
||||
@ -782,18 +644,18 @@ const pinnedTopRowData = computed(() => [
|
||||
majorName: '',
|
||||
hasCost: false,
|
||||
hasArea: false,
|
||||
amount: isOnlyCostScaleService.value ? onlyCostScaleSourceRow.value.amount : null,
|
||||
benchmarkBudget: null,
|
||||
benchmarkBudgetBasic: null,
|
||||
benchmarkBudgetOptional: null,
|
||||
amount: totalAmount.value,
|
||||
benchmarkBudget: totalBenchmarkBudget.value,
|
||||
benchmarkBudgetBasic: totalBenchmarkBudgetBasic.value,
|
||||
benchmarkBudgetOptional: totalBenchmarkBudgetOptional.value,
|
||||
benchmarkBudgetBasicChecked: true,
|
||||
benchmarkBudgetOptionalChecked: true,
|
||||
basicFormula: '',
|
||||
optionalFormula: '',
|
||||
consultCategoryFactor: isOnlyCostScaleService.value ? onlyCostScaleSourceRow.value.consultCategoryFactor : null,
|
||||
majorFactor: isOnlyCostScaleService.value ? onlyCostScaleSourceRow.value.majorFactor : null,
|
||||
workStageFactor: isOnlyCostScaleService.value ? onlyCostScaleSourceRow.value.workStageFactor : null,
|
||||
workRatio: isOnlyCostScaleService.value ? onlyCostScaleSourceRow.value.workRatio : null,
|
||||
consultCategoryFactor: null,
|
||||
majorFactor: null,
|
||||
workStageFactor: null,
|
||||
workRatio: null,
|
||||
budgetFee: totalBudgetFee.value,
|
||||
budgetFeeBasic: totalBudgetFeeBasic.value,
|
||||
budgetFeeOptional: totalBudgetFeeOptional.value,
|
||||
@ -865,9 +727,6 @@ const loadFromIndexedDB = async () => {
|
||||
const applyContractDefaultRows = async () => {
|
||||
const htData = await localforage.getItem<{ detailRows: SourceRow[] }>(HT_DB_KEY.value)
|
||||
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
|
||||
? mergeWithDictRows(htData!.detailRows, { includeFactorValues: true })
|
||||
: buildDefaultRows().map(row => ({
|
||||
@ -875,7 +734,6 @@ const loadFromIndexedDB = async () => {
|
||||
consultCategoryFactor: getDefaultConsultCategoryFactor(),
|
||||
majorFactor: getDefaultMajorFactorById(row.id)
|
||||
}))
|
||||
}
|
||||
syncComputedValuesToDetailRows()
|
||||
}
|
||||
if (shouldForceDefaultLoad()) {
|
||||
@ -885,9 +743,7 @@ const loadFromIndexedDB = async () => {
|
||||
|
||||
const data = await localforage.getItem<XmInfoState>(DB_KEY.value)
|
||||
if (data) {
|
||||
detailRows.value = isOnlyCostScaleService.value
|
||||
? buildOnlyCostScaleRows(data.detailRows as any)
|
||||
: mergeWithDictRows(data.detailRows)
|
||||
detailRows.value = mergeWithDictRows(data.detailRows)
|
||||
syncComputedValuesToDetailRows()
|
||||
return
|
||||
}
|
||||
@ -895,7 +751,7 @@ const loadFromIndexedDB = async () => {
|
||||
await applyContractDefaultRows()
|
||||
} catch (error) {
|
||||
console.error('loadFromIndexedDB failed:', error)
|
||||
detailRows.value = isOnlyCostScaleService.value ? buildOnlyCostScaleRows() : buildDefaultRows()
|
||||
detailRows.value = buildDefaultRows()
|
||||
syncComputedValuesToDetailRows()
|
||||
}
|
||||
}
|
||||
@ -910,9 +766,6 @@ const importContractData = async () => {
|
||||
await loadFactorDefaults()
|
||||
const htData = await localforage.getItem<{ detailRows: SourceRow[] }>(HT_DB_KEY.value)
|
||||
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
|
||||
? mergeWithDictRows(htData!.detailRows, { includeFactorValues: true })
|
||||
: buildDefaultRows().map(row => ({
|
||||
@ -920,7 +773,6 @@ const importContractData = async () => {
|
||||
consultCategoryFactor: getDefaultConsultCategoryFactor(),
|
||||
majorFactor: getDefaultMajorFactorById(row.id)
|
||||
}))
|
||||
}
|
||||
await saveToIndexedDB()
|
||||
} catch (error) {
|
||||
console.error('importContractData failed:', error)
|
||||
@ -929,7 +781,7 @@ const importContractData = async () => {
|
||||
|
||||
const clearAllData = async () => {
|
||||
try {
|
||||
detailRows.value = isOnlyCostScaleService.value ? buildOnlyCostScaleRows() : buildDefaultRows()
|
||||
detailRows.value = buildDefaultRows()
|
||||
await saveToIndexedDB()
|
||||
} catch (error) {
|
||||
console.error('clearAllData failed:', error)
|
||||
@ -950,29 +802,7 @@ let persistTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
|
||||
let gridPersistTimer: ReturnType<typeof setTimeout> | null = null
|
||||
const applyOnlyCostScalePinnedValue = (field: string, rawValue: unknown) => {
|
||||
const parsedValue = parseNumberOrNull(rawValue, { precision: 2 })
|
||||
const 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)
|
||||
}
|
||||
const handleCellValueChanged = () => {
|
||||
syncComputedValuesToDetailRows()
|
||||
if (gridPersistTimer) clearTimeout(gridPersistTimer)
|
||||
gridPersistTimer = setTimeout(() => {
|
||||
@ -1069,7 +899,7 @@ const processCellFromClipboard = (params: any) => {
|
||||
</div>
|
||||
|
||||
<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"
|
||||
@cell-value-changed="handleCellValueChanged" :suppressColumnVirtualisation="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 type { ColDef, ColGroupDef } from 'ag-grid-community'
|
||||
import localforage from 'localforage'
|
||||
import { getMajorDictEntries, industryTypeList, isMajorIdInIndustryScope } from '@/sql'
|
||||
import { getMajorDictEntries, isMajorIdInIndustryScope } from '@/sql'
|
||||
import { myTheme, gridOptions } from '@/lib/diyAgGridOptions'
|
||||
import { decimalAggSum, roundTo, sumByNumber } from '@/lib/decimal'
|
||||
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())
|
||||
let factorDefaultsLoaded = false
|
||||
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 getDefaultConsultCategoryFactor = () =>
|
||||
@ -473,9 +463,10 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
|
||||
minWidth: 130,
|
||||
flex: 1,
|
||||
cellClass: 'ag-right-aligned-cell',
|
||||
aggFunc: decimalAggSum,
|
||||
valueGetter: params =>
|
||||
params.node?.rowPinned
|
||||
? null
|
||||
? params.data?.benchmarkBudgetBasic ?? null
|
||||
: getBenchmarkBudgetSplitByLandArea(params.data)?.basic ?? null,
|
||||
cellRenderer: createBudgetCellRendererWithCheck('benchmarkBudgetBasicChecked'),
|
||||
valueFormatter: formatReadonlyMoney
|
||||
@ -488,9 +479,10 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
|
||||
minWidth: 130,
|
||||
flex: 1,
|
||||
cellClass: 'ag-right-aligned-cell',
|
||||
aggFunc: decimalAggSum,
|
||||
valueGetter: params =>
|
||||
params.node?.rowPinned
|
||||
? null
|
||||
? params.data?.benchmarkBudgetOptional ?? null
|
||||
: getBenchmarkBudgetSplitByLandArea(params.data)?.optional ?? null,
|
||||
cellRenderer: createBudgetCellRendererWithCheck('benchmarkBudgetOptionalChecked'),
|
||||
valueFormatter: formatReadonlyMoney
|
||||
@ -503,9 +495,10 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
|
||||
minWidth: 100,
|
||||
flex: 1,
|
||||
cellClass: 'ag-right-aligned-cell',
|
||||
aggFunc: decimalAggSum,
|
||||
valueGetter: params =>
|
||||
params.node?.rowPinned
|
||||
? null
|
||||
? params.data?.benchmarkBudget ?? null
|
||||
: getBenchmarkBudgetSplitByLandArea(params.data)?.total ?? null,
|
||||
valueFormatter: formatReadonlyMoney
|
||||
}
|
||||
@ -623,13 +616,13 @@ const autoGroupColumnDef: ColDef = {
|
||||
},
|
||||
valueFormatter: params => {
|
||||
if (params.node?.rowPinned) {
|
||||
return totalLabel.value
|
||||
return '总合计'
|
||||
}
|
||||
const nodeId = String(params.value || '')
|
||||
return idLabelMap.get(nodeId) || nodeId
|
||||
},
|
||||
tooltipValueGetter: params => {
|
||||
if (params.node?.rowPinned) return totalLabel.value
|
||||
if (params.node?.rowPinned) return '总合计'
|
||||
const nodeId = String(params.value || '')
|
||||
return idLabelMap.get(nodeId) || nodeId
|
||||
}
|
||||
@ -655,11 +648,11 @@ const pinnedTopRowData = computed(() => [
|
||||
majorName: '',
|
||||
hasCost: false,
|
||||
hasArea: false,
|
||||
amount: null,
|
||||
amount: totalAmount.value,
|
||||
landArea: null,
|
||||
benchmarkBudget: null,
|
||||
benchmarkBudgetBasic: null,
|
||||
benchmarkBudgetOptional: null,
|
||||
benchmarkBudget: totalBenchmarkBudget.value,
|
||||
benchmarkBudgetBasic: totalBenchmarkBudgetBasic.value,
|
||||
benchmarkBudgetOptional: totalBenchmarkBudgetOptional.value,
|
||||
benchmarkBudgetBasicChecked: true,
|
||||
benchmarkBudgetOptionalChecked: true,
|
||||
basicFormula: '',
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<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 { AgGridVue } from 'ag-grid-vue3'
|
||||
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 { parseNumberOrNull } from '@/lib/number'
|
||||
import { formatThousands, formatThousandsFlexible } from '@/lib/numberFormat'
|
||||
import {
|
||||
getPricingMethodTotalsForService,
|
||||
getPricingMethodTotalsForServices,
|
||||
type PricingMethodTotals
|
||||
} from '@/lib/pricingMethodTotals'
|
||||
import { getPricingMethodTotalsForService, getPricingMethodTotalsForServices } from '@/lib/pricingMethodTotals'
|
||||
import { ZXFW_RELOAD_SERVICE_KEY } from '@/lib/zxFwPricingSync'
|
||||
import { Pencil, Eraser, Trash2 } from 'lucide-vue-next'
|
||||
import {
|
||||
@ -25,11 +21,18 @@ import {
|
||||
AlertDialogPortal,
|
||||
AlertDialogRoot,
|
||||
AlertDialogTitle,
|
||||
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogOverlay,
|
||||
DialogPortal,
|
||||
DialogRoot,
|
||||
DialogTitle,
|
||||
DialogTrigger
|
||||
} from 'reka-ui'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { TooltipProvider } from '@/components/ui/tooltip'
|
||||
import { getServiceDictEntries, isIndustryEnabledByType,getIndustryTypeValue } from '@/sql'
|
||||
import { getServiceDictEntries } from '@/sql'
|
||||
import { useTabStore } from '@/pinia/tab'
|
||||
import { usePricingPaneReloadStore } from '@/pinia/pricingPaneReload'
|
||||
import ServiceCheckboxSelector from '@/components/views/ServiceCheckboxSelector.vue'
|
||||
@ -38,7 +41,6 @@ interface ServiceItem {
|
||||
id: string
|
||||
code: string
|
||||
name: string
|
||||
type: ServiceMethodType
|
||||
}
|
||||
|
||||
interface DetailRow {
|
||||
@ -59,17 +61,6 @@ interface ZxFwState {
|
||||
detailRows: DetailRow[]
|
||||
}
|
||||
|
||||
interface XmBaseInfoState {
|
||||
projectIndustry?: string
|
||||
}
|
||||
|
||||
interface ServiceMethodType {
|
||||
scale?: boolean | null
|
||||
onlyCostScale?: boolean | null
|
||||
amount?: boolean | null
|
||||
workDay?: boolean | null
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
contractId: string
|
||||
contractName?: string
|
||||
@ -77,66 +68,25 @@ const props = defineProps<{
|
||||
const tabStore = useTabStore()
|
||||
const pricingPaneReloadStore = usePricingPaneReloadStore()
|
||||
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_FORCE_DEFAULT_PREFIX = 'pricing-force-default:'
|
||||
const PRICING_CLEAR_SKIP_TTL_MS = 5000
|
||||
const PRICING_TOTALS_OPTIONS = { excludeInvestmentCostAndAreaRows: true } as const
|
||||
const projectIndustry = ref('')
|
||||
|
||||
type ServiceListItem = {
|
||||
code?: string
|
||||
ref?: string
|
||||
name: string
|
||||
defCoe: number | null
|
||||
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()
|
||||
type ServiceListItem = { code?: string; ref?: string; name: string; defCoe: number | null }
|
||||
const serviceDict: ServiceItem[] = getServiceDictEntries()
|
||||
.map(({ id, item }) => ({ id, item: item as ServiceListItem }))
|
||||
.filter(({ item }) => {
|
||||
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,
|
||||
code: item.code || item.ref || '',
|
||||
name: item.name,
|
||||
type: {
|
||||
scale: toNullableBoolean(item.scale),
|
||||
onlyCostScale: toNullableBoolean(item.onlyCostScale),
|
||||
amount: toNullableBoolean(item.amount),
|
||||
workDay: toNullableBoolean(item.workDay)
|
||||
}
|
||||
name: item.name
|
||||
}))
|
||||
})
|
||||
|
||||
const serviceById = computed(() => new Map(serviceDict.value.map(item => [item.id, item])))
|
||||
const serviceIdByCode = computed(() => new Map(serviceDict.value.map(item => [item.code, item.id])))
|
||||
const serviceIdSignature = computed(() => serviceDict.value.map(item => item.id).join('|'))
|
||||
const serviceById = new Map(serviceDict.map(item => [item.id, item]))
|
||||
const serviceIdByCode = new Map(serviceDict.map(item => [item.code, item.id]))
|
||||
const fixedBudgetRow: Pick<DetailRow, 'id' | 'code' | 'name'> = { id: 'fixed-budget-c', code: 'C', name: '合同预算' }
|
||||
const isFixedRow = (row?: DetailRow | null) => row?.id === fixedBudgetRow.id
|
||||
|
||||
@ -217,7 +167,7 @@ const startDragAutoScroll = () => {
|
||||
const selectedServiceText = computed(() => {
|
||||
if (selectedIds.value.length === 0) return ''
|
||||
const names = selectedIds.value
|
||||
.map(id => serviceById.value.get(id)?.name || '')
|
||||
.map(id => serviceById.get(id)?.name || '')
|
||||
.filter(Boolean)
|
||||
if (names.length <= 2) return names.join('、')
|
||||
return `${names.slice(0, 2).join('、')} 等 ${names.length} 项`
|
||||
@ -227,7 +177,7 @@ const pendingClearServiceName = computed(() => {
|
||||
if (!pendingClearServiceId.value) return ''
|
||||
const row = detailRows.value.find(item => item.id === pendingClearServiceId.value)
|
||||
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}`
|
||||
return pendingClearServiceId.value
|
||||
})
|
||||
@ -236,7 +186,7 @@ const pendingDeleteServiceName = computed(() => {
|
||||
if (!pendingDeleteServiceId.value) return ''
|
||||
const row = detailRows.value.find(item => item.id === pendingDeleteServiceId.value)
|
||||
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}`
|
||||
return pendingDeleteServiceId.value
|
||||
})
|
||||
@ -291,8 +241,8 @@ const confirmDeleteRow = async () => {
|
||||
|
||||
const filteredServiceDict = computed(() => {
|
||||
const keyword = pickerSearch.value.trim()
|
||||
if (!keyword) return serviceDict.value
|
||||
return serviceDict.value.filter(item => item.code.includes(keyword) || item.name.includes(keyword))
|
||||
if (!keyword) return serviceDict
|
||||
return serviceDict.filter(item => item.code.includes(keyword) || item.name.includes(keyword))
|
||||
})
|
||||
|
||||
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 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 = (
|
||||
rows: DetailRow[],
|
||||
field: 'investScale' | 'landScale' | 'workload' | 'hourly'
|
||||
@ -397,19 +313,17 @@ const clearRowValues = async (row: DetailRow) => {
|
||||
await clearPricingPaneValues(row.id)
|
||||
const totals = await getPricingMethodTotalsForService({
|
||||
contractId: props.contractId,
|
||||
serviceId: row.id,
|
||||
options: PRICING_TOTALS_OPTIONS
|
||||
serviceId: row.id
|
||||
})
|
||||
const sanitizedTotals = sanitizePricingTotalsByService(row.id, totals)
|
||||
const clearedRows = detailRows.value.map(item =>
|
||||
item.id !== row.id
|
||||
? item
|
||||
: {
|
||||
...item,
|
||||
investScale: sanitizedTotals.investScale,
|
||||
landScale: sanitizedTotals.landScale,
|
||||
workload: sanitizedTotals.workload,
|
||||
hourly: sanitizedTotals.hourly
|
||||
investScale: totals.investScale,
|
||||
landScale: totals.landScale,
|
||||
workload: totals.workload,
|
||||
hourly: totals.hourly
|
||||
}
|
||||
)
|
||||
const nextInvestScale = getMethodTotalFromRows(clearedRows, 'investScale')
|
||||
@ -432,7 +346,6 @@ const clearRowValues = async (row: DetailRow) => {
|
||||
}
|
||||
|
||||
const openEditTab = (row: DetailRow) => {
|
||||
const serviceType = serviceById.value.get(row.id)?.type
|
||||
tabStore.openTab({
|
||||
id: `zxfw-edit-${props.contractId}-${row.id}`,
|
||||
title: `服务编辑-${row.code}${row.name}`,
|
||||
@ -441,8 +354,7 @@ const openEditTab = (row: DetailRow) => {
|
||||
contractId: props.contractId,
|
||||
contractName: props.contractName || '',
|
||||
serviceId: row.id,
|
||||
fwName: row.code + row.name,
|
||||
type: serviceType ? { ...serviceType } : undefined
|
||||
fwName: row.code + row.name
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -650,15 +562,13 @@ const fillPricingTotalsForServiceIds = async (serviceIds: string[]) => {
|
||||
|
||||
const totalsByServiceId = await getPricingMethodTotalsForServices({
|
||||
contractId: props.contractId,
|
||||
serviceIds: targetIds,
|
||||
options: PRICING_TOTALS_OPTIONS
|
||||
serviceIds: targetIds
|
||||
})
|
||||
|
||||
const targetSet = new Set(targetIds.map(id => String(id)))
|
||||
const nextRows = detailRows.value.map(row => {
|
||||
if (isFixedRow(row) || !targetSet.has(String(row.id))) return row
|
||||
const totalsRaw = totalsByServiceId.get(String(row.id))
|
||||
const totals = totalsRaw ? sanitizePricingTotalsByService(String(row.id), totalsRaw) : null
|
||||
const totals = totalsByServiceId.get(String(row.id))
|
||||
if (!totals) return row
|
||||
return {
|
||||
...row,
|
||||
@ -675,35 +585,29 @@ const fillPricingTotalsForServiceIds = async (serviceIds: string[]) => {
|
||||
const applySelection = (codes: string[]) => {
|
||||
const prevSelectedSet = new Set(selectedIds.value)
|
||||
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 baseRows: DetailRow[] = uniqueIds
|
||||
.map(id => {
|
||||
const dictItem = serviceById.value.get(id)
|
||||
const dictItem = serviceById.get(id)
|
||||
if (!dictItem) return null
|
||||
|
||||
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 {
|
||||
id: old?.id || id,
|
||||
code: dictItem.code,
|
||||
name: dictItem.name,
|
||||
investScale: nextValues.investScale,
|
||||
landScale: nextValues.landScale,
|
||||
workload: nextValues.workload,
|
||||
hourly: nextValues.hourly
|
||||
investScale: old?.investScale ?? null,
|
||||
landScale: old?.landScale ?? null,
|
||||
workload: old?.workload ?? null,
|
||||
hourly: old?.hourly ?? null
|
||||
}
|
||||
})
|
||||
.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))
|
||||
|
||||
const fixedOld = existingMap.get(fixedBudgetRow.id)
|
||||
@ -826,7 +730,6 @@ const applyDragSelectionByRect = () => {
|
||||
}
|
||||
|
||||
pickerTempIds.value = serviceDict
|
||||
.value
|
||||
.map(item => item.id)
|
||||
.filter(id => nextSelectedSet.has(id))
|
||||
}
|
||||
@ -894,25 +797,19 @@ const loadFromIndexedDB = async () => {
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
const savedRowMap = new Map((data.detailRows || []).map(row => [row.id, row]))
|
||||
detailRows.value = detailRows.value.map(row => {
|
||||
const old = savedRowMap.get(row.id)
|
||||
if (!old) return row
|
||||
const nextValues = sanitizePricingFieldsByService(row.id, {
|
||||
return {
|
||||
...row,
|
||||
investScale: typeof old.investScale === 'number' ? old.investScale : null,
|
||||
landScale: typeof old.landScale === 'number' ? old.landScale : null,
|
||||
workload: typeof old.workload === 'number' ? old.workload : 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)
|
||||
@ -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(
|
||||
() => pricingPaneReloadStore.getReloadVersion(props.contractId, ZXFW_RELOAD_SERVICE_KEY),
|
||||
(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
|
||||
const handleCellValueChanged = () => {
|
||||
if (gridPersistTimer) clearTimeout(gridPersistTimer)
|
||||
@ -960,12 +837,6 @@ const handleCellValueChanged = () => {
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await loadProjectIndustry()
|
||||
await loadFromIndexedDB()
|
||||
})
|
||||
|
||||
onActivated(async () => {
|
||||
await loadProjectIndustry()
|
||||
await loadFromIndexedDB()
|
||||
})
|
||||
|
||||
|
||||
@ -53,14 +53,10 @@ interface HourlyRow {
|
||||
interface MajorLite {
|
||||
code: string
|
||||
defCoe: number | null
|
||||
hasCost?: boolean
|
||||
hasArea?: boolean
|
||||
industryId?: string | number | null
|
||||
}
|
||||
|
||||
interface ServiceLite {
|
||||
defCoe: number | null
|
||||
onlyCostScale?: boolean | null
|
||||
}
|
||||
|
||||
interface TaskLite {
|
||||
@ -74,10 +70,6 @@ interface ExpertLite {
|
||||
manageCoe: number | null
|
||||
}
|
||||
|
||||
interface XmBaseInfoState {
|
||||
projectIndustry?: string
|
||||
}
|
||||
|
||||
export interface PricingMethodTotals {
|
||||
investScale: number | null
|
||||
landScale: number | null
|
||||
@ -85,12 +77,6 @@ export interface PricingMethodTotals {
|
||||
hourly: number | null
|
||||
}
|
||||
|
||||
interface PricingMethodTotalsOptions {
|
||||
excludeInvestmentCostAndAreaRows?: boolean
|
||||
}
|
||||
|
||||
const ONLY_COST_SCALE_ROW_ID = '__only-cost-scale-total__'
|
||||
|
||||
const hasOwn = (obj: unknown, key: string) =>
|
||||
Object.prototype.hasOwnProperty.call(obj || {}, key)
|
||||
|
||||
@ -107,11 +93,6 @@ const getDefaultConsultCategoryFactor = (serviceId: string | number) => {
|
||||
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 majorIdAliasMap = getMajorIdAliasMap()
|
||||
|
||||
@ -121,27 +102,6 @@ const getDefaultMajorFactorById = (id: string) => {
|
||||
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 = (
|
||||
row: { budgetValue?: number | null; standardFactor?: number | null } | undefined,
|
||||
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) => {
|
||||
return getScaleBudgetFee({
|
||||
benchmarkBudget: getBenchmarkBudgetByLandArea(row.landArea),
|
||||
@ -445,46 +373,30 @@ const resolveScaleRows = (
|
||||
export const getPricingMethodTotalsForService = async (params: {
|
||||
contractId: string
|
||||
serviceId: string | number
|
||||
options?: PricingMethodTotalsOptions
|
||||
}): Promise<PricingMethodTotals> => {
|
||||
const serviceId = String(params.serviceId)
|
||||
const htDbKey = `ht-info-v3-${params.contractId}`
|
||||
const consultFactorDbKey = `ht-consult-category-factor-v1-${params.contractId}`
|
||||
const majorFactorDbKey = `ht-major-factor-v1-${params.contractId}`
|
||||
const baseInfoDbKey = 'xm-base-info-v1'
|
||||
const investDbKey = `tzGMF-${params.contractId}-${serviceId}`
|
||||
const landDbKey = `ydGMF-${params.contractId}-${serviceId}`
|
||||
const workloadDbKey = `gzlF-${params.contractId}-${serviceId}`
|
||||
const hourlyDbKey = `hourlyPricing-${params.contractId}-${serviceId}`
|
||||
|
||||
const [investData, landData, workloadData, hourlyData, 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>(landDbKey),
|
||||
localforage.getItem<StoredDetailRowsState>(workloadDbKey),
|
||||
localforage.getItem<StoredDetailRowsState>(hourlyDbKey),
|
||||
localforage.getItem<StoredDetailRowsState>(htDbKey),
|
||||
localforage.getItem<StoredFactorState>(consultFactorDbKey),
|
||||
localforage.getItem<StoredFactorState>(majorFactorDbKey),
|
||||
localforage.getItem<XmBaseInfoState>(baseInfoDbKey)
|
||||
localforage.getItem<StoredFactorState>(majorFactorDbKey)
|
||||
])
|
||||
|
||||
const consultCategoryFactorMap = buildConsultCategoryFactorMap(consultFactorData)
|
||||
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(
|
||||
serviceId,
|
||||
investData,
|
||||
@ -492,11 +404,7 @@ export const getPricingMethodTotalsForService = async (params: {
|
||||
consultCategoryFactorMap,
|
||||
majorFactorMap
|
||||
)
|
||||
return sumByNumber(investRows, row => {
|
||||
if (excludeInvestmentCostAndAreaRows && isDualScaleMajorById(row.id)) return null
|
||||
return getInvestmentBudgetFee(row)
|
||||
})
|
||||
})()
|
||||
const investScale = sumByNumber(investRows, row => getInvestmentBudgetFee(row))
|
||||
|
||||
const landRows = resolveScaleRows(
|
||||
serviceId,
|
||||
@ -535,15 +443,13 @@ export const getPricingMethodTotalsForService = async (params: {
|
||||
export const getPricingMethodTotalsForServices = async (params: {
|
||||
contractId: string
|
||||
serviceIds: Array<string | number>
|
||||
options?: PricingMethodTotalsOptions
|
||||
}) => {
|
||||
const result = new Map<string, PricingMethodTotals>()
|
||||
await Promise.all(
|
||||
params.serviceIds.map(async serviceId => {
|
||||
const totals = await getPricingMethodTotalsForService({
|
||||
contractId: params.contractId,
|
||||
serviceId,
|
||||
options: params.options
|
||||
serviceId
|
||||
})
|
||||
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)
|
||||
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 = {
|
||||
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 },
|
||||
@ -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 },
|
||||
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 },
|
||||
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 },
|
||||
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 },
|
||||
@ -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 },
|
||||
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 },
|
||||
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 },
|
||||
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 },
|
||||
@ -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 },
|
||||
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 },
|
||||
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 },
|
||||
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 },
|
||||
@ -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']
|
||||
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>)
|
||||
|
||||
/**
|
||||
* 判断字典项是否属于某行业(基于 isRoad/isRailway/isWaterway)。
|
||||
* @returns 是否属于该行业
|
||||
*/
|
||||
export const isDictItemInIndustryScope = (item: DictItem | undefined, industryCode: unknown): boolean =>
|
||||
isIndustryEnabledByType(item, getIndustryTypeValue(industryCode))
|
||||
|
||||
/**
|
||||
* 构建“专业ID -> 专业项”映射。
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user