1
This commit is contained in:
parent
e4c6203a98
commit
8417f8d5cc
@ -3,12 +3,14 @@ import { computed, onActivated, onMounted, ref, watch } from 'vue'
|
||||
import { Check, ChevronDown, Circle, CircleDot } from 'lucide-vue-next'
|
||||
import {
|
||||
getQuickCalcGroups,
|
||||
getMajorDictEntries,
|
||||
getServiceDictEntries,
|
||||
getMajorDictItemById,
|
||||
getServiceDictItemById,
|
||||
industryTypeList
|
||||
} from '@/sql'
|
||||
import { parseNumberOrNull } from '@/lib/number'
|
||||
import { useKvStore } from '@/pinia/kv'
|
||||
import { loadConsultCategoryFactorMap, loadMajorFactorMap } from '@/lib/xmFactorDefaults'
|
||||
import { getIndustryMajorEntry } from '@/lib/pricingScaleCalc'
|
||||
import { getBenchmarkBudgetSplitByScale, getScaleBudgetFeeSplit } from '@/lib/pricingScaleFee'
|
||||
import { QUICK_PROJECT_INFO_KEY } from '@/lib/workspace'
|
||||
import { initializeProjectFactorStates } from '@/lib/projectWorkspace'
|
||||
@ -47,42 +49,16 @@ type DictFactorItem = {
|
||||
onlyCostScale?: boolean | null
|
||||
}
|
||||
|
||||
type QuickCalcScaleMode = 'cost' | 'area'
|
||||
|
||||
const kvStore = useKvStore()
|
||||
const serviceDictEntries = getServiceDictEntries()
|
||||
const majorDictEntries = getMajorDictEntries()
|
||||
const serviceDictIndex = new Map<string, DictFactorItem>(
|
||||
serviceDictEntries.map(({ id, item }) => [
|
||||
`${String(item?.code || '').trim()}::${String(item?.name || '').trim()}`,
|
||||
{
|
||||
id: String(id),
|
||||
name: String(item?.name || ''),
|
||||
code: String(item?.code || ''),
|
||||
defCoe: typeof item?.defCoe === 'number' ? item.defCoe : null,
|
||||
scale: item?.scale === true,
|
||||
onlyCostScale: item?.onlyCostScale === true
|
||||
}
|
||||
])
|
||||
)
|
||||
const majorDictIndex = new Map<string, DictFactorItem>(
|
||||
majorDictEntries.map(({ id, item }) => [
|
||||
`${String(item?.code || '').trim()}::${String(item?.name || '').trim()}`,
|
||||
{
|
||||
id: String(id),
|
||||
name: String(item?.name || ''),
|
||||
code: String(item?.code || ''),
|
||||
defCoe: typeof item?.defCoe === 'number' ? item.defCoe : null,
|
||||
hasCost: item?.hasCost === true,
|
||||
hasArea: item?.hasArea === true
|
||||
}
|
||||
])
|
||||
)
|
||||
const consultFactorMap = ref<Map<string, number | null>>(new Map())
|
||||
const majorFactorMap = ref<Map<string, number | null>>(new Map())
|
||||
const quickCalcGroups = getQuickCalcGroups()
|
||||
|
||||
const projectIndustry = ref('')
|
||||
const selectedConsultLabel = ref('')
|
||||
const selectedMajor = ref<{ groupKey: string; label: string } | null>(null)
|
||||
const selectedConsultKey = ref('')
|
||||
const selectedMajor = ref<{ groupKey: string; optionKey: string } | null>(null)
|
||||
const investScale = ref('')
|
||||
const landScale = ref('')
|
||||
const workEnvFactor = ref('1')
|
||||
@ -90,20 +66,42 @@ const industrySaving = ref(false)
|
||||
let latestIndustryRequest = 0
|
||||
|
||||
const hasSelectedIndustry = computed(() => projectIndustry.value.trim() !== '')
|
||||
const hasSelectedConsult = computed(() => selectedConsultLabel.value.trim() !== '')
|
||||
const hasSelectedConsult = computed(() => selectedConsultKey.value.trim() !== '')
|
||||
|
||||
const mapDictItemToFactorItem = (id: string, item: Record<string, unknown> | undefined): DictFactorItem | null => {
|
||||
if (!item) return null
|
||||
return {
|
||||
id,
|
||||
name: String(item.name || ''),
|
||||
code: String(item.code || ''),
|
||||
defCoe: typeof item.defCoe === 'number' ? item.defCoe : null,
|
||||
hasCost: item.hasCost === true,
|
||||
hasArea: item.hasArea === true,
|
||||
scale: item.scale === true,
|
||||
onlyCostScale: item.onlyCostScale === true
|
||||
}
|
||||
}
|
||||
|
||||
const isDefined = <T,>(value: T | null | undefined): value is T => value != null
|
||||
|
||||
const resolveOptionId = (option: { id: string | Record<string, string> } | null) => {
|
||||
if (!option) return ''
|
||||
if (typeof option.id === 'string') return option.id
|
||||
const industry = projectIndustry.value.trim()
|
||||
return option.id[industry] || Object.values(option.id)[0] || ''
|
||||
}
|
||||
|
||||
const shouldShowMajorOption = (
|
||||
group: { key: string },
|
||||
option: { label: string; code: string | Record<string, string> }
|
||||
option: { id: string | Record<string, string> }
|
||||
) => {
|
||||
if (!hasSelectedConsult.value) return false
|
||||
if (!consultSupportsScale.value) return false
|
||||
const code = resolveOptionCode(option)
|
||||
if (consultOnlySupportsCostScale.value) return false
|
||||
const majorDictItem = getMajorDictItemById(resolveOptionId(option))
|
||||
const code = String(majorDictItem?.code || '').trim()
|
||||
if (!code) return false
|
||||
const isLeafMajor = code.includes('-')
|
||||
if (consultOnlySupportsCostScale.value) {
|
||||
return group.key !== 'general' && !isLeafMajor
|
||||
}
|
||||
return isLeafMajor
|
||||
}
|
||||
|
||||
@ -117,14 +115,15 @@ const visibleGroups = computed(() => {
|
||||
return group.industryId === industry
|
||||
})
|
||||
.map(group => {
|
||||
if (group.key === 'consult') return group
|
||||
const filteredItems = group.items.filter(item => shouldShowMajorOption(group, item))
|
||||
const visibleLabels = new Set(filteredItems.map(item => item.label))
|
||||
const filteredItems = group.key === 'consult'
|
||||
? group.items
|
||||
: group.items.filter(item => shouldShowMajorOption(group, item))
|
||||
const itemMap = new Map(filteredItems.map(item => [item.key, item]))
|
||||
return {
|
||||
...group,
|
||||
items: filteredItems,
|
||||
rows: group.rows
|
||||
.map(row => row.filter(label => visibleLabels.has(label)))
|
||||
.map(row => row.map(optionKey => itemMap.get(optionKey)).filter(isDefined))
|
||||
.filter(row => row.length > 0)
|
||||
}
|
||||
})
|
||||
@ -139,7 +138,7 @@ const industryLabel = computed(() => {
|
||||
const selectedConsultOption = computed(() =>
|
||||
quickCalcGroups
|
||||
.find(item => item.key === 'consult')
|
||||
?.items.find(item => item.label === selectedConsultLabel.value) || null
|
||||
?.items.find(item => item.key === selectedConsultKey.value) || null
|
||||
)
|
||||
const selectedMajorGroup = computed(() =>
|
||||
selectedMajor.value
|
||||
@ -149,24 +148,32 @@ const selectedMajorGroup = computed(() =>
|
||||
const selectedMajorOption = computed(() => {
|
||||
if (!selectedMajor.value) return null
|
||||
const group = visibleGroups.value.find(item => item.key === selectedMajor.value?.groupKey)
|
||||
return group?.items.find(item => item.label === selectedMajor.value?.label) || null
|
||||
return group?.items.find(item => item.key === selectedMajor.value?.optionKey) || null
|
||||
})
|
||||
|
||||
const resolveOptionCode = (option: { code: string | Record<string, string> } | null) => {
|
||||
if (!option) return ''
|
||||
if (typeof option.code === 'string') return option.code
|
||||
const industry = projectIndustry.value.trim()
|
||||
return option.code[industry] || Object.values(option.code)[0] || ''
|
||||
}
|
||||
|
||||
const selectedConsultCode = computed(() => resolveOptionCode(selectedConsultOption.value))
|
||||
const selectedMajorCode = computed(() => resolveOptionCode(selectedMajorOption.value))
|
||||
const selectedConsultId = computed(() => resolveOptionId(selectedConsultOption.value))
|
||||
const selectedMajorId = computed(() => resolveOptionId(selectedMajorOption.value))
|
||||
const selectedConsultCode = computed(() => String(selectedConsultDictItem.value?.code || ''))
|
||||
const selectedConsultLabel = computed(() => selectedConsultOption.value?.label || '')
|
||||
const selectedConsultDictItem = computed(() =>
|
||||
serviceDictIndex.get(`${selectedConsultCode.value}::${selectedConsultLabel.value}`) || null
|
||||
mapDictItemToFactorItem(selectedConsultId.value, getServiceDictItemById(selectedConsultId.value) as Record<string, unknown> | undefined)
|
||||
)
|
||||
const selectedMajorDictItem = computed(() => {
|
||||
if (!selectedMajor.value) return null
|
||||
return majorDictIndex.get(`${selectedMajorCode.value}::${selectedMajor.value.label}`) || null
|
||||
return mapDictItemToFactorItem(selectedMajorId.value, getMajorDictItemById(selectedMajorId.value) as Record<string, unknown> | undefined)
|
||||
})
|
||||
const effectiveMajorDictItem = computed(() => {
|
||||
if (selectedMajorDictItem.value) return selectedMajorDictItem.value
|
||||
if (!consultOnlySupportsCostScale.value) return null
|
||||
const industryMajorEntry = getIndustryMajorEntry(projectIndustry.value.trim())
|
||||
if (!industryMajorEntry) return null
|
||||
return {
|
||||
id: String(industryMajorEntry.id),
|
||||
name: String(industryMajorEntry.item?.name || ''),
|
||||
code: String(industryMajorEntry.item?.code || ''),
|
||||
defCoe: typeof industryMajorEntry.item?.defCoe === 'number' ? industryMajorEntry.item.defCoe : null,
|
||||
hasCost: industryMajorEntry.item?.hasCost === true,
|
||||
hasArea: industryMajorEntry.item?.hasArea === true
|
||||
}
|
||||
})
|
||||
const consultCategoryFactor = computed(() => {
|
||||
const serviceId = selectedConsultDictItem.value?.id
|
||||
@ -174,34 +181,52 @@ const consultCategoryFactor = computed(() => {
|
||||
return consultFactorMap.value.get(serviceId) ?? null
|
||||
})
|
||||
const defaultEngineeringMajorFactor = computed(() => {
|
||||
const majorId = selectedMajorDictItem.value?.id
|
||||
const majorId = effectiveMajorDictItem.value?.id
|
||||
if (!majorId) return null
|
||||
return majorFactorMap.value.get(majorId) ?? null
|
||||
})
|
||||
const engineeringMajorFactor = computed(() => defaultEngineeringMajorFactor.value)
|
||||
const hasResolvedMajor = computed(() => effectiveMajorDictItem.value != null)
|
||||
const majorSupportsCostScale = computed(() => effectiveMajorDictItem.value?.hasCost === true)
|
||||
const majorSupportsLandScale = computed(() => effectiveMajorDictItem.value?.hasArea === true)
|
||||
const preferLandScaleForDualMajor = computed(() => majorSupportsCostScale.value && majorSupportsLandScale.value)
|
||||
const workEnvCoefficient = computed(() => {
|
||||
const parsed = Number(workEnvFactor.value)
|
||||
return Number.isFinite(parsed) ? parsed : null
|
||||
})
|
||||
const consultSupportsScale = computed(() => selectedConsultDictItem.value?.scale === true)
|
||||
const consultOnlySupportsCostScale = computed(() => selectedConsultDictItem.value?.onlyCostScale === true)
|
||||
const canUseInvestScale = computed(() => consultSupportsScale.value)
|
||||
const canUseInvestScale = computed(() =>
|
||||
consultSupportsScale.value &&
|
||||
hasResolvedMajor.value &&
|
||||
(
|
||||
consultOnlySupportsCostScale.value ||
|
||||
(majorSupportsCostScale.value && !preferLandScaleForDualMajor.value)
|
||||
)
|
||||
)
|
||||
const canUseLandScale = computed(() =>
|
||||
consultSupportsScale.value &&
|
||||
!consultOnlySupportsCostScale.value
|
||||
hasResolvedMajor.value &&
|
||||
!consultOnlySupportsCostScale.value &&
|
||||
majorSupportsLandScale.value
|
||||
)
|
||||
const investScalePlaceholder = computed(() => {
|
||||
if (!selectedConsultLabel.value) return '请先选择咨询类别'
|
||||
if (!consultSupportsScale.value) return '当前分类不适用规模法'
|
||||
if (!hasResolvedMajor.value) return '请先选择工程专业'
|
||||
if (preferLandScaleForDualMajor.value) return '当前专业按用地规模计价'
|
||||
if (!consultOnlySupportsCostScale.value && !majorSupportsCostScale.value) return '当前专业不适用投资规模'
|
||||
return '请输入'
|
||||
})
|
||||
const landScalePlaceholder = computed(() => {
|
||||
if (!selectedConsultLabel.value) return '请先选择咨询类别'
|
||||
if (!consultSupportsScale.value) return '当前分类不适用规模法'
|
||||
if (!hasResolvedMajor.value) return '请先选择工程专业'
|
||||
if (consultOnlySupportsCostScale.value) return '当前分类仅支持投资规模'
|
||||
if (!majorSupportsLandScale.value) return '当前专业不适用用地规模'
|
||||
return '请输入'
|
||||
})
|
||||
const activeScaleMode = computed<'cost' | 'area' | null>(() => {
|
||||
const activeScaleMode = computed<QuickCalcScaleMode | null>(() => {
|
||||
const hasInvestValue = investScale.value.trim() !== ''
|
||||
const hasLandValue = landScale.value.trim() !== ''
|
||||
|
||||
@ -211,58 +236,97 @@ const activeScaleMode = computed<'cost' | 'area' | null>(() => {
|
||||
if (canUseLandScale.value) return 'area'
|
||||
return null
|
||||
})
|
||||
const benchmarkSplit = computed(() => {
|
||||
const scaleBudgetPreview = computed(() => {
|
||||
const mode = activeScaleMode.value
|
||||
if (!mode) return null
|
||||
return getBenchmarkBudgetSplitByScale(mode === 'cost' ? investScale.value : landScale.value, mode)
|
||||
const scaleValue = parseNumberOrNull(
|
||||
mode === 'cost' ? investScale.value : landScale.value,
|
||||
{ sanitize: true, precision: 3 }
|
||||
)
|
||||
if (scaleValue == null) return null
|
||||
console.log(mode)
|
||||
|
||||
const rawSplit = getBenchmarkBudgetSplitByScale(scaleValue, mode)
|
||||
console.log(rawSplit)
|
||||
if (!rawSplit) return null
|
||||
|
||||
const checkedSplit = {
|
||||
...rawSplit,
|
||||
basic: rawSplit.basic,
|
||||
optional: rawSplit.optional,
|
||||
total: rawSplit.total
|
||||
}
|
||||
const budgetFeeSplit = getScaleBudgetFeeSplit({
|
||||
benchmarkBudgetBasic: checkedSplit.basic,
|
||||
benchmarkBudgetOptional: checkedSplit.optional,
|
||||
majorFactor: engineeringMajorFactor.value,
|
||||
consultCategoryFactor: consultCategoryFactor.value,
|
||||
workStageFactor: workEnvCoefficient.value
|
||||
})
|
||||
|
||||
return {
|
||||
mode,
|
||||
scaleValue,
|
||||
benchmarkBudgetBasic: checkedSplit.basic,
|
||||
benchmarkBudgetOptional: checkedSplit.optional,
|
||||
benchmarkBudget: checkedSplit.total,
|
||||
basicFormula: rawSplit.basicFormula,
|
||||
optionalFormula: rawSplit.optionalFormula,
|
||||
budgetFeeBasic: budgetFeeSplit?.basic ?? null,
|
||||
budgetFeeOptional: budgetFeeSplit?.optional ?? null,
|
||||
budgetFeeTotal: budgetFeeSplit?.total ?? null
|
||||
}
|
||||
})
|
||||
const formulaText = computed(() => {
|
||||
const split = benchmarkSplit.value
|
||||
if (!split) return '请先选择工程行业、咨询类别、工程专业,并输入对应规模'
|
||||
const parts = [split.basicFormula, split.optionalFormula].filter(Boolean)
|
||||
const preview = scaleBudgetPreview.value
|
||||
if (!preview) return '请先选择输入对应规模'
|
||||
const parts = [preview.basicFormula, preview.optionalFormula].filter(Boolean)
|
||||
return parts.join(' + ') || '--'
|
||||
})
|
||||
const benchmarkAmountText = computed(() => {
|
||||
const total = benchmarkSplit.value?.total
|
||||
const total = scaleBudgetPreview.value?.benchmarkBudget
|
||||
if (total == null) return '--'
|
||||
return total.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
||||
})
|
||||
const computedBudgetAmount = computed(() => {
|
||||
const split = benchmarkSplit.value
|
||||
if (!split) return '--'
|
||||
const total = getScaleBudgetFeeSplit({
|
||||
benchmarkBudgetBasic: split.basic,
|
||||
benchmarkBudgetOptional: split.optional,
|
||||
majorFactor: engineeringMajorFactor.value,
|
||||
consultCategoryFactor: consultCategoryFactor.value,
|
||||
workStageFactor: workEnvCoefficient.value
|
||||
})?.total
|
||||
const total = scaleBudgetPreview.value?.budgetFeeTotal
|
||||
if (total == null) return '--'
|
||||
return total.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
||||
})
|
||||
|
||||
const applyScaleInput = (field: 'invest' | 'land') => {
|
||||
const currentValue = field === 'invest' ? investScale.value : landScale.value
|
||||
const next = parseNumberOrNull(currentValue, { sanitize: true, precision: 3 })
|
||||
const normalized = next == null ? '' : String(next)
|
||||
if (field === 'invest') {
|
||||
investScale.value = normalized
|
||||
return
|
||||
}
|
||||
landScale.value = normalized
|
||||
}
|
||||
|
||||
const totalSelectedCount = computed(() => {
|
||||
let count = 0
|
||||
if (selectedConsultLabel.value) count += 1
|
||||
if (selectedConsultKey.value) count += 1
|
||||
if (selectedMajor.value) count += 1
|
||||
return count
|
||||
})
|
||||
|
||||
const isOptionSelected = (groupKey: string, label: string) => {
|
||||
if (groupKey === 'consult') return selectedConsultLabel.value === label
|
||||
return selectedMajor.value?.groupKey === groupKey && selectedMajor.value?.label === label
|
||||
const isOptionSelected = (groupKey: string, optionKey: string) => {
|
||||
if (groupKey === 'consult') return selectedConsultKey.value === optionKey
|
||||
return selectedMajor.value?.groupKey === groupKey && selectedMajor.value?.optionKey === optionKey
|
||||
}
|
||||
|
||||
const toggleItem = (groupKey: string, label: string) => {
|
||||
const toggleItem = (groupKey: string, optionKey: string) => {
|
||||
if (groupKey === 'consult') {
|
||||
selectedConsultLabel.value = selectedConsultLabel.value === label ? '' : label
|
||||
selectedConsultKey.value = selectedConsultKey.value === optionKey ? '' : optionKey
|
||||
return
|
||||
}
|
||||
if (isOptionSelected(groupKey, label)) {
|
||||
if (isOptionSelected(groupKey, optionKey)) {
|
||||
selectedMajor.value = null
|
||||
return
|
||||
}
|
||||
selectedMajor.value = { groupKey, label }
|
||||
selectedMajor.value = { groupKey, optionKey }
|
||||
}
|
||||
|
||||
const loadFactorDefaults = async () => {
|
||||
@ -340,7 +404,7 @@ watch(
|
||||
() => {
|
||||
if (!selectedMajor.value) return
|
||||
const group = visibleGroups.value.find(item => item.key === selectedMajor.value?.groupKey)
|
||||
const stillVisible = group?.items.some(item => item.label === selectedMajor.value?.label) === true
|
||||
const stillVisible = group?.items.some(item => item.key === selectedMajor.value?.optionKey) === true
|
||||
if (!stillVisible) {
|
||||
selectedMajor.value = null
|
||||
}
|
||||
@ -426,6 +490,10 @@ watch(canUseLandScale, enabled => {
|
||||
当前咨询类别不适用规模法,因此不显示专业分类。
|
||||
</div>
|
||||
|
||||
<div v-else-if="consultOnlySupportsCostScale" class="quick-calc-empty-state">
|
||||
当前咨询类别按行业汇总计价,工程专业系数已按所选行业自动带入,不再显示内部互补专业行。
|
||||
</div>
|
||||
|
||||
<div class="quick-calc-catalog">
|
||||
<article
|
||||
v-for="group in visibleGroups"
|
||||
@ -449,26 +517,26 @@ watch(canUseLandScale, enabled => {
|
||||
class="quick-calc-selection-row"
|
||||
>
|
||||
<label
|
||||
v-for="label in row"
|
||||
:key="`${group.key}-${label}`"
|
||||
v-for="option in row"
|
||||
:key="`${group.key}-${option.key}`"
|
||||
class="quick-calc-inline-option"
|
||||
:class="{ 'is-selected': isOptionSelected(group.key, label) }"
|
||||
:class="{ 'is-selected': isOptionSelected(group.key, option.key) }"
|
||||
>
|
||||
<input
|
||||
:checked="isOptionSelected(group.key, label)"
|
||||
:checked="isOptionSelected(group.key, option.key)"
|
||||
type="radio"
|
||||
:name="group.key === 'consult' ? 'quick-calc-consult-choice' : 'quick-calc-major-choice'"
|
||||
class="quick-calc-option__input"
|
||||
@change="toggleItem(group.key, label)"
|
||||
@change="toggleItem(group.key, option.key)"
|
||||
>
|
||||
<span class="quick-calc-inline-option__icon" :class="{ 'is-selected': isOptionSelected(group.key, label) }">
|
||||
<CircleDot v-if="isOptionSelected(group.key, label)" class="h-3.5 w-3.5" />
|
||||
<span class="quick-calc-inline-option__icon" :class="{ 'is-selected': isOptionSelected(group.key, option.key) }">
|
||||
<CircleDot v-if="isOptionSelected(group.key, option.key)" class="h-3.5 w-3.5" />
|
||||
<Circle v-else class="h-3.5 w-3.5" />
|
||||
</span>
|
||||
<span
|
||||
class="quick-calc-inline-option__text"
|
||||
:class="{ 'is-selected': isOptionSelected(group.key, label) }"
|
||||
>{{ label }}</span>
|
||||
:class="{ 'is-selected': isOptionSelected(group.key, option.key) }"
|
||||
>{{ option.label }}</span>
|
||||
</label>
|
||||
|
||||
<div v-if="row.length === 0" class="quick-calc-selection-row__blank" />
|
||||
@ -520,10 +588,13 @@ watch(canUseLandScale, enabled => {
|
||||
<span class="quick-calc-field__label">投资规模(万元)</span>
|
||||
<input
|
||||
v-model="investScale"
|
||||
type="text"
|
||||
inputmode="decimal"
|
||||
class="quick-calc-field__input"
|
||||
:class="{ 'is-disabled': !canUseInvestScale }"
|
||||
:disabled="!canUseInvestScale"
|
||||
:placeholder="investScalePlaceholder"
|
||||
@blur="applyScaleInput('invest')"
|
||||
>
|
||||
</label>
|
||||
|
||||
@ -531,10 +602,13 @@ watch(canUseLandScale, enabled => {
|
||||
<span class="quick-calc-field__label">用地规模(亩)</span>
|
||||
<input
|
||||
v-model="landScale"
|
||||
type="text"
|
||||
inputmode="decimal"
|
||||
class="quick-calc-field__input"
|
||||
:class="{ 'is-disabled': !canUseLandScale }"
|
||||
:disabled="!canUseLandScale"
|
||||
:placeholder="landScalePlaceholder"
|
||||
@blur="applyScaleInput('land')"
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
@ -898,18 +972,18 @@ watch(canUseLandScale, enabled => {
|
||||
|
||||
.quick-calc-group__rows {
|
||||
display: grid;
|
||||
grid-template-rows: repeat(3, minmax(46px, auto));
|
||||
grid-auto-rows: minmax(46px, auto);
|
||||
background:
|
||||
linear-gradient(180deg, color-mix(in srgb, white 18%, transparent) 0%, transparent 100%);
|
||||
}
|
||||
|
||||
.quick-calc-group__rows:has(.quick-calc-selection-row:only-child) {
|
||||
grid-template-rows: minmax(46px, auto);
|
||||
grid-auto-rows: minmax(46px, auto);
|
||||
}
|
||||
|
||||
.quick-calc-group--general .quick-calc-group__rows,
|
||||
.quick-calc-group--general .quick-calc-group__rows:has(.quick-calc-selection-row:only-child) {
|
||||
grid-template-rows: minmax(38px, auto);
|
||||
grid-auto-rows: minmax(38px, auto);
|
||||
}
|
||||
|
||||
.quick-calc-selection-row {
|
||||
|
||||
@ -12,6 +12,16 @@ export interface ScaleFeeSplitResult {
|
||||
optionalFormula: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据“规模值 + 规模类型”查表,得到基准预算拆分结果。
|
||||
*
|
||||
* 这个方法只负责“规模法基准预算”这一层:
|
||||
* 1. 按投资规模或用地规模命中费率表
|
||||
* 2. 返回基准预算的基本部分 / 附加部分 / 合计
|
||||
* 3. 同时返回对应的计算式文本,供页面展示
|
||||
*
|
||||
* 它不参与任何咨询分类系数、专业系数、阶段系数的计算。
|
||||
*/
|
||||
export const getBenchmarkBudgetSplitByScale = (
|
||||
value: unknown,
|
||||
mode: ScaleMode
|
||||
@ -40,6 +50,20 @@ export const getBenchmarkBudgetByScale = (value: unknown, mode: ScaleMode) => {
|
||||
return splitResult ? splitResult.total : null
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据“已算出的基准预算拆分 + 各类系数”计算最终服务预算。
|
||||
*
|
||||
* 这个方法负责“乘系数”这一层:
|
||||
* 1. 输入的是已经拆好的基准预算 basic / optional
|
||||
* 2. 再乘咨询分类系数、专业系数、阶段系数、工作占比
|
||||
* 3. 返回服务费的基本部分 / 附加部分 / 合计
|
||||
*
|
||||
* 和 `getBenchmarkBudgetSplitByScale` 的区别是:
|
||||
* - `getBenchmarkBudgetSplitByScale` 解决“规模值对应多少基准预算”
|
||||
* - `getScaleBudgetFeeSplit` 解决“基准预算乘完系数后服务费是多少”
|
||||
*
|
||||
* 这里不返回计算式文本,所以 `basicFormula / optionalFormula` 固定为空字符串。
|
||||
*/
|
||||
export const getScaleBudgetFeeSplit = (params: {
|
||||
benchmarkBudgetBasic: unknown
|
||||
benchmarkBudgetOptional: unknown
|
||||
|
||||
161
src/sql.ts
161
src/sql.ts
@ -675,8 +675,9 @@ export const getServiceDictItemById = (id: string | number): DictItem | undefine
|
||||
}
|
||||
|
||||
export type QuickCalcOption = {
|
||||
key: string
|
||||
label: string
|
||||
code: string | Record<string, string>
|
||||
id: string | Record<string, string>
|
||||
}
|
||||
|
||||
export type QuickCalcGroup = {
|
||||
@ -691,25 +692,27 @@ export type QuickCalcGroup = {
|
||||
const getQuickDictLabel = (item: DictItem | undefined, fallback = '') =>
|
||||
String(item?.quickLabel || item?.name || fallback)
|
||||
|
||||
const createQuickOptionByServiceCode = (
|
||||
code: string,
|
||||
const createQuickOptionByServiceKey = (
|
||||
key: string,
|
||||
override?: Partial<QuickCalcOption> & { label?: string }
|
||||
): QuickCalcOption => {
|
||||
const entry = getServiceDictEntries().find(item => String(item.item?.code || '') === code)
|
||||
const entry = getServiceDictEntries().find(item => String(item.id || '') === key)
|
||||
return {
|
||||
label: override?.label || getQuickDictLabel(entry?.item, code),
|
||||
code: override?.code || code
|
||||
key: override?.key || key,
|
||||
label: override?.label || getQuickDictLabel(entry?.item, key),
|
||||
id: override?.id || key
|
||||
}
|
||||
}
|
||||
|
||||
const createQuickOptionByMajorCode = (
|
||||
code: string,
|
||||
const createQuickOptionByMajorKey = (
|
||||
key: string,
|
||||
override?: Partial<QuickCalcOption> & { label?: string }
|
||||
): QuickCalcOption => {
|
||||
const entry = getMajorDictEntries().find(item => String(item.item?.code || '') === code)
|
||||
const entry = getMajorDictEntries().find(item => String(item.id || '') === key)
|
||||
return {
|
||||
label: override?.label || getQuickDictLabel(entry?.item, code),
|
||||
code: override?.code || code
|
||||
key: override?.key || key,
|
||||
label: override?.label || getQuickDictLabel(entry?.item, key),
|
||||
id: override?.id || key
|
||||
}
|
||||
}
|
||||
|
||||
@ -719,28 +722,42 @@ export const getQuickCalcGroups = (): QuickCalcGroup[] => [
|
||||
label: '咨询类别(常用)',
|
||||
hint: '先选择咨询类别,再补规模和预算参数。',
|
||||
items: [
|
||||
createQuickOptionByServiceCode('D1'),
|
||||
createQuickOptionByServiceCode('D2-1'),
|
||||
createQuickOptionByServiceCode('D2-2-1', { code: { '0': 'D2-2-1', '1': 'D2-2-2', '2': 'D2-2-1' } }),
|
||||
createQuickOptionByServiceCode('D3-1'),
|
||||
createQuickOptionByServiceCode('D3-2'),
|
||||
createQuickOptionByServiceCode('D3-3'),
|
||||
createQuickOptionByServiceCode('D3-4'),
|
||||
createQuickOptionByServiceCode('D3-5'),
|
||||
createQuickOptionByServiceCode('D3-6-1', { code: { '0': 'D3-6-1', '1': 'D3-6-2', '2': 'D3-6-1' } }),
|
||||
createQuickOptionByServiceCode('D3-7'),
|
||||
createQuickOptionByServiceCode('D4-6'),
|
||||
createQuickOptionByServiceCode('D4-7'),
|
||||
createQuickOptionByServiceCode('D4-8'),
|
||||
createQuickOptionByServiceCode('D4-9'),
|
||||
createQuickOptionByServiceCode('D4-10'),
|
||||
createQuickOptionByServiceCode('D4-11'),
|
||||
createQuickOptionByServiceCode('D4-12')
|
||||
createQuickOptionByServiceKey('0'),
|
||||
createQuickOptionByServiceKey('2'),
|
||||
createQuickOptionByServiceKey('3', {
|
||||
key: 'D2-2',
|
||||
id: {//行业不同显示的不同
|
||||
'0': '3',
|
||||
'1': '4',
|
||||
'2': '3'
|
||||
}
|
||||
}),
|
||||
createQuickOptionByServiceKey('6'),
|
||||
createQuickOptionByServiceKey('7'),
|
||||
createQuickOptionByServiceKey('8'),
|
||||
createQuickOptionByServiceKey('9'),
|
||||
createQuickOptionByServiceKey('10'),
|
||||
createQuickOptionByServiceKey('11', {
|
||||
key: 'D3-6',
|
||||
id: {
|
||||
'0': '11',
|
||||
'1': '12',
|
||||
'2': '11'
|
||||
}
|
||||
}),
|
||||
createQuickOptionByServiceKey('13'),
|
||||
createQuickOptionByServiceKey('20'),
|
||||
createQuickOptionByServiceKey('21'),
|
||||
createQuickOptionByServiceKey('22'),
|
||||
createQuickOptionByServiceKey('23'),
|
||||
createQuickOptionByServiceKey('24'),
|
||||
createQuickOptionByServiceKey('25'),
|
||||
createQuickOptionByServiceKey('26')
|
||||
],
|
||||
rows: [
|
||||
['全过程造价咨询', '项目前期阶段造价咨询', '项目实施阶段造价咨询'],
|
||||
['投资估算', '设计概算', '施工图预算', '招标工程量清单及清单预算(或最高投标限价)', '清理概算(仅限铁路)', '合同(工程)结算', '竣工决算'],
|
||||
['造价鉴定', '工程成本测算', '工程成本核算', '计算工程量', '工程变更费用咨询', '调整估算', '调整概算']
|
||||
['0', '2', 'D2-2'],
|
||||
['6', '7', '8', '9', '10', 'D3-6', '13'],
|
||||
['20', '21', '22', '23', '24', '25', '26']
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -748,12 +765,12 @@ export const getQuickCalcGroups = (): QuickCalcGroup[] => [
|
||||
label: '通用专业',
|
||||
hint: '跨行业共用的补偿与其他费用专业。',
|
||||
items: [
|
||||
createQuickOptionByMajorCode('E1-1'),
|
||||
createQuickOptionByMajorCode('E1-2'),
|
||||
createQuickOptionByMajorCode('E1-3'),
|
||||
createQuickOptionByMajorCode('E1-4')
|
||||
createQuickOptionByMajorKey('1'),
|
||||
createQuickOptionByMajorKey('2'),
|
||||
createQuickOptionByMajorKey('3'),
|
||||
createQuickOptionByMajorKey('4')
|
||||
],
|
||||
rows: [['征地(用海)补偿', '拆迁补偿', '迁改工程等费用', '工程建设其他费']]
|
||||
rows: [['1', '2', '3', '4']]
|
||||
},
|
||||
{
|
||||
key: 'road',
|
||||
@ -761,22 +778,22 @@ export const getQuickCalcGroups = (): QuickCalcGroup[] => [
|
||||
hint: '首页行业为公路工程时默认展示。',
|
||||
industryId: '0',
|
||||
items: [
|
||||
createQuickOptionByMajorCode('E2'),
|
||||
createQuickOptionByMajorCode('E2-1'),
|
||||
createQuickOptionByMajorCode('E2-2'),
|
||||
createQuickOptionByMajorCode('E2-3'),
|
||||
createQuickOptionByMajorCode('E2-4'),
|
||||
createQuickOptionByMajorCode('E2-5'),
|
||||
createQuickOptionByMajorCode('E2-6'),
|
||||
createQuickOptionByMajorCode('E2-7'),
|
||||
createQuickOptionByMajorCode('E2-8'),
|
||||
createQuickOptionByMajorCode('E2-9'),
|
||||
createQuickOptionByMajorCode('E2-10')
|
||||
createQuickOptionByMajorKey('7'),
|
||||
createQuickOptionByMajorKey('8'),
|
||||
createQuickOptionByMajorKey('9'),
|
||||
createQuickOptionByMajorKey('10'),
|
||||
createQuickOptionByMajorKey('11'),
|
||||
createQuickOptionByMajorKey('12'),
|
||||
createQuickOptionByMajorKey('13'),
|
||||
createQuickOptionByMajorKey('14'),
|
||||
createQuickOptionByMajorKey('15'),
|
||||
createQuickOptionByMajorKey('16'),
|
||||
createQuickOptionByMajorKey('17')
|
||||
],
|
||||
rows: [
|
||||
['建设工程项目'],
|
||||
['临时工程', '路基工程', '路面工程', '桥涵工程', '隧道工程', '交叉工程'],
|
||||
['机电工程', '交通安全设施工程', '绿化及环境保护工程', '房建工程']
|
||||
['7'],
|
||||
['8', '9', '10', '11', '12', '13'],
|
||||
['14', '15', '16', '17']
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -785,21 +802,21 @@ export const getQuickCalcGroups = (): QuickCalcGroup[] => [
|
||||
hint: '首页行业为铁路工程时默认展示。',
|
||||
industryId: '1',
|
||||
items: [
|
||||
createQuickOptionByMajorCode('E3'),
|
||||
createQuickOptionByMajorCode('E3-1'),
|
||||
createQuickOptionByMajorCode('E3-2'),
|
||||
createQuickOptionByMajorCode('E3-3'),
|
||||
createQuickOptionByMajorCode('E3-4'),
|
||||
createQuickOptionByMajorCode('E3-5'),
|
||||
createQuickOptionByMajorCode('E3-6'),
|
||||
createQuickOptionByMajorCode('E3-7'),
|
||||
createQuickOptionByMajorCode('E3-8'),
|
||||
createQuickOptionByMajorCode('E3-9')
|
||||
createQuickOptionByMajorKey('18'),
|
||||
createQuickOptionByMajorKey('19'),
|
||||
createQuickOptionByMajorKey('20'),
|
||||
createQuickOptionByMajorKey('21'),
|
||||
createQuickOptionByMajorKey('22'),
|
||||
createQuickOptionByMajorKey('23'),
|
||||
createQuickOptionByMajorKey('24'),
|
||||
createQuickOptionByMajorKey('25'),
|
||||
createQuickOptionByMajorKey('26'),
|
||||
createQuickOptionByMajorKey('27')
|
||||
],
|
||||
rows: [
|
||||
['建设工程项目'],
|
||||
['大型临时设施和过渡工程', '路基工程', '桥涵工程', '隧道及明洞工程', '轨道工程'],
|
||||
['通信、信号、信息及灾害监测工程', '电力及电力牵引供电工程', '房建工程(房屋建筑及附属工程)', '装饰装修工程']
|
||||
['18'],
|
||||
['19', '20', '21', '22', '23'],
|
||||
['24', '25', '26', '27']
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -808,17 +825,17 @@ export const getQuickCalcGroups = (): QuickCalcGroup[] => [
|
||||
hint: '首页行业为水运工程时默认展示。',
|
||||
industryId: '2',
|
||||
items: [
|
||||
createQuickOptionByMajorCode('E4'),
|
||||
createQuickOptionByMajorCode('E4-1'),
|
||||
createQuickOptionByMajorCode('E4-2'),
|
||||
createQuickOptionByMajorCode('E4-3'),
|
||||
createQuickOptionByMajorCode('E4-4'),
|
||||
createQuickOptionByMajorCode('E4-5', { label: '房建工程(房屋建筑及附属工程)' })
|
||||
createQuickOptionByMajorKey('28'),
|
||||
createQuickOptionByMajorKey('29'),
|
||||
createQuickOptionByMajorKey('30'),
|
||||
createQuickOptionByMajorKey('31'),
|
||||
createQuickOptionByMajorKey('32'),
|
||||
createQuickOptionByMajorKey('33', { label: '房建工程(房屋建筑及附属工程)' })
|
||||
],
|
||||
rows: [
|
||||
['建设工程项目'],
|
||||
['临时工程', '土建工程'],
|
||||
['机电与金属结构工程', '设备工程', '房建工程(房屋建筑及附属工程)']
|
||||
['28'],
|
||||
['29', '30'],
|
||||
['31', '32', '33']
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user