fix
This commit is contained in:
parent
ad4e9cdee0
commit
043e1fc879
@ -123,6 +123,23 @@ const loadFromIndexedDB = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const columnDefs: ColDef<FeeRow>[] = [
|
const columnDefs: ColDef<FeeRow>[] = [
|
||||||
|
{
|
||||||
|
headerName: '序号',
|
||||||
|
colId: 'rowNo',
|
||||||
|
minWidth: 68,
|
||||||
|
maxWidth: 80,
|
||||||
|
flex: 0.6,
|
||||||
|
editable: false,
|
||||||
|
sortable: false,
|
||||||
|
filter: false,
|
||||||
|
cellStyle: { textAlign: 'center' },
|
||||||
|
valueGetter: params =>
|
||||||
|
params.node?.rowPinned
|
||||||
|
? ''
|
||||||
|
: typeof params.node?.rowIndex === 'number'
|
||||||
|
? params.node.rowIndex + 1
|
||||||
|
: ''
|
||||||
|
},
|
||||||
{
|
{
|
||||||
headerName: '费用项',
|
headerName: '费用项',
|
||||||
field: 'feeItem',
|
field: 'feeItem',
|
||||||
|
|||||||
@ -1,14 +1,55 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { serviceList } from '@/sql'
|
import { computed, onActivated, onMounted, ref } from 'vue'
|
||||||
|
import localforage from 'localforage'
|
||||||
|
import { getServiceDictEntries, isIndustryEnabledByType, getIndustryTypeValue } from '@/sql'
|
||||||
import XmFactorGrid from '@/components/common/XmFactorGrid.vue'
|
import XmFactorGrid from '@/components/common/XmFactorGrid.vue'
|
||||||
|
|
||||||
|
interface XmBaseInfoState {
|
||||||
|
projectIndustry?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ServiceItem = {
|
||||||
|
code: string
|
||||||
|
name: string
|
||||||
|
defCoe: number | null
|
||||||
|
desc?: string | null
|
||||||
|
notshowByzxflxs?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const PROJECT_INFO_KEY = 'xm-base-info-v1'
|
||||||
|
const projectIndustry = ref('')
|
||||||
|
|
||||||
|
const loadProjectIndustry = async () => {
|
||||||
|
try {
|
||||||
|
const data = await localforage.getItem<XmBaseInfoState>(PROJECT_INFO_KEY)
|
||||||
|
projectIndustry.value =
|
||||||
|
typeof data?.projectIndustry === 'string' ? data.projectIndustry.trim() : ''
|
||||||
|
} catch (error) {
|
||||||
|
console.error('loadProjectIndustry failed:', error)
|
||||||
|
projectIndustry.value = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const filteredServiceDict = computed<Record<string, ServiceItem>>(() => {
|
||||||
|
const industry = projectIndustry.value
|
||||||
|
if (!industry) return {}
|
||||||
|
const entries = getServiceDictEntries()
|
||||||
|
.filter(({ item }) => isIndustryEnabledByType(item, getIndustryTypeValue(industry))
|
||||||
|
)
|
||||||
|
.map(({ id, item }) => [id, item as ServiceItem] as const)
|
||||||
|
return Object.fromEntries(entries)
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
void loadProjectIndustry()
|
||||||
|
})
|
||||||
|
|
||||||
|
onActivated(() => {
|
||||||
|
void loadProjectIndustry()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<XmFactorGrid
|
<XmFactorGrid title="咨询分类系数明细" storage-key="xm-consult-category-factor-v1" :dict="filteredServiceDict"
|
||||||
title="咨询分类系数明细"
|
:disable-budget-edit-when-standard-null="true" :exclude-notshow-by-zxflxs="true" />
|
||||||
storage-key="xm-consult-category-factor-v1"
|
|
||||||
:dict="serviceList"
|
|
||||||
:disable-budget-edit-when-standard-null="true"
|
|
||||||
:exclude-notshow-by-zxflxs="true"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -5,20 +5,29 @@
|
|||||||
:subtitle="`合同ID:${contractId}`"
|
:subtitle="`合同ID:${contractId}`"
|
||||||
:copy-text="contractId"
|
:copy-text="contractId"
|
||||||
:storage-key="`zxfw-pricing-active-cat-${contractId}-${serviceId}`"
|
:storage-key="`zxfw-pricing-active-cat-${contractId}-${serviceId}`"
|
||||||
default-category="investment-scale-method"
|
:default-category="defaultCategory"
|
||||||
:categories="pricingCategories"
|
:categories="pricingCategories"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { defineAsyncComponent, defineComponent, h, markRaw, type Component } from 'vue'
|
import { computed, defineAsyncComponent, defineComponent, h, markRaw, type Component } from 'vue'
|
||||||
import TypeLine from '@/layout/typeLine.vue'
|
import TypeLine from '@/layout/typeLine.vue'
|
||||||
|
import MethodUnavailableNotice from '@/components/common/MethodUnavailableNotice.vue'
|
||||||
|
|
||||||
|
interface ServiceMethodType {
|
||||||
|
scale?: boolean | null
|
||||||
|
onlyCostScale?: boolean | null
|
||||||
|
amount?: boolean | null
|
||||||
|
workDay?: boolean | null
|
||||||
|
}
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
contractId: string
|
contractId: string
|
||||||
contractName?: string
|
contractName?: string
|
||||||
serviceId: string|number
|
serviceId: string|number
|
||||||
fwName:string
|
fwName:string
|
||||||
|
type?: ServiceMethodType
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
interface PricingCategoryItem {
|
interface PricingCategoryItem {
|
||||||
@ -27,6 +36,22 @@ interface PricingCategoryItem {
|
|||||||
component: Component
|
component: Component
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const resolveMethodEnabled = (value: boolean | null | undefined, fallback = true) =>
|
||||||
|
typeof value === 'boolean' ? value : fallback
|
||||||
|
|
||||||
|
const methodAvailability = computed(() => {
|
||||||
|
const scale = resolveMethodEnabled(props.type?.scale, true)
|
||||||
|
const onlyCostScale = resolveMethodEnabled(props.type?.onlyCostScale, false)
|
||||||
|
const amount = resolveMethodEnabled(props.type?.amount, true)
|
||||||
|
const workDay = resolveMethodEnabled(props.type?.workDay, true)
|
||||||
|
return {
|
||||||
|
investmentScale: scale,
|
||||||
|
landScale: scale && !onlyCostScale,
|
||||||
|
workload: amount,
|
||||||
|
hourly: workDay
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const createPricingPane = (name: string) =>
|
const createPricingPane = (name: string) =>
|
||||||
markRaw(
|
markRaw(
|
||||||
defineComponent({
|
defineComponent({
|
||||||
@ -44,15 +69,66 @@ const createPricingPane = (name: string) =>
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const createMethodUnavailablePane = (title: string, message: string) =>
|
||||||
|
markRaw(
|
||||||
|
defineComponent({
|
||||||
|
name: 'MethodUnavailablePane',
|
||||||
|
setup() {
|
||||||
|
return () => h(MethodUnavailableNotice, { title, message })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
const investmentScaleView = createPricingPane('InvestmentScalePricingPane')
|
const investmentScaleView = createPricingPane('InvestmentScalePricingPane')
|
||||||
const landScaleView = createPricingPane('LandScalePricingPane')
|
const landScaleView = createPricingPane('LandScalePricingPane')
|
||||||
const workloadView = createPricingPane('WorkloadPricingPane')
|
const workloadView = createPricingPane('WorkloadPricingPane')
|
||||||
const hourlyView = createPricingPane('HourlyPricingPane')
|
const hourlyView = createPricingPane('HourlyPricingPane')
|
||||||
|
|
||||||
const pricingCategories: PricingCategoryItem[] = [
|
const investmentScaleUnavailableView = createMethodUnavailablePane(
|
||||||
{ key: 'investment-scale-method', label: '投资规模法', component: investmentScaleView },
|
'该服务不适用投资规模法',
|
||||||
{ key: 'land-scale-method', label: '用地规模法', component: landScaleView },
|
'当前服务未启用规模法,投资规模法不可编辑。'
|
||||||
{ key: 'workload-method', label: '工作量法', component: workloadView },
|
)
|
||||||
{ key: 'hourly-method', label: '工时法', component: hourlyView }
|
const landScaleUnavailableView = createMethodUnavailablePane(
|
||||||
]
|
'该服务不适用用地规模法',
|
||||||
|
'当前服务仅支持投资规模法,用地规模法不可编辑。'
|
||||||
|
)
|
||||||
|
const workloadUnavailableView = createMethodUnavailablePane(
|
||||||
|
'该服务不适用工作量法',
|
||||||
|
'当前服务未启用工作量法,工作量法不可编辑。'
|
||||||
|
)
|
||||||
|
const hourlyUnavailableView = createMethodUnavailablePane(
|
||||||
|
'该服务不适用工时法',
|
||||||
|
'当前服务未启用工时法,工时法不可编辑。'
|
||||||
|
)
|
||||||
|
|
||||||
|
const pricingCategories = computed<PricingCategoryItem[]>(() => [
|
||||||
|
{
|
||||||
|
key: 'investment-scale-method',
|
||||||
|
label: '投资规模法',
|
||||||
|
component: methodAvailability.value.investmentScale ? investmentScaleView : investmentScaleUnavailableView
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'land-scale-method',
|
||||||
|
label: '用地规模法',
|
||||||
|
component: methodAvailability.value.landScale ? landScaleView : landScaleUnavailableView
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'workload-method',
|
||||||
|
label: '工作量法',
|
||||||
|
component: methodAvailability.value.workload ? workloadView : workloadUnavailableView
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'hourly-method',
|
||||||
|
label: '工时法',
|
||||||
|
component: methodAvailability.value.hourly ? hourlyView : hourlyUnavailableView
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
const defaultCategory = computed(() => {
|
||||||
|
if (methodAvailability.value.investmentScale) return 'investment-scale-method'
|
||||||
|
if (methodAvailability.value.landScale) return 'land-scale-method'
|
||||||
|
if (methodAvailability.value.workload) return 'workload-method'
|
||||||
|
if (methodAvailability.value.hourly) return 'hourly-method'
|
||||||
|
return 'investment-scale-method'
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -124,9 +124,10 @@ const xmCategories: XmCategoryItem[] = [
|
|||||||
{ key: 'info', label: '规模信息', component: htView },
|
{ key: 'info', label: '规模信息', component: htView },
|
||||||
{ key: 'consult-category-factor', label: '咨询分类系数', component: consultCategoryFactorView },
|
{ key: 'consult-category-factor', label: '咨询分类系数', component: consultCategoryFactorView },
|
||||||
{ key: 'major-factor', label: '工程专业系数', component: majorFactorView },
|
{ key: 'major-factor', label: '工程专业系数', component: majorFactorView },
|
||||||
|
{ key: 'contract', label: '咨询服务', component: zxfwView },
|
||||||
|
|
||||||
{ key: 'additional-work-fee', label: '附加工作费', component: additionalWorkFeeView },
|
{ key: 'additional-work-fee', label: '附加工作费', component: additionalWorkFeeView },
|
||||||
{ key: 'reserve-fee', label: '预备费', component: reserveFeeView },
|
{ key: 'reserve-fee', label: '预备费', component: reserveFeeView },
|
||||||
{ key: 'contract', label: '咨询服务', component: zxfwView },
|
|
||||||
|
|
||||||
];
|
];
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { computed, onActivated, onBeforeUnmount, onMounted, ref, watch } from 'v
|
|||||||
import { AgGridVue } from 'ag-grid-vue3'
|
import { AgGridVue } from 'ag-grid-vue3'
|
||||||
import type { ColDef, ColGroupDef } from 'ag-grid-community'
|
import type { ColDef, ColGroupDef } from 'ag-grid-community'
|
||||||
import localforage from 'localforage'
|
import localforage from 'localforage'
|
||||||
import { getMajorDictEntries, isMajorIdInIndustryScope } from '@/sql'
|
import { getMajorDictEntries, getServiceDictItemById, isMajorIdInIndustryScope } from '@/sql'
|
||||||
import { myTheme, gridOptions } from '@/lib/diyAgGridOptions'
|
import { myTheme, gridOptions } from '@/lib/diyAgGridOptions'
|
||||||
import { decimalAggSum, roundTo, sumByNumber } from '@/lib/decimal'
|
import { decimalAggSum, roundTo, sumByNumber } from '@/lib/decimal'
|
||||||
import { formatThousands } from '@/lib/numberFormat'
|
import { formatThousands } from '@/lib/numberFormat'
|
||||||
@ -78,6 +78,10 @@ interface XmBaseInfoState {
|
|||||||
projectIndustry?: string
|
projectIndustry?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ServiceLite {
|
||||||
|
onlyCostScale?: boolean | null
|
||||||
|
}
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
contractId: string,
|
contractId: string,
|
||||||
serviceId: string | number
|
serviceId: string | number
|
||||||
@ -95,11 +99,16 @@ const consultCategoryFactorMap = ref<Map<string, number | null>>(new Map())
|
|||||||
const majorFactorMap = ref<Map<string, number | null>>(new Map())
|
const majorFactorMap = ref<Map<string, number | null>>(new Map())
|
||||||
let factorDefaultsLoaded = false
|
let factorDefaultsLoaded = false
|
||||||
const paneInstanceCreatedAt = Date.now()
|
const paneInstanceCreatedAt = Date.now()
|
||||||
|
const ONLY_COST_SCALE_ROW_ID = '__only-cost-scale-total__'
|
||||||
|
|
||||||
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 loadFactorDefaults = async () => {
|
const loadFactorDefaults = async () => {
|
||||||
const [consultMap, majorMap] = await Promise.all([
|
const [consultMap, majorMap] = await Promise.all([
|
||||||
@ -184,12 +193,17 @@ const detailDict: DictGroup[] = (() => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hasCost = item.hasCost !== false
|
||||||
|
const hasArea = item.hasArea !== false
|
||||||
|
// 特殊规则:投资规模法中,hasCost && hasArea 的专业不参与明细行
|
||||||
|
if (hasCost && hasArea) continue
|
||||||
|
|
||||||
groupMap.get(parentCode)!.children.push({
|
groupMap.get(parentCode)!.children.push({
|
||||||
id: key,
|
id: key,
|
||||||
code,
|
code,
|
||||||
name: item.name,
|
name: item.name,
|
||||||
hasCost: item.hasCost !== false,
|
hasCost,
|
||||||
hasArea: item.hasArea !== false
|
hasArea
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -241,6 +255,48 @@ const buildDefaultRows = (): DetailRow[] => {
|
|||||||
return rows
|
return rows
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const calcOnlyCostScaleAmountFromRows = (rows?: Array<{ amount?: unknown }>) =>
|
||||||
|
sumByNumber(rows || [], row => (typeof row?.amount === 'number' ? row.amount : null))
|
||||||
|
|
||||||
|
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 : 1,
|
||||||
|
workStageFactor: typeof fromDb?.workStageFactor === 'number' ? fromDb.workStageFactor : 1,
|
||||||
|
workRatio: typeof fromDb?.workRatio === 'number' ? fromDb.workRatio : 100,
|
||||||
|
budgetFee: null,
|
||||||
|
budgetFeeBasic: null,
|
||||||
|
budgetFeeOptional: null,
|
||||||
|
remark: typeof fromDb?.remark === 'string' ? fromDb.remark : '',
|
||||||
|
path: [ONLY_COST_SCALE_ROW_ID]
|
||||||
|
})
|
||||||
|
|
||||||
|
const buildOnlyCostScaleRows = (
|
||||||
|
rowsFromDb?: Array<Partial<DetailRow> & Pick<DetailRow, 'id'>>
|
||||||
|
): DetailRow[] => {
|
||||||
|
const totalAmount = calcOnlyCostScaleAmountFromRows(rowsFromDb)
|
||||||
|
const onlyRow = rowsFromDb?.find(row => String(row.id) === ONLY_COST_SCALE_ROW_ID)
|
||||||
|
return [buildOnlyCostScaleRow(totalAmount, onlyRow)]
|
||||||
|
}
|
||||||
|
|
||||||
type SourceRow = Pick<DetailRow, 'id'> &
|
type SourceRow = Pick<DetailRow, 'id'> &
|
||||||
Partial<
|
Partial<
|
||||||
Pick<
|
Pick<
|
||||||
@ -340,6 +396,11 @@ const formatMajorFactor = (params: any) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const formatEditableMoney = (params: any) => {
|
const formatEditableMoney = (params: any) => {
|
||||||
|
if (isOnlyCostScaleService.value) {
|
||||||
|
if (!params.node?.rowPinned) return ''
|
||||||
|
if (params.value == null || params.value === '') return '点击输入'
|
||||||
|
return formatThousands(params.value)
|
||||||
|
}
|
||||||
if (!params.node?.group && !params.node?.rowPinned && !params.data?.hasCost) {
|
if (!params.node?.group && !params.node?.rowPinned && !params.data?.hasCost) {
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
@ -432,14 +493,21 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
|
|||||||
minWidth: 90,
|
minWidth: 90,
|
||||||
flex: 2,
|
flex: 2,
|
||||||
|
|
||||||
editable: params => !params.node?.group && !params.node?.rowPinned && Boolean(params.data?.hasCost),
|
editable: params => {
|
||||||
|
if (isOnlyCostScaleService.value) return Boolean(params.node?.rowPinned)
|
||||||
|
return !params.node?.group && !params.node?.rowPinned && Boolean(params.data?.hasCost)
|
||||||
|
},
|
||||||
cellClass: params =>
|
cellClass: params =>
|
||||||
!params.node?.group && !params.node?.rowPinned && params.data?.hasCost
|
isOnlyCostScaleService.value && params.node?.rowPinned
|
||||||
|
? 'ag-right-aligned-cell editable-cell-line'
|
||||||
|
: !params.node?.group && !params.node?.rowPinned && params.data?.hasCost
|
||||||
? 'ag-right-aligned-cell editable-cell-line'
|
? 'ag-right-aligned-cell editable-cell-line'
|
||||||
: 'ag-right-aligned-cell',
|
: 'ag-right-aligned-cell',
|
||||||
cellClassRules: {
|
cellClassRules: {
|
||||||
'editable-cell-empty': params =>
|
'editable-cell-empty': params =>
|
||||||
!params.node?.group && !params.node?.rowPinned && Boolean(params.data?.hasCost) && (params.value == null || params.value === '')
|
isOnlyCostScaleService.value && params.node?.rowPinned
|
||||||
|
? params.value == null || params.value === ''
|
||||||
|
: !params.node?.group && !params.node?.rowPinned && Boolean(params.data?.hasCost) && (params.value == null || params.value === '')
|
||||||
},
|
},
|
||||||
aggFunc: decimalAggSum,
|
aggFunc: decimalAggSum,
|
||||||
valueParser: params => parseNumberOrNull(params.newValue, { precision: 2 }),
|
valueParser: params => parseNumberOrNull(params.newValue, { precision: 2 }),
|
||||||
@ -613,13 +681,13 @@ const autoGroupColumnDef: ColDef = {
|
|||||||
},
|
},
|
||||||
valueFormatter: params => {
|
valueFormatter: params => {
|
||||||
if (params.node?.rowPinned) {
|
if (params.node?.rowPinned) {
|
||||||
return '总合计'
|
return isOnlyCostScaleService.value ? '总投资' : '总合计'
|
||||||
}
|
}
|
||||||
const nodeId = String(params.value || '')
|
const nodeId = String(params.value || '')
|
||||||
return idLabelMap.get(nodeId) || nodeId
|
return idLabelMap.get(nodeId) || nodeId
|
||||||
},
|
},
|
||||||
tooltipValueGetter: params => {
|
tooltipValueGetter: params => {
|
||||||
if (params.node?.rowPinned) return '总合计'
|
if (params.node?.rowPinned) return isOnlyCostScaleService.value ? '总投资' : '总合计'
|
||||||
const nodeId = String(params.value || '')
|
const nodeId = String(params.value || '')
|
||||||
return idLabelMap.get(nodeId) || nodeId
|
return idLabelMap.get(nodeId) || nodeId
|
||||||
}
|
}
|
||||||
@ -627,6 +695,7 @@ 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 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))
|
||||||
@ -727,6 +796,9 @@ const loadFromIndexedDB = async () => {
|
|||||||
const applyContractDefaultRows = async () => {
|
const applyContractDefaultRows = async () => {
|
||||||
const htData = await localforage.getItem<{ detailRows: SourceRow[] }>(HT_DB_KEY.value)
|
const htData = await localforage.getItem<{ detailRows: SourceRow[] }>(HT_DB_KEY.value)
|
||||||
const hasContractRows = Array.isArray(htData?.detailRows) && htData.detailRows.length > 0
|
const hasContractRows = Array.isArray(htData?.detailRows) && htData.detailRows.length > 0
|
||||||
|
if (isOnlyCostScaleService.value) {
|
||||||
|
detailRows.value = hasContractRows ? buildOnlyCostScaleRows(htData!.detailRows as any) : buildOnlyCostScaleRows()
|
||||||
|
} else {
|
||||||
detailRows.value = hasContractRows
|
detailRows.value = hasContractRows
|
||||||
? mergeWithDictRows(htData!.detailRows, { includeFactorValues: true })
|
? mergeWithDictRows(htData!.detailRows, { includeFactorValues: true })
|
||||||
: buildDefaultRows().map(row => ({
|
: buildDefaultRows().map(row => ({
|
||||||
@ -734,6 +806,7 @@ const loadFromIndexedDB = async () => {
|
|||||||
consultCategoryFactor: getDefaultConsultCategoryFactor(),
|
consultCategoryFactor: getDefaultConsultCategoryFactor(),
|
||||||
majorFactor: getDefaultMajorFactorById(row.id)
|
majorFactor: getDefaultMajorFactorById(row.id)
|
||||||
}))
|
}))
|
||||||
|
}
|
||||||
syncComputedValuesToDetailRows()
|
syncComputedValuesToDetailRows()
|
||||||
}
|
}
|
||||||
if (shouldForceDefaultLoad()) {
|
if (shouldForceDefaultLoad()) {
|
||||||
@ -743,7 +816,9 @@ const loadFromIndexedDB = async () => {
|
|||||||
|
|
||||||
const data = await localforage.getItem<XmInfoState>(DB_KEY.value)
|
const data = await localforage.getItem<XmInfoState>(DB_KEY.value)
|
||||||
if (data) {
|
if (data) {
|
||||||
detailRows.value = mergeWithDictRows(data.detailRows)
|
detailRows.value = isOnlyCostScaleService.value
|
||||||
|
? buildOnlyCostScaleRows(data.detailRows as any)
|
||||||
|
: mergeWithDictRows(data.detailRows)
|
||||||
syncComputedValuesToDetailRows()
|
syncComputedValuesToDetailRows()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -751,7 +826,7 @@ const loadFromIndexedDB = async () => {
|
|||||||
await applyContractDefaultRows()
|
await applyContractDefaultRows()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('loadFromIndexedDB failed:', error)
|
console.error('loadFromIndexedDB failed:', error)
|
||||||
detailRows.value = buildDefaultRows()
|
detailRows.value = isOnlyCostScaleService.value ? buildOnlyCostScaleRows() : buildDefaultRows()
|
||||||
syncComputedValuesToDetailRows()
|
syncComputedValuesToDetailRows()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -766,6 +841,9 @@ const importContractData = async () => {
|
|||||||
await loadFactorDefaults()
|
await loadFactorDefaults()
|
||||||
const htData = await localforage.getItem<{ detailRows: SourceRow[] }>(HT_DB_KEY.value)
|
const htData = await localforage.getItem<{ detailRows: SourceRow[] }>(HT_DB_KEY.value)
|
||||||
const hasContractRows = Array.isArray(htData?.detailRows) && htData.detailRows.length > 0
|
const hasContractRows = Array.isArray(htData?.detailRows) && htData.detailRows.length > 0
|
||||||
|
if (isOnlyCostScaleService.value) {
|
||||||
|
detailRows.value = hasContractRows ? buildOnlyCostScaleRows(htData!.detailRows as any) : buildOnlyCostScaleRows()
|
||||||
|
} else {
|
||||||
detailRows.value = hasContractRows
|
detailRows.value = hasContractRows
|
||||||
? mergeWithDictRows(htData!.detailRows, { includeFactorValues: true })
|
? mergeWithDictRows(htData!.detailRows, { includeFactorValues: true })
|
||||||
: buildDefaultRows().map(row => ({
|
: buildDefaultRows().map(row => ({
|
||||||
@ -773,6 +851,7 @@ const importContractData = async () => {
|
|||||||
consultCategoryFactor: getDefaultConsultCategoryFactor(),
|
consultCategoryFactor: getDefaultConsultCategoryFactor(),
|
||||||
majorFactor: getDefaultMajorFactorById(row.id)
|
majorFactor: getDefaultMajorFactorById(row.id)
|
||||||
}))
|
}))
|
||||||
|
}
|
||||||
await saveToIndexedDB()
|
await saveToIndexedDB()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('importContractData failed:', error)
|
console.error('importContractData failed:', error)
|
||||||
@ -781,7 +860,7 @@ const importContractData = async () => {
|
|||||||
|
|
||||||
const clearAllData = async () => {
|
const clearAllData = async () => {
|
||||||
try {
|
try {
|
||||||
detailRows.value = buildDefaultRows()
|
detailRows.value = isOnlyCostScaleService.value ? buildOnlyCostScaleRows() : buildDefaultRows()
|
||||||
await saveToIndexedDB()
|
await saveToIndexedDB()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('clearAllData failed:', error)
|
console.error('clearAllData failed:', error)
|
||||||
@ -802,7 +881,20 @@ let persistTimer: ReturnType<typeof setTimeout> | null = null
|
|||||||
|
|
||||||
|
|
||||||
let gridPersistTimer: ReturnType<typeof setTimeout> | null = null
|
let gridPersistTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
const handleCellValueChanged = () => {
|
const applyOnlyCostScalePinnedAmount = (rawValue: unknown) => {
|
||||||
|
const amount = parseNumberOrNull(rawValue, { precision: 2 })
|
||||||
|
const current = detailRows.value[0]
|
||||||
|
if (!current) {
|
||||||
|
detailRows.value = [buildOnlyCostScaleRow(amount)]
|
||||||
|
return
|
||||||
|
}
|
||||||
|
detailRows.value = [{ ...current, amount }]
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCellValueChanged = (event?: any) => {
|
||||||
|
if (isOnlyCostScaleService.value && event?.node?.rowPinned && event.colDef?.field === 'amount') {
|
||||||
|
applyOnlyCostScalePinnedAmount(event.newValue)
|
||||||
|
}
|
||||||
syncComputedValuesToDetailRows()
|
syncComputedValuesToDetailRows()
|
||||||
if (gridPersistTimer) clearTimeout(gridPersistTimer)
|
if (gridPersistTimer) clearTimeout(gridPersistTimer)
|
||||||
gridPersistTimer = setTimeout(() => {
|
gridPersistTimer = setTimeout(() => {
|
||||||
@ -899,7 +991,7 @@ const processCellFromClipboard = (params: any) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="ag-theme-quartz h-full min-h-0 w-full flex-1">
|
<div class="ag-theme-quartz h-full min-h-0 w-full flex-1">
|
||||||
<AgGridVue :style="{ height: '100%' }" :rowData="detailRows" :pinnedTopRowData="pinnedTopRowData"
|
<AgGridVue :style="{ height: '100%' }" :rowData="visibleDetailRows" :pinnedTopRowData="pinnedTopRowData"
|
||||||
:columnDefs="columnDefs" :autoGroupColumnDef="autoGroupColumnDef" :gridOptions="gridOptions" :theme="myTheme"
|
:columnDefs="columnDefs" :autoGroupColumnDef="autoGroupColumnDef" :gridOptions="gridOptions" :theme="myTheme"
|
||||||
@cell-value-changed="handleCellValueChanged" :suppressColumnVirtualisation="true"
|
@cell-value-changed="handleCellValueChanged" :suppressColumnVirtualisation="true"
|
||||||
:suppressRowVirtualisation="true" :cellSelection="{ handle: { mode: 'range' } }" :enableClipboard="true"
|
:suppressRowVirtualisation="true" :cellSelection="{ handle: { mode: 'range' } }" :enableClipboard="true"
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, defineComponent, h, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
import { computed, defineComponent, h, nextTick, onActivated, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||||
import type { ComponentPublicInstance, PropType } from 'vue'
|
import type { ComponentPublicInstance, PropType } from 'vue'
|
||||||
import { AgGridVue } from 'ag-grid-vue3'
|
import { AgGridVue } from 'ag-grid-vue3'
|
||||||
import type { ColDef, GridOptions, ICellRendererParams } from 'ag-grid-community'
|
import type { ColDef, GridOptions, ICellRendererParams } from 'ag-grid-community'
|
||||||
@ -9,7 +9,11 @@ import { AG_GRID_LOCALE_CN } from '@ag-grid-community/locale'
|
|||||||
import { addNumbers } from '@/lib/decimal'
|
import { addNumbers } from '@/lib/decimal'
|
||||||
import { parseNumberOrNull } from '@/lib/number'
|
import { parseNumberOrNull } from '@/lib/number'
|
||||||
import { formatThousands, formatThousandsFlexible } from '@/lib/numberFormat'
|
import { formatThousands, formatThousandsFlexible } from '@/lib/numberFormat'
|
||||||
import { getPricingMethodTotalsForService, getPricingMethodTotalsForServices } from '@/lib/pricingMethodTotals'
|
import {
|
||||||
|
getPricingMethodTotalsForService,
|
||||||
|
getPricingMethodTotalsForServices,
|
||||||
|
type PricingMethodTotals
|
||||||
|
} from '@/lib/pricingMethodTotals'
|
||||||
import { ZXFW_RELOAD_SERVICE_KEY } from '@/lib/zxFwPricingSync'
|
import { ZXFW_RELOAD_SERVICE_KEY } from '@/lib/zxFwPricingSync'
|
||||||
import { Pencil, Eraser, Trash2 } from 'lucide-vue-next'
|
import { Pencil, Eraser, Trash2 } from 'lucide-vue-next'
|
||||||
import {
|
import {
|
||||||
@ -21,18 +25,11 @@ import {
|
|||||||
AlertDialogPortal,
|
AlertDialogPortal,
|
||||||
AlertDialogRoot,
|
AlertDialogRoot,
|
||||||
AlertDialogTitle,
|
AlertDialogTitle,
|
||||||
DialogClose,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogOverlay,
|
|
||||||
DialogPortal,
|
|
||||||
DialogRoot,
|
|
||||||
DialogTitle,
|
|
||||||
DialogTrigger
|
|
||||||
} from 'reka-ui'
|
} from 'reka-ui'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { TooltipProvider } from '@/components/ui/tooltip'
|
import { TooltipProvider } from '@/components/ui/tooltip'
|
||||||
import { getServiceDictEntries } from '@/sql'
|
import { getServiceDictEntries, isIndustryEnabledByType,getIndustryTypeValue } from '@/sql'
|
||||||
import { useTabStore } from '@/pinia/tab'
|
import { useTabStore } from '@/pinia/tab'
|
||||||
import { usePricingPaneReloadStore } from '@/pinia/pricingPaneReload'
|
import { usePricingPaneReloadStore } from '@/pinia/pricingPaneReload'
|
||||||
import ServiceCheckboxSelector from '@/components/views/ServiceCheckboxSelector.vue'
|
import ServiceCheckboxSelector from '@/components/views/ServiceCheckboxSelector.vue'
|
||||||
@ -41,6 +38,7 @@ interface ServiceItem {
|
|||||||
id: string
|
id: string
|
||||||
code: string
|
code: string
|
||||||
name: string
|
name: string
|
||||||
|
type: ServiceMethodType
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DetailRow {
|
interface DetailRow {
|
||||||
@ -61,6 +59,17 @@ interface ZxFwState {
|
|||||||
detailRows: DetailRow[]
|
detailRows: DetailRow[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface XmBaseInfoState {
|
||||||
|
projectIndustry?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ServiceMethodType {
|
||||||
|
scale?: boolean | null
|
||||||
|
onlyCostScale?: boolean | null
|
||||||
|
amount?: boolean | null
|
||||||
|
workDay?: boolean | null
|
||||||
|
}
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
contractId: string
|
contractId: string
|
||||||
contractName?: string
|
contractName?: string
|
||||||
@ -68,25 +77,66 @@ const props = defineProps<{
|
|||||||
const tabStore = useTabStore()
|
const tabStore = useTabStore()
|
||||||
const pricingPaneReloadStore = usePricingPaneReloadStore()
|
const pricingPaneReloadStore = usePricingPaneReloadStore()
|
||||||
const DB_KEY = computed(() => `zxFW-${props.contractId}`)
|
const DB_KEY = computed(() => `zxFW-${props.contractId}`)
|
||||||
|
const PROJECT_INFO_KEY = 'xm-base-info-v1'
|
||||||
const PRICING_CLEAR_SKIP_PREFIX = 'pricing-clear-skip:'
|
const PRICING_CLEAR_SKIP_PREFIX = 'pricing-clear-skip:'
|
||||||
const PRICING_FORCE_DEFAULT_PREFIX = 'pricing-force-default:'
|
const PRICING_FORCE_DEFAULT_PREFIX = 'pricing-force-default:'
|
||||||
const PRICING_CLEAR_SKIP_TTL_MS = 5000
|
const PRICING_CLEAR_SKIP_TTL_MS = 5000
|
||||||
|
const PRICING_TOTALS_OPTIONS = { excludeInvestmentCostAndAreaRows: true } as const
|
||||||
|
const projectIndustry = ref('')
|
||||||
|
|
||||||
type ServiceListItem = { code?: string; ref?: string; name: string; defCoe: number | null }
|
type ServiceListItem = {
|
||||||
const serviceDict: ServiceItem[] = getServiceDictEntries()
|
code?: string
|
||||||
|
ref?: string
|
||||||
|
name: string
|
||||||
|
defCoe: number | null
|
||||||
|
isRoad?: boolean
|
||||||
|
isRailway?: boolean
|
||||||
|
isWaterway?: boolean
|
||||||
|
scale?: boolean | null
|
||||||
|
onlyCostScale?: boolean | null
|
||||||
|
amount?: boolean | null
|
||||||
|
workDay?: boolean | null
|
||||||
|
}
|
||||||
|
|
||||||
|
const toNullableBoolean = (value: unknown): boolean | null =>
|
||||||
|
typeof value === 'boolean' ? value : null
|
||||||
|
|
||||||
|
const resolveMethodEnabled = (value: boolean | null | undefined, fallback = true) =>
|
||||||
|
typeof value === 'boolean' ? value : fallback
|
||||||
|
|
||||||
|
const defaultServiceMethodType = {
|
||||||
|
scale: true,
|
||||||
|
onlyCostScale: false,
|
||||||
|
amount: true,
|
||||||
|
workDay: true
|
||||||
|
}
|
||||||
|
|
||||||
|
const serviceDict = computed<ServiceItem[]>(() => {
|
||||||
|
const industry = projectIndustry.value
|
||||||
|
if (!industry) return []
|
||||||
|
const filteredEntries = getServiceDictEntries()
|
||||||
.map(({ id, item }) => ({ id, item: item as ServiceListItem }))
|
.map(({ id, item }) => ({ id, item: item as ServiceListItem }))
|
||||||
.filter(({ item }) => {
|
.filter(({ item }) => {
|
||||||
const itemCode = item?.code || item?.ref
|
const itemCode = item?.code || item?.ref
|
||||||
return Boolean(itemCode && item?.name) && item.defCoe !== null
|
return Boolean(itemCode && item?.name) && item.defCoe !== null && isIndustryEnabledByType(item, getIndustryTypeValue(industry))
|
||||||
|
|
||||||
})
|
})
|
||||||
.map(({ id, item }) => ({
|
return filteredEntries.map(({ id, item }) => ({
|
||||||
id,
|
id,
|
||||||
code: item.code || item.ref || '',
|
code: item.code || item.ref || '',
|
||||||
name: item.name
|
name: item.name,
|
||||||
|
type: {
|
||||||
|
scale: toNullableBoolean(item.scale),
|
||||||
|
onlyCostScale: toNullableBoolean(item.onlyCostScale),
|
||||||
|
amount: toNullableBoolean(item.amount),
|
||||||
|
workDay: toNullableBoolean(item.workDay)
|
||||||
|
}
|
||||||
}))
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
const serviceById = new Map(serviceDict.map(item => [item.id, item]))
|
const serviceById = computed(() => new Map(serviceDict.value.map(item => [item.id, item])))
|
||||||
const serviceIdByCode = new Map(serviceDict.map(item => [item.code, item.id]))
|
const serviceIdByCode = computed(() => new Map(serviceDict.value.map(item => [item.code, item.id])))
|
||||||
|
const serviceIdSignature = computed(() => serviceDict.value.map(item => item.id).join('|'))
|
||||||
const fixedBudgetRow: Pick<DetailRow, 'id' | 'code' | 'name'> = { id: 'fixed-budget-c', code: 'C', name: '合同预算' }
|
const fixedBudgetRow: Pick<DetailRow, 'id' | 'code' | 'name'> = { id: 'fixed-budget-c', code: 'C', name: '合同预算' }
|
||||||
const isFixedRow = (row?: DetailRow | null) => row?.id === fixedBudgetRow.id
|
const isFixedRow = (row?: DetailRow | null) => row?.id === fixedBudgetRow.id
|
||||||
|
|
||||||
@ -167,7 +217,7 @@ const startDragAutoScroll = () => {
|
|||||||
const selectedServiceText = computed(() => {
|
const selectedServiceText = computed(() => {
|
||||||
if (selectedIds.value.length === 0) return ''
|
if (selectedIds.value.length === 0) return ''
|
||||||
const names = selectedIds.value
|
const names = selectedIds.value
|
||||||
.map(id => serviceById.get(id)?.name || '')
|
.map(id => serviceById.value.get(id)?.name || '')
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
if (names.length <= 2) return names.join('、')
|
if (names.length <= 2) return names.join('、')
|
||||||
return `${names.slice(0, 2).join('、')} 等 ${names.length} 项`
|
return `${names.slice(0, 2).join('、')} 等 ${names.length} 项`
|
||||||
@ -177,7 +227,7 @@ const pendingClearServiceName = computed(() => {
|
|||||||
if (!pendingClearServiceId.value) return ''
|
if (!pendingClearServiceId.value) return ''
|
||||||
const row = detailRows.value.find(item => item.id === pendingClearServiceId.value)
|
const row = detailRows.value.find(item => item.id === pendingClearServiceId.value)
|
||||||
if (row) return `${row.code}${row.name}`
|
if (row) return `${row.code}${row.name}`
|
||||||
const dict = serviceById.get(pendingClearServiceId.value)
|
const dict = serviceById.value.get(pendingClearServiceId.value)
|
||||||
if (dict) return `${dict.code}${dict.name}`
|
if (dict) return `${dict.code}${dict.name}`
|
||||||
return pendingClearServiceId.value
|
return pendingClearServiceId.value
|
||||||
})
|
})
|
||||||
@ -186,7 +236,7 @@ const pendingDeleteServiceName = computed(() => {
|
|||||||
if (!pendingDeleteServiceId.value) return ''
|
if (!pendingDeleteServiceId.value) return ''
|
||||||
const row = detailRows.value.find(item => item.id === pendingDeleteServiceId.value)
|
const row = detailRows.value.find(item => item.id === pendingDeleteServiceId.value)
|
||||||
if (row) return `${row.code}${row.name}`
|
if (row) return `${row.code}${row.name}`
|
||||||
const dict = serviceById.get(pendingDeleteServiceId.value)
|
const dict = serviceById.value.get(pendingDeleteServiceId.value)
|
||||||
if (dict) return `${dict.code}${dict.name}`
|
if (dict) return `${dict.code}${dict.name}`
|
||||||
return pendingDeleteServiceId.value
|
return pendingDeleteServiceId.value
|
||||||
})
|
})
|
||||||
@ -241,8 +291,8 @@ const confirmDeleteRow = async () => {
|
|||||||
|
|
||||||
const filteredServiceDict = computed(() => {
|
const filteredServiceDict = computed(() => {
|
||||||
const keyword = pickerSearch.value.trim()
|
const keyword = pickerSearch.value.trim()
|
||||||
if (!keyword) return serviceDict
|
if (!keyword) return serviceDict.value
|
||||||
return serviceDict.filter(item => item.code.includes(keyword) || item.name.includes(keyword))
|
return serviceDict.value.filter(item => item.code.includes(keyword) || item.name.includes(keyword))
|
||||||
})
|
})
|
||||||
|
|
||||||
const dragRectStyle = computed(() => {
|
const dragRectStyle = computed(() => {
|
||||||
@ -265,6 +315,40 @@ const numericParser = (newValue: any): number | null => {
|
|||||||
|
|
||||||
const valueOrZero = (v: number | null | undefined) => (typeof v === 'number' ? v : 0)
|
const valueOrZero = (v: number | null | undefined) => (typeof v === 'number' ? v : 0)
|
||||||
|
|
||||||
|
const getServiceMethodTypeById = (serviceId: string) => {
|
||||||
|
const type = serviceById.value.get(serviceId)?.type
|
||||||
|
const scale = resolveMethodEnabled(type?.scale, defaultServiceMethodType.scale)
|
||||||
|
const onlyCostScale = resolveMethodEnabled(type?.onlyCostScale, defaultServiceMethodType.onlyCostScale)
|
||||||
|
const amount = resolveMethodEnabled(type?.amount, defaultServiceMethodType.amount)
|
||||||
|
const workDay = resolveMethodEnabled(type?.workDay, defaultServiceMethodType.workDay)
|
||||||
|
return { scale, onlyCostScale, amount, workDay }
|
||||||
|
}
|
||||||
|
|
||||||
|
const sanitizePricingTotalsByService = (serviceId: string, totals: PricingMethodTotals): PricingMethodTotals => {
|
||||||
|
const methodType = getServiceMethodTypeById(serviceId)
|
||||||
|
const isScaleEnabled = methodType.scale
|
||||||
|
const isLandScaleEnabled = isScaleEnabled && !methodType.onlyCostScale
|
||||||
|
return {
|
||||||
|
investScale: isScaleEnabled ? totals.investScale : null,
|
||||||
|
landScale: isLandScaleEnabled ? totals.landScale : null,
|
||||||
|
workload: methodType.amount ? totals.workload : null,
|
||||||
|
hourly: methodType.workDay ? totals.hourly : null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sanitizePricingFieldsByService = (
|
||||||
|
serviceId: string,
|
||||||
|
values: Pick<DetailRow, 'investScale' | 'landScale' | 'workload' | 'hourly'>
|
||||||
|
) => {
|
||||||
|
const sanitized = sanitizePricingTotalsByService(serviceId, values)
|
||||||
|
return {
|
||||||
|
investScale: sanitized.investScale,
|
||||||
|
landScale: sanitized.landScale,
|
||||||
|
workload: sanitized.workload,
|
||||||
|
hourly: sanitized.hourly
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const getMethodTotalFromRows = (
|
const getMethodTotalFromRows = (
|
||||||
rows: DetailRow[],
|
rows: DetailRow[],
|
||||||
field: 'investScale' | 'landScale' | 'workload' | 'hourly'
|
field: 'investScale' | 'landScale' | 'workload' | 'hourly'
|
||||||
@ -313,17 +397,19 @@ const clearRowValues = async (row: DetailRow) => {
|
|||||||
await clearPricingPaneValues(row.id)
|
await clearPricingPaneValues(row.id)
|
||||||
const totals = await getPricingMethodTotalsForService({
|
const totals = await getPricingMethodTotalsForService({
|
||||||
contractId: props.contractId,
|
contractId: props.contractId,
|
||||||
serviceId: row.id
|
serviceId: row.id,
|
||||||
|
options: PRICING_TOTALS_OPTIONS
|
||||||
})
|
})
|
||||||
|
const sanitizedTotals = sanitizePricingTotalsByService(row.id, totals)
|
||||||
const clearedRows = detailRows.value.map(item =>
|
const clearedRows = detailRows.value.map(item =>
|
||||||
item.id !== row.id
|
item.id !== row.id
|
||||||
? item
|
? item
|
||||||
: {
|
: {
|
||||||
...item,
|
...item,
|
||||||
investScale: totals.investScale,
|
investScale: sanitizedTotals.investScale,
|
||||||
landScale: totals.landScale,
|
landScale: sanitizedTotals.landScale,
|
||||||
workload: totals.workload,
|
workload: sanitizedTotals.workload,
|
||||||
hourly: totals.hourly
|
hourly: sanitizedTotals.hourly
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
const nextInvestScale = getMethodTotalFromRows(clearedRows, 'investScale')
|
const nextInvestScale = getMethodTotalFromRows(clearedRows, 'investScale')
|
||||||
@ -346,6 +432,7 @@ const clearRowValues = async (row: DetailRow) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const openEditTab = (row: DetailRow) => {
|
const openEditTab = (row: DetailRow) => {
|
||||||
|
const serviceType = serviceById.value.get(row.id)?.type
|
||||||
tabStore.openTab({
|
tabStore.openTab({
|
||||||
id: `zxfw-edit-${props.contractId}-${row.id}`,
|
id: `zxfw-edit-${props.contractId}-${row.id}`,
|
||||||
title: `服务编辑-${row.code}${row.name}`,
|
title: `服务编辑-${row.code}${row.name}`,
|
||||||
@ -354,7 +441,8 @@ const openEditTab = (row: DetailRow) => {
|
|||||||
contractId: props.contractId,
|
contractId: props.contractId,
|
||||||
contractName: props.contractName || '',
|
contractName: props.contractName || '',
|
||||||
serviceId: row.id,
|
serviceId: row.id,
|
||||||
fwName: row.code + row.name
|
fwName: row.code + row.name,
|
||||||
|
type: serviceType ? { ...serviceType } : undefined
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -562,13 +650,15 @@ const fillPricingTotalsForServiceIds = async (serviceIds: string[]) => {
|
|||||||
|
|
||||||
const totalsByServiceId = await getPricingMethodTotalsForServices({
|
const totalsByServiceId = await getPricingMethodTotalsForServices({
|
||||||
contractId: props.contractId,
|
contractId: props.contractId,
|
||||||
serviceIds: targetIds
|
serviceIds: targetIds,
|
||||||
|
options: PRICING_TOTALS_OPTIONS
|
||||||
})
|
})
|
||||||
|
|
||||||
const targetSet = new Set(targetIds.map(id => String(id)))
|
const targetSet = new Set(targetIds.map(id => String(id)))
|
||||||
const nextRows = detailRows.value.map(row => {
|
const nextRows = detailRows.value.map(row => {
|
||||||
if (isFixedRow(row) || !targetSet.has(String(row.id))) return row
|
if (isFixedRow(row) || !targetSet.has(String(row.id))) return row
|
||||||
const totals = totalsByServiceId.get(String(row.id))
|
const totalsRaw = totalsByServiceId.get(String(row.id))
|
||||||
|
const totals = totalsRaw ? sanitizePricingTotalsByService(String(row.id), totalsRaw) : null
|
||||||
if (!totals) return row
|
if (!totals) return row
|
||||||
return {
|
return {
|
||||||
...row,
|
...row,
|
||||||
@ -585,29 +675,35 @@ const fillPricingTotalsForServiceIds = async (serviceIds: string[]) => {
|
|||||||
const applySelection = (codes: string[]) => {
|
const applySelection = (codes: string[]) => {
|
||||||
const prevSelectedSet = new Set(selectedIds.value)
|
const prevSelectedSet = new Set(selectedIds.value)
|
||||||
const uniqueIds = Array.from(new Set(codes)).filter(
|
const uniqueIds = Array.from(new Set(codes)).filter(
|
||||||
id => serviceById.has(id) && id !== fixedBudgetRow.id
|
id => serviceById.value.has(id) && id !== fixedBudgetRow.id
|
||||||
)
|
)
|
||||||
const existingMap = new Map(detailRows.value.map(row => [row.id, row]))
|
const existingMap = new Map(detailRows.value.map(row => [row.id, row]))
|
||||||
|
|
||||||
const baseRows: DetailRow[] = uniqueIds
|
const baseRows: DetailRow[] = uniqueIds
|
||||||
.map(id => {
|
.map(id => {
|
||||||
const dictItem = serviceById.get(id)
|
const dictItem = serviceById.value.get(id)
|
||||||
if (!dictItem) return null
|
if (!dictItem) return null
|
||||||
|
|
||||||
const old = existingMap.get(id)
|
const old = existingMap.get(id)
|
||||||
return {
|
const nextValues = sanitizePricingFieldsByService(id, {
|
||||||
id: old?.id || id,
|
|
||||||
code: dictItem.code,
|
|
||||||
name: dictItem.name,
|
|
||||||
investScale: old?.investScale ?? null,
|
investScale: old?.investScale ?? null,
|
||||||
landScale: old?.landScale ?? null,
|
landScale: old?.landScale ?? null,
|
||||||
workload: old?.workload ?? null,
|
workload: old?.workload ?? null,
|
||||||
hourly: old?.hourly ?? null
|
hourly: old?.hourly ?? null
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
id: old?.id || id,
|
||||||
|
code: dictItem.code,
|
||||||
|
name: dictItem.name,
|
||||||
|
investScale: nextValues.investScale,
|
||||||
|
landScale: nextValues.landScale,
|
||||||
|
workload: nextValues.workload,
|
||||||
|
hourly: nextValues.hourly
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.filter((row): row is DetailRow => Boolean(row))
|
.filter((row): row is DetailRow => Boolean(row))
|
||||||
|
|
||||||
const orderMap = new Map(serviceDict.map((item, index) => [item.id, index]))
|
const orderMap = new Map(serviceDict.value.map((item, index) => [item.id, index]))
|
||||||
baseRows.sort((a, b) => (orderMap.get(a.id) || 0) - (orderMap.get(b.id) || 0))
|
baseRows.sort((a, b) => (orderMap.get(a.id) || 0) - (orderMap.get(b.id) || 0))
|
||||||
|
|
||||||
const fixedOld = existingMap.get(fixedBudgetRow.id)
|
const fixedOld = existingMap.get(fixedBudgetRow.id)
|
||||||
@ -730,6 +826,7 @@ const applyDragSelectionByRect = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pickerTempIds.value = serviceDict
|
pickerTempIds.value = serviceDict
|
||||||
|
.value
|
||||||
.map(item => item.id)
|
.map(item => item.id)
|
||||||
.filter(id => nextSelectedSet.has(id))
|
.filter(id => nextSelectedSet.has(id))
|
||||||
}
|
}
|
||||||
@ -797,19 +894,25 @@ const loadFromIndexedDB = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const idsFromStorage = data.selectedIds
|
const idsFromStorage = data.selectedIds
|
||||||
|| (data.selectedCodes || []).map(code => serviceIdByCode.get(code)).filter((id): id is string => Boolean(id))
|
|| (data.selectedCodes || []).map(code => serviceIdByCode.value.get(code)).filter((id): id is string => Boolean(id))
|
||||||
applySelection(idsFromStorage)
|
applySelection(idsFromStorage)
|
||||||
|
|
||||||
const savedRowMap = new Map((data.detailRows || []).map(row => [row.id, row]))
|
const savedRowMap = new Map((data.detailRows || []).map(row => [row.id, row]))
|
||||||
detailRows.value = detailRows.value.map(row => {
|
detailRows.value = detailRows.value.map(row => {
|
||||||
const old = savedRowMap.get(row.id)
|
const old = savedRowMap.get(row.id)
|
||||||
if (!old) return row
|
if (!old) return row
|
||||||
return {
|
const nextValues = sanitizePricingFieldsByService(row.id, {
|
||||||
...row,
|
|
||||||
investScale: typeof old.investScale === 'number' ? old.investScale : null,
|
investScale: typeof old.investScale === 'number' ? old.investScale : null,
|
||||||
landScale: typeof old.landScale === 'number' ? old.landScale : null,
|
landScale: typeof old.landScale === 'number' ? old.landScale : null,
|
||||||
workload: typeof old.workload === 'number' ? old.workload : null,
|
workload: typeof old.workload === 'number' ? old.workload : null,
|
||||||
hourly: typeof old.hourly === 'number' ? old.hourly : null
|
hourly: typeof old.hourly === 'number' ? old.hourly : null
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
...row,
|
||||||
|
investScale: nextValues.investScale,
|
||||||
|
landScale: nextValues.landScale,
|
||||||
|
workload: nextValues.workload,
|
||||||
|
hourly: nextValues.hourly
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
detailRows.value = applyFixedRowTotals(detailRows.value)
|
detailRows.value = applyFixedRowTotals(detailRows.value)
|
||||||
@ -820,6 +923,17 @@ const loadFromIndexedDB = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const loadProjectIndustry = async () => {
|
||||||
|
try {
|
||||||
|
const data = await localforage.getItem<XmBaseInfoState>(PROJECT_INFO_KEY)
|
||||||
|
projectIndustry.value =
|
||||||
|
typeof data?.projectIndustry === 'string' ? data.projectIndustry.trim() : ''
|
||||||
|
} catch (error) {
|
||||||
|
console.error('loadProjectIndustry failed:', error)
|
||||||
|
projectIndustry.value = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => pricingPaneReloadStore.getReloadVersion(props.contractId, ZXFW_RELOAD_SERVICE_KEY),
|
() => pricingPaneReloadStore.getReloadVersion(props.contractId, ZXFW_RELOAD_SERVICE_KEY),
|
||||||
(nextVersion, prevVersion) => {
|
(nextVersion, prevVersion) => {
|
||||||
@ -828,6 +942,15 @@ watch(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
watch(serviceIdSignature, () => {
|
||||||
|
const availableIds = new Set(serviceDict.value.map(item => item.id))
|
||||||
|
const nextSelectedIds = selectedIds.value.filter(id => availableIds.has(id))
|
||||||
|
if (nextSelectedIds.length !== selectedIds.value.length) {
|
||||||
|
applySelection(nextSelectedIds)
|
||||||
|
void saveToIndexedDB()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
let gridPersistTimer: ReturnType<typeof setTimeout> | null = null
|
let gridPersistTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
const handleCellValueChanged = () => {
|
const handleCellValueChanged = () => {
|
||||||
if (gridPersistTimer) clearTimeout(gridPersistTimer)
|
if (gridPersistTimer) clearTimeout(gridPersistTimer)
|
||||||
@ -837,6 +960,12 @@ const handleCellValueChanged = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
await loadProjectIndustry()
|
||||||
|
await loadFromIndexedDB()
|
||||||
|
})
|
||||||
|
|
||||||
|
onActivated(async () => {
|
||||||
|
await loadProjectIndustry()
|
||||||
await loadFromIndexedDB()
|
await loadFromIndexedDB()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -53,10 +53,13 @@ interface HourlyRow {
|
|||||||
interface MajorLite {
|
interface MajorLite {
|
||||||
code: string
|
code: string
|
||||||
defCoe: number | null
|
defCoe: number | null
|
||||||
|
hasCost?: boolean
|
||||||
|
hasArea?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ServiceLite {
|
interface ServiceLite {
|
||||||
defCoe: number | null
|
defCoe: number | null
|
||||||
|
onlyCostScale?: boolean | null
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TaskLite {
|
interface TaskLite {
|
||||||
@ -77,6 +80,12 @@ export interface PricingMethodTotals {
|
|||||||
hourly: number | null
|
hourly: number | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface PricingMethodTotalsOptions {
|
||||||
|
excludeInvestmentCostAndAreaRows?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const ONLY_COST_SCALE_ROW_ID = '__only-cost-scale-total__'
|
||||||
|
|
||||||
const hasOwn = (obj: unknown, key: string) =>
|
const hasOwn = (obj: unknown, key: string) =>
|
||||||
Object.prototype.hasOwnProperty.call(obj || {}, key)
|
Object.prototype.hasOwnProperty.call(obj || {}, key)
|
||||||
|
|
||||||
@ -93,6 +102,11 @@ const getDefaultConsultCategoryFactor = (serviceId: string | number) => {
|
|||||||
return toFiniteNumberOrNull(service?.defCoe)
|
return toFiniteNumberOrNull(service?.defCoe)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isOnlyCostScaleService = (serviceId: string | number) => {
|
||||||
|
const service = (getServiceDictById() as Record<string, ServiceLite | undefined>)[String(serviceId)]
|
||||||
|
return service?.onlyCostScale === true
|
||||||
|
}
|
||||||
|
|
||||||
const majorById = new Map(getMajorDictEntries().map(({ id, item }) => [id, item as MajorLite]))
|
const majorById = new Map(getMajorDictEntries().map(({ id, item }) => [id, item as MajorLite]))
|
||||||
const majorIdAliasMap = getMajorIdAliasMap()
|
const majorIdAliasMap = getMajorIdAliasMap()
|
||||||
|
|
||||||
@ -102,6 +116,15 @@ 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 resolveFactorValue = (
|
const resolveFactorValue = (
|
||||||
row: { budgetValue?: number | null; standardFactor?: number | null } | undefined,
|
row: { budgetValue?: number | null; standardFactor?: number | null } | undefined,
|
||||||
fallback: number | null
|
fallback: number | null
|
||||||
@ -227,6 +250,31 @@ const getInvestmentBudgetFee = (row: ScaleRow) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getOnlyCostScaleBudgetFee = (
|
||||||
|
serviceId: string,
|
||||||
|
rowsFromDb: Array<Record<string, unknown>> | undefined,
|
||||||
|
consultCategoryFactorMap?: Map<string, number | 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 majorFactor = toFiniteNumberOrNull(onlyRow?.majorFactor) ?? 1
|
||||||
|
const workStageFactor = toFiniteNumberOrNull(onlyRow?.workStageFactor) ?? 1
|
||||||
|
const workRatio = toFiniteNumberOrNull(onlyRow?.workRatio) ?? 100
|
||||||
|
return getScaleBudgetFee({
|
||||||
|
benchmarkBudget: getBenchmarkBudgetByAmount(totalAmount),
|
||||||
|
majorFactor,
|
||||||
|
consultCategoryFactor,
|
||||||
|
workStageFactor,
|
||||||
|
workRatio
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const getLandBudgetFee = (row: ScaleRow) => {
|
const getLandBudgetFee = (row: ScaleRow) => {
|
||||||
return getScaleBudgetFee({
|
return getScaleBudgetFee({
|
||||||
benchmarkBudget: getBenchmarkBudgetByLandArea(row.landArea),
|
benchmarkBudget: getBenchmarkBudgetByLandArea(row.landArea),
|
||||||
@ -373,6 +421,7 @@ 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}`
|
||||||
@ -395,8 +444,18 @@ export const getPricingMethodTotalsForService = async (params: {
|
|||||||
|
|
||||||
const consultCategoryFactorMap = buildConsultCategoryFactorMap(consultFactorData)
|
const consultCategoryFactorMap = buildConsultCategoryFactorMap(consultFactorData)
|
||||||
const majorFactorMap = buildMajorFactorMap(majorFactorData)
|
const majorFactorMap = buildMajorFactorMap(majorFactorData)
|
||||||
|
const onlyCostScale = isOnlyCostScaleService(serviceId)
|
||||||
|
|
||||||
// 优先使用对应计费页的数据;不存在时回退合同段规模信息,再回退默认字典行。
|
// 优先使用对应计费页的数据;不存在时回退合同段规模信息,再回退默认字典行。
|
||||||
|
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
|
||||||
|
)
|
||||||
|
: (() => {
|
||||||
const investRows = resolveScaleRows(
|
const investRows = resolveScaleRows(
|
||||||
serviceId,
|
serviceId,
|
||||||
investData,
|
investData,
|
||||||
@ -404,7 +463,11 @@ export const getPricingMethodTotalsForService = async (params: {
|
|||||||
consultCategoryFactorMap,
|
consultCategoryFactorMap,
|
||||||
majorFactorMap
|
majorFactorMap
|
||||||
)
|
)
|
||||||
const investScale = sumByNumber(investRows, row => getInvestmentBudgetFee(row))
|
return sumByNumber(investRows, row => {
|
||||||
|
if (excludeInvestmentCostAndAreaRows && isDualScaleMajorById(row.id)) return null
|
||||||
|
return getInvestmentBudgetFee(row)
|
||||||
|
})
|
||||||
|
})()
|
||||||
|
|
||||||
const landRows = resolveScaleRows(
|
const landRows = resolveScaleRows(
|
||||||
serviceId,
|
serviceId,
|
||||||
@ -443,13 +506,15 @@ export const getPricingMethodTotalsForService = async (params: {
|
|||||||
export const getPricingMethodTotalsForServices = async (params: {
|
export const getPricingMethodTotalsForServices = async (params: {
|
||||||
contractId: string
|
contractId: string
|
||||||
serviceIds: Array<string | number>
|
serviceIds: Array<string | number>
|
||||||
|
options?: PricingMethodTotalsOptions
|
||||||
}) => {
|
}) => {
|
||||||
const result = new Map<string, PricingMethodTotals>()
|
const result = new Map<string, PricingMethodTotals>()
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
params.serviceIds.map(async serviceId => {
|
params.serviceIds.map(async serviceId => {
|
||||||
const totals = await getPricingMethodTotalsForService({
|
const totals = await getPricingMethodTotalsForService({
|
||||||
contractId: params.contractId,
|
contractId: params.contractId,
|
||||||
serviceId
|
serviceId,
|
||||||
|
options: params.options
|
||||||
})
|
})
|
||||||
result.set(String(serviceId), totals)
|
result.set(String(serviceId), totals)
|
||||||
})
|
})
|
||||||
|
|||||||
24
src/sql.ts
24
src/sql.ts
@ -11,6 +11,12 @@ const toFiniteNumber = (value: unknown) => {
|
|||||||
const num = Number(value)
|
const num = Number(value)
|
||||||
return Number.isFinite(num) ? num : 0
|
return Number.isFinite(num) ? num : 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const industryTypeList = [
|
||||||
|
{id:'0', name: '公路工程', type: 'isRoad' },
|
||||||
|
{id:'1', name: '铁路工程', type: 'isRailway' },
|
||||||
|
{id:'2', name: '水运工程', type: 'isWaterway' }
|
||||||
|
] as const
|
||||||
export const majorList = {
|
export const majorList = {
|
||||||
0: { code: 'E1', name: '交通运输工程通用专业',hideInIndustrySelector: true, maxCoe: null, minCoe: null, defCoe: null, desc: '', isRoad: true, isRailway: true, isWaterway: true, order: 1, hasCost: false, hasArea: false },
|
0: { code: 'E1', name: '交通运输工程通用专业',hideInIndustrySelector: true, maxCoe: null, minCoe: null, defCoe: null, desc: '', isRoad: true, isRailway: true, isWaterway: true, order: 1, hasCost: false, hasArea: false },
|
||||||
1: { code: 'E1-1', name: '征地(用海)补偿', maxCoe: null, minCoe: null, defCoe: 1, desc: '适用于交通建设项目征地(用海)补偿的施工图预算、招标工程量清单及清单预算(或最高投标限价)、清理概算(仅限铁路工程)、合同(工程)结算和造价鉴定、计算工程量、工程变更费用咨询、工程成本测(核)算', isRoad: true, isRailway: true, isWaterway: true, order: 2, hasCost: true, hasArea: true },
|
1: { code: 'E1-1', name: '征地(用海)补偿', maxCoe: null, minCoe: null, defCoe: 1, desc: '适用于交通建设项目征地(用海)补偿的施工图预算、招标工程量清单及清单预算(或最高投标限价)、清理概算(仅限铁路工程)、合同(工程)结算和造价鉴定、计算工程量、工程变更费用咨询、工程成本测(核)算', isRoad: true, isRailway: true, isWaterway: true, order: 2, hasCost: true, hasArea: true },
|
||||||
@ -19,7 +25,7 @@ export const majorList = {
|
|||||||
4: { code: 'E1-4', name: '工程建设其他费', maxCoe: null, minCoe: null, defCoe: 1, desc: '适用于交通建设项目的工程建设其他费的施工图预算、招标工程量清单及清单预算(或最高投标限价)、清理概算(仅限铁路工程)和造价鉴定、计算工程量、工程变更费用咨询、工程成本测(核)算', isRoad: true, isRailway: true, isWaterway: true, order: 5, hasCost: true, hasArea: false },
|
4: { code: 'E1-4', name: '工程建设其他费', maxCoe: null, minCoe: null, defCoe: 1, desc: '适用于交通建设项目的工程建设其他费的施工图预算、招标工程量清单及清单预算(或最高投标限价)、清理概算(仅限铁路工程)和造价鉴定、计算工程量、工程变更费用咨询、工程成本测(核)算', isRoad: true, isRailway: true, isWaterway: true, order: 5, hasCost: true, hasArea: false },
|
||||||
5: { code: 'E1-5', name: '预备费', maxCoe: null, minCoe: null, defCoe: 1, desc: '', isRoad: true, isRailway: true, isWaterway: true, order: 6, hasCost: true, hasArea: false },
|
5: { code: 'E1-5', name: '预备费', maxCoe: null, minCoe: null, defCoe: 1, desc: '', isRoad: true, isRailway: true, isWaterway: true, order: 6, hasCost: true, hasArea: false },
|
||||||
6: { code: 'E1-6', name: '建设期贷款利息', maxCoe: null, minCoe: null, defCoe: 1, desc: '', isRoad: true, isRailway: true, isWaterway: true, order: 7, hasCost: true, hasArea: false },
|
6: { code: 'E1-6', name: '建设期贷款利息', maxCoe: null, minCoe: null, defCoe: 1, desc: '', isRoad: true, isRailway: true, isWaterway: true, order: 7, hasCost: true, hasArea: false },
|
||||||
7: { code: 'E2', name: '公路工程专业', maxCoe: null, minCoe: null, defCoe: 1, desc: '适用于公路工程的全过程造价咨询、分阶段造价咨询、投资估算、初步设计概算、竣工决算和调整估算、调整概算(含征地拆迁和工程建设其他费)', isRoad: true, isRailway: false, isWaterway: false, order: 8, hasCost: false, hasArea: false },
|
7: { code: 'E2', name: '公路工程专业', maxCoe: null, minCoe: null, defCoe: 1, desc: '适用于公路工程的全过程造价咨询、分阶段造价咨询、投资估算、初步设计概算、竣工决算和调整估算、调整概算(含征地拆迁和工程建设其他费)', isRoad: true, isRailway: false, isWaterway: false, order: 8, hasCost: false, hasArea: false ,industryId:'0' },
|
||||||
8: { code: 'E2-1', name: '临时工程', maxCoe: null, minCoe: null, defCoe: 1, desc: '适用于临时工程专业的施工图预算、招标工程量清单及清单预算(或最高投标限价)、合同(工程)结算和造价鉴定、计算工程量、工程变更费用咨询、工程成本测(核)算', isRoad: true, isRailway: false, isWaterway: false, order: 9, hasCost: true, hasArea: false },
|
8: { code: 'E2-1', name: '临时工程', maxCoe: null, minCoe: null, defCoe: 1, desc: '适用于临时工程专业的施工图预算、招标工程量清单及清单预算(或最高投标限价)、合同(工程)结算和造价鉴定、计算工程量、工程变更费用咨询、工程成本测(核)算', isRoad: true, isRailway: false, isWaterway: false, order: 9, hasCost: true, hasArea: false },
|
||||||
9: { code: 'E2-2', name: '路基工程', maxCoe: null, minCoe: null, defCoe: 1.2, desc: '适用于路基工程专业的施工图预算、招标工程量清单及清单预算(或最高投标限价)、合同(工程)结算和造价鉴定、计算工程量、工程变更费用咨询、工程成本测(核)算', isRoad: true, isRailway: false, isWaterway: false, order: 10, hasCost: true, hasArea: false },
|
9: { code: 'E2-2', name: '路基工程', maxCoe: null, minCoe: null, defCoe: 1.2, desc: '适用于路基工程专业的施工图预算、招标工程量清单及清单预算(或最高投标限价)、合同(工程)结算和造价鉴定、计算工程量、工程变更费用咨询、工程成本测(核)算', isRoad: true, isRailway: false, isWaterway: false, order: 10, hasCost: true, hasArea: false },
|
||||||
10: { code: 'E2-3', name: '路面工程', maxCoe: null, minCoe: null, defCoe: 0.8, desc: '适用于路面工程专业的施工图预算、招标工程量清单及清单预算(或最高投标限价)、合同(工程)结算和造价鉴定、计算工程量、工程变更费用咨询、工程成本测(核)算', isRoad: true, isRailway: false, isWaterway: false, order: 11, hasCost: true, hasArea: false },
|
10: { code: 'E2-3', name: '路面工程', maxCoe: null, minCoe: null, defCoe: 0.8, desc: '适用于路面工程专业的施工图预算、招标工程量清单及清单预算(或最高投标限价)、合同(工程)结算和造价鉴定、计算工程量、工程变更费用咨询、工程成本测(核)算', isRoad: true, isRailway: false, isWaterway: false, order: 11, hasCost: true, hasArea: false },
|
||||||
@ -30,7 +36,7 @@ export const majorList = {
|
|||||||
15: { code: 'E2-8', name: '交通安全设施工程', maxCoe: null, minCoe: null, defCoe: 1.2, desc: '适用于交通安全设施工程专业的施工图预算、招标工程量清单及清单预算(或最高投标限价)和造价鉴定、计算工程量、工程变更费用咨询、工程成本测(核)算', isRoad: true, isRailway: false, isWaterway: false, order: 16, hasCost: true, hasArea: false },
|
15: { code: 'E2-8', name: '交通安全设施工程', maxCoe: null, minCoe: null, defCoe: 1.2, desc: '适用于交通安全设施工程专业的施工图预算、招标工程量清单及清单预算(或最高投标限价)和造价鉴定、计算工程量、工程变更费用咨询、工程成本测(核)算', isRoad: true, isRailway: false, isWaterway: false, order: 16, hasCost: true, hasArea: false },
|
||||||
16: { code: 'E2-9', name: '绿化及环境保护工程', maxCoe: null, minCoe: null, defCoe: 1.2, desc: '适用于绿化工程专业的施工图预算、招标工程量清单及清单预算(或最高投标限价)和造价鉴定、计算工程量、工程变更费用咨询、工程成本测(核)算', isRoad: true, isRailway: false, isWaterway: false, order: 17, hasCost: true, hasArea: false },
|
16: { code: 'E2-9', name: '绿化及环境保护工程', maxCoe: null, minCoe: null, defCoe: 1.2, desc: '适用于绿化工程专业的施工图预算、招标工程量清单及清单预算(或最高投标限价)和造价鉴定、计算工程量、工程变更费用咨询、工程成本测(核)算', isRoad: true, isRailway: false, isWaterway: false, order: 17, hasCost: true, hasArea: false },
|
||||||
17: { code: 'E2-10', name: '房建工程', maxCoe: null, minCoe: null, defCoe: 2.5, desc: '适用于房建工程专业的施工图预算、招标工程量清单及清单预算(或最高投标限价)、合同(工程)结算和造价鉴定、计算工程量、工程变更费用咨询、工程成本测(核)算', isRoad: true, isRailway: false, isWaterway: false, order: 18, hasCost: true, hasArea: false },
|
17: { code: 'E2-10', name: '房建工程', maxCoe: null, minCoe: null, defCoe: 2.5, desc: '适用于房建工程专业的施工图预算、招标工程量清单及清单预算(或最高投标限价)、合同(工程)结算和造价鉴定、计算工程量、工程变更费用咨询、工程成本测(核)算', isRoad: true, isRailway: false, isWaterway: false, order: 18, hasCost: true, hasArea: false },
|
||||||
18: { code: 'E3', name: '铁路工程专业', maxCoe: null, minCoe: null, defCoe: 1, desc: '适用于铁路工程的投资估算、初步设计概算、清理概算、竣工决算和调整估算、调整概算(含征地拆迁和工程建设其他费)', isRoad: false, isRailway: true, isWaterway: false, order: 19, hasCost: false, hasArea: false },
|
18: { code: 'E3', name: '铁路工程专业', maxCoe: null, minCoe: null, defCoe: 1, desc: '适用于铁路工程的投资估算、初步设计概算、清理概算、竣工决算和调整估算、调整概算(含征地拆迁和工程建设其他费)', isRoad: false, isRailway: true, isWaterway: false, order: 19, hasCost: false, hasArea: false ,industryId:'1' },
|
||||||
19: { code: 'E3-1', name: '大型临时设施和过渡工程', maxCoe: null, minCoe: null, defCoe: 1, desc: '适用于大型临时设施和过渡工程专业的施工图预算、招标工程量清单及清单预算(或最高投标限价)、合同(工程)结算和造价鉴定、计算工程量、工程变更费用咨询、工程成本测(核)算', isRoad: false, isRailway: true, isWaterway: false, order: 20, hasCost: true, hasArea: false },
|
19: { code: 'E3-1', name: '大型临时设施和过渡工程', maxCoe: null, minCoe: null, defCoe: 1, desc: '适用于大型临时设施和过渡工程专业的施工图预算、招标工程量清单及清单预算(或最高投标限价)、合同(工程)结算和造价鉴定、计算工程量、工程变更费用咨询、工程成本测(核)算', isRoad: false, isRailway: true, isWaterway: false, order: 20, hasCost: true, hasArea: false },
|
||||||
20: { code: 'E3-2', name: '路基工程', maxCoe: null, minCoe: null, defCoe: 1.2, desc: '适用于路基工程专业的施工图预算、招标工程量清单及清单预算(或最高投标限价)、合同(工程)结算和造价鉴定、计算工程量、工程变更费用咨询、工程成本测(核)算', isRoad: false, isRailway: true, isWaterway: false, order: 21, hasCost: true, hasArea: false },
|
20: { code: 'E3-2', name: '路基工程', maxCoe: null, minCoe: null, defCoe: 1.2, desc: '适用于路基工程专业的施工图预算、招标工程量清单及清单预算(或最高投标限价)、合同(工程)结算和造价鉴定、计算工程量、工程变更费用咨询、工程成本测(核)算', isRoad: false, isRailway: true, isWaterway: false, order: 21, hasCost: true, hasArea: false },
|
||||||
21: { code: 'E3-3', name: '桥涵工程', maxCoe: null, minCoe: null, defCoe: 0.9, desc: '适用于桥涵工程专业的施工图预算、招标工程量清单及清单预算(或最高投标限价)、合同(工程)结算和造价鉴定、计算工程量、工程变更费用咨询、工程成本测(核)算', isRoad: false, isRailway: true, isWaterway: false, order: 22, hasCost: true, hasArea: false },
|
21: { code: 'E3-3', name: '桥涵工程', maxCoe: null, minCoe: null, defCoe: 0.9, desc: '适用于桥涵工程专业的施工图预算、招标工程量清单及清单预算(或最高投标限价)、合同(工程)结算和造价鉴定、计算工程量、工程变更费用咨询、工程成本测(核)算', isRoad: false, isRailway: true, isWaterway: false, order: 22, hasCost: true, hasArea: false },
|
||||||
@ -40,7 +46,7 @@ export const majorList = {
|
|||||||
25: { code: 'E3-7', name: '电力及电力牵引供电工程', maxCoe: null, minCoe: null, defCoe: 1.5, desc: '适用于电力及电力牵引供电工程专业的施工图预算、招标工程量清单及清单预算(或最高投标限价)、合同(工程)结算和造价鉴定、计算工程量、工程变更费用咨询、工程成本测(核)算', isRoad: false, isRailway: true, isWaterway: false, order: 26, hasCost: true, hasArea: false },
|
25: { code: 'E3-7', name: '电力及电力牵引供电工程', maxCoe: null, minCoe: null, defCoe: 1.5, desc: '适用于电力及电力牵引供电工程专业的施工图预算、招标工程量清单及清单预算(或最高投标限价)、合同(工程)结算和造价鉴定、计算工程量、工程变更费用咨询、工程成本测(核)算', isRoad: false, isRailway: true, isWaterway: false, order: 26, hasCost: true, hasArea: false },
|
||||||
26: { code: 'E3-8', name: '房建工程(房屋建筑及附属工程)', maxCoe: null, minCoe: null, defCoe: 2.5, desc: '适用于房屋建筑及附属工程专业的施工图预算、招标工程量清单及清单预算(或最高投标限价)、合同(工程)结算和造价鉴定、计算工程量、工程变更费用咨询、工程成本测(核)算', isRoad: false, isRailway: true, isWaterway: false, order: 27, hasCost: true, hasArea: false },
|
26: { code: 'E3-8', name: '房建工程(房屋建筑及附属工程)', maxCoe: null, minCoe: null, defCoe: 2.5, desc: '适用于房屋建筑及附属工程专业的施工图预算、招标工程量清单及清单预算(或最高投标限价)、合同(工程)结算和造价鉴定、计算工程量、工程变更费用咨询、工程成本测(核)算', isRoad: false, isRailway: true, isWaterway: false, order: 27, hasCost: true, hasArea: false },
|
||||||
27: { code: 'E3-9', name: '装饰装修工程', maxCoe: null, minCoe: null, defCoe: 2.7, desc: '适用于装饰装修工程专业的施工图预算、招标工程量清单及清单预算(或最高投标限价)、合同(工程)结算和造价鉴定、计算工程量、工程变更费用咨询、工程成本测(核)算', isRoad: false, isRailway: true, isWaterway: false, order: 28, hasCost: true, hasArea: false },
|
27: { code: 'E3-9', name: '装饰装修工程', maxCoe: null, minCoe: null, defCoe: 2.7, desc: '适用于装饰装修工程专业的施工图预算、招标工程量清单及清单预算(或最高投标限价)、合同(工程)结算和造价鉴定、计算工程量、工程变更费用咨询、工程成本测(核)算', isRoad: false, isRailway: true, isWaterway: false, order: 28, hasCost: true, hasArea: false },
|
||||||
28: { code: 'E4', name: '水运工程专业', maxCoe: null, minCoe: null, defCoe: 1, desc: '适用于水运工程的投资估算、初步设计概算、竣工决算和调整估算、调整概算(含征地拆迁和工程建设其他费)', isRoad: false, isRailway: false, isWaterway: true, order: 29, hasCost: false, hasArea: false },
|
28: { code: 'E4', name: '水运工程专业', maxCoe: null, minCoe: null, defCoe: 1, desc: '适用于水运工程的投资估算、初步设计概算、竣工决算和调整估算、调整概算(含征地拆迁和工程建设其他费)', isRoad: false, isRailway: false, isWaterway: true, order: 29, hasCost: false, hasArea: false ,industryId:'2' },
|
||||||
29: { code: 'E4-1', name: '临时工程', maxCoe: null, minCoe: null, defCoe: 1.1, desc: '适用于临时工程专业的施工图预算、招标工程量清单及清单预算(或最高投标限价)、合同(工程)结算、竣工决算和造价鉴定、计算工程量、工程变更费用咨询、工程成本测(核)算', isRoad: false, isRailway: false, isWaterway: true, order: 30, hasCost: true, hasArea: false },
|
29: { code: 'E4-1', name: '临时工程', maxCoe: null, minCoe: null, defCoe: 1.1, desc: '适用于临时工程专业的施工图预算、招标工程量清单及清单预算(或最高投标限价)、合同(工程)结算、竣工决算和造价鉴定、计算工程量、工程变更费用咨询、工程成本测(核)算', isRoad: false, isRailway: false, isWaterway: true, order: 30, hasCost: true, hasArea: false },
|
||||||
30: { code: 'E4-2', name: '土建工程', maxCoe: null, minCoe: null, defCoe: 1, desc: '适用于土建工程专业的施工图预算、招标工程量清单及清单预算(或最高投标限价)、合同(工程)结算、竣工决算和造价鉴定、计算工程量、工程变更费用咨询、工程成本测(核)算', isRoad: false, isRailway: false, isWaterway: true, order: 31, hasCost: true, hasArea: false },
|
30: { code: 'E4-2', name: '土建工程', maxCoe: null, minCoe: null, defCoe: 1, desc: '适用于土建工程专业的施工图预算、招标工程量清单及清单预算(或最高投标限价)、合同(工程)结算、竣工决算和造价鉴定、计算工程量、工程变更费用咨询、工程成本测(核)算', isRoad: false, isRailway: false, isWaterway: true, order: 31, hasCost: true, hasArea: false },
|
||||||
31: { code: 'E4-3', name: '机电与金属结构工程', maxCoe: null, minCoe: null, defCoe: 1.5, desc: '适用于机电与金属结构专业的施工图预算、招标工程量清单及清单预算(或最高投标限价)、合同(工程)结算和造价鉴定、计算工程量、工程变更费用咨询、工程成本测(核)算', isRoad: false, isRailway: false, isWaterway: true, order: 32, hasCost: true, hasArea: false },
|
31: { code: 'E4-3', name: '机电与金属结构工程', maxCoe: null, minCoe: null, defCoe: 1.5, desc: '适用于机电与金属结构专业的施工图预算、招标工程量清单及清单预算(或最高投标限价)、合同(工程)结算和造价鉴定、计算工程量、工程变更费用咨询、工程成本测(核)算', isRoad: false, isRailway: false, isWaterway: true, order: 32, hasCost: true, hasArea: false },
|
||||||
@ -176,11 +182,7 @@ let areaScaleCal = [
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
export const industryTypeList = [
|
|
||||||
{id:'0', name: '公路工程', type: 'isRoad' },
|
|
||||||
{id:'1', name: '铁路工程', type: 'isRailway' },
|
|
||||||
{id:'2', name: '水运工程', type: 'isWaterway' }
|
|
||||||
] as const
|
|
||||||
|
|
||||||
export type IndustryType = (typeof industryTypeList)[number]['type']
|
export type IndustryType = (typeof industryTypeList)[number]['type']
|
||||||
type DictItem = Record<string, any>
|
type DictItem = Record<string, any>
|
||||||
@ -314,12 +316,6 @@ export const getMajorDictEntries = (): DictEntry[] => buildDictEntries(majorList
|
|||||||
*/
|
*/
|
||||||
export const getServiceDictEntries = (): DictEntry[] => buildDictEntries(serviceList as Record<string, any>)
|
export const getServiceDictEntries = (): DictEntry[] => buildDictEntries(serviceList as Record<string, any>)
|
||||||
|
|
||||||
/**
|
|
||||||
* 判断字典项是否属于某行业(基于 isRoad/isRailway/isWaterway)。
|
|
||||||
* @returns 是否属于该行业
|
|
||||||
*/
|
|
||||||
export const isDictItemInIndustryScope = (item: DictItem | undefined, industryCode: unknown): boolean =>
|
|
||||||
isIndustryEnabledByType(item, getIndustryTypeValue(industryCode))
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 构建“专业ID -> 专业项”映射。
|
* 构建“专业ID -> 专业项”映射。
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user