This commit is contained in:
wintsa 2026-03-24 10:20:33 +08:00
parent e4c6203a98
commit 8417f8d5cc
3 changed files with 283 additions and 168 deletions

View File

@ -3,12 +3,14 @@ import { computed, onActivated, onMounted, ref, watch } from 'vue'
import { Check, ChevronDown, Circle, CircleDot } from 'lucide-vue-next' import { Check, ChevronDown, Circle, CircleDot } from 'lucide-vue-next'
import { import {
getQuickCalcGroups, getQuickCalcGroups,
getMajorDictEntries, getMajorDictItemById,
getServiceDictEntries, getServiceDictItemById,
industryTypeList industryTypeList
} from '@/sql' } from '@/sql'
import { parseNumberOrNull } from '@/lib/number'
import { useKvStore } from '@/pinia/kv' import { useKvStore } from '@/pinia/kv'
import { loadConsultCategoryFactorMap, loadMajorFactorMap } from '@/lib/xmFactorDefaults' import { loadConsultCategoryFactorMap, loadMajorFactorMap } from '@/lib/xmFactorDefaults'
import { getIndustryMajorEntry } from '@/lib/pricingScaleCalc'
import { getBenchmarkBudgetSplitByScale, getScaleBudgetFeeSplit } from '@/lib/pricingScaleFee' import { getBenchmarkBudgetSplitByScale, getScaleBudgetFeeSplit } from '@/lib/pricingScaleFee'
import { QUICK_PROJECT_INFO_KEY } from '@/lib/workspace' import { QUICK_PROJECT_INFO_KEY } from '@/lib/workspace'
import { initializeProjectFactorStates } from '@/lib/projectWorkspace' import { initializeProjectFactorStates } from '@/lib/projectWorkspace'
@ -47,42 +49,16 @@ type DictFactorItem = {
onlyCostScale?: boolean | null onlyCostScale?: boolean | null
} }
type QuickCalcScaleMode = 'cost' | 'area'
const kvStore = useKvStore() 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 consultFactorMap = 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())
const quickCalcGroups = getQuickCalcGroups() const quickCalcGroups = getQuickCalcGroups()
const projectIndustry = ref('') const projectIndustry = ref('')
const selectedConsultLabel = ref('') const selectedConsultKey = ref('')
const selectedMajor = ref<{ groupKey: string; label: string } | null>(null) const selectedMajor = ref<{ groupKey: string; optionKey: string } | null>(null)
const investScale = ref('') const investScale = ref('')
const landScale = ref('') const landScale = ref('')
const workEnvFactor = ref('1') const workEnvFactor = ref('1')
@ -90,20 +66,42 @@ const industrySaving = ref(false)
let latestIndustryRequest = 0 let latestIndustryRequest = 0
const hasSelectedIndustry = computed(() => projectIndustry.value.trim() !== '') 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 = ( const shouldShowMajorOption = (
group: { key: string }, group: { key: string },
option: { label: string; code: string | Record<string, string> } option: { id: string | Record<string, string> }
) => { ) => {
if (!hasSelectedConsult.value) return false if (!hasSelectedConsult.value) return false
if (!consultSupportsScale.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 if (!code) return false
const isLeafMajor = code.includes('-') const isLeafMajor = code.includes('-')
if (consultOnlySupportsCostScale.value) {
return group.key !== 'general' && !isLeafMajor
}
return isLeafMajor return isLeafMajor
} }
@ -117,14 +115,15 @@ const visibleGroups = computed(() => {
return group.industryId === industry return group.industryId === industry
}) })
.map(group => { .map(group => {
if (group.key === 'consult') return group const filteredItems = group.key === 'consult'
const filteredItems = group.items.filter(item => shouldShowMajorOption(group, item)) ? group.items
const visibleLabels = new Set(filteredItems.map(item => item.label)) : group.items.filter(item => shouldShowMajorOption(group, item))
const itemMap = new Map(filteredItems.map(item => [item.key, item]))
return { return {
...group, ...group,
items: filteredItems, items: filteredItems,
rows: group.rows 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) .filter(row => row.length > 0)
} }
}) })
@ -139,7 +138,7 @@ const industryLabel = computed(() => {
const selectedConsultOption = computed(() => const selectedConsultOption = computed(() =>
quickCalcGroups quickCalcGroups
.find(item => item.key === 'consult') .find(item => item.key === 'consult')
?.items.find(item => item.label === selectedConsultLabel.value) || null ?.items.find(item => item.key === selectedConsultKey.value) || null
) )
const selectedMajorGroup = computed(() => const selectedMajorGroup = computed(() =>
selectedMajor.value selectedMajor.value
@ -149,24 +148,32 @@ const selectedMajorGroup = computed(() =>
const selectedMajorOption = computed(() => { const selectedMajorOption = computed(() => {
if (!selectedMajor.value) return null if (!selectedMajor.value) return null
const group = visibleGroups.value.find(item => item.key === selectedMajor.value?.groupKey) 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 selectedConsultId = computed(() => resolveOptionId(selectedConsultOption.value))
const resolveOptionCode = (option: { code: string | Record<string, string> } | null) => { const selectedMajorId = computed(() => resolveOptionId(selectedMajorOption.value))
if (!option) return '' const selectedConsultCode = computed(() => String(selectedConsultDictItem.value?.code || ''))
if (typeof option.code === 'string') return option.code const selectedConsultLabel = computed(() => selectedConsultOption.value?.label || '')
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 selectedConsultDictItem = computed(() => const selectedConsultDictItem = computed(() =>
serviceDictIndex.get(`${selectedConsultCode.value}::${selectedConsultLabel.value}`) || null mapDictItemToFactorItem(selectedConsultId.value, getServiceDictItemById(selectedConsultId.value) as Record<string, unknown> | undefined)
) )
const selectedMajorDictItem = computed(() => { const selectedMajorDictItem = computed(() => {
if (!selectedMajor.value) return null 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 consultCategoryFactor = computed(() => {
const serviceId = selectedConsultDictItem.value?.id const serviceId = selectedConsultDictItem.value?.id
@ -174,34 +181,52 @@ const consultCategoryFactor = computed(() => {
return consultFactorMap.value.get(serviceId) ?? null return consultFactorMap.value.get(serviceId) ?? null
}) })
const defaultEngineeringMajorFactor = computed(() => { const defaultEngineeringMajorFactor = computed(() => {
const majorId = selectedMajorDictItem.value?.id const majorId = effectiveMajorDictItem.value?.id
if (!majorId) return null if (!majorId) return null
return majorFactorMap.value.get(majorId) ?? null return majorFactorMap.value.get(majorId) ?? null
}) })
const engineeringMajorFactor = computed(() => defaultEngineeringMajorFactor.value) 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 workEnvCoefficient = computed(() => {
const parsed = Number(workEnvFactor.value) const parsed = Number(workEnvFactor.value)
return Number.isFinite(parsed) ? parsed : null return Number.isFinite(parsed) ? parsed : null
}) })
const consultSupportsScale = computed(() => selectedConsultDictItem.value?.scale === true) const consultSupportsScale = computed(() => selectedConsultDictItem.value?.scale === true)
const consultOnlySupportsCostScale = computed(() => selectedConsultDictItem.value?.onlyCostScale === 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(() => const canUseLandScale = computed(() =>
consultSupportsScale.value && consultSupportsScale.value &&
!consultOnlySupportsCostScale.value hasResolvedMajor.value &&
!consultOnlySupportsCostScale.value &&
majorSupportsLandScale.value
) )
const investScalePlaceholder = computed(() => { const investScalePlaceholder = computed(() => {
if (!selectedConsultLabel.value) return '请先选择咨询类别' if (!selectedConsultLabel.value) return '请先选择咨询类别'
if (!consultSupportsScale.value) return '当前分类不适用规模法' if (!consultSupportsScale.value) return '当前分类不适用规模法'
if (!hasResolvedMajor.value) return '请先选择工程专业'
if (preferLandScaleForDualMajor.value) return '当前专业按用地规模计价'
if (!consultOnlySupportsCostScale.value && !majorSupportsCostScale.value) return '当前专业不适用投资规模'
return '请输入' return '请输入'
}) })
const landScalePlaceholder = computed(() => { const landScalePlaceholder = computed(() => {
if (!selectedConsultLabel.value) return '请先选择咨询类别' if (!selectedConsultLabel.value) return '请先选择咨询类别'
if (!consultSupportsScale.value) return '当前分类不适用规模法' if (!consultSupportsScale.value) return '当前分类不适用规模法'
if (!hasResolvedMajor.value) return '请先选择工程专业'
if (consultOnlySupportsCostScale.value) return '当前分类仅支持投资规模' if (consultOnlySupportsCostScale.value) return '当前分类仅支持投资规模'
if (!majorSupportsLandScale.value) return '当前专业不适用用地规模'
return '请输入' return '请输入'
}) })
const activeScaleMode = computed<'cost' | 'area' | null>(() => { const activeScaleMode = computed<QuickCalcScaleMode | null>(() => {
const hasInvestValue = investScale.value.trim() !== '' const hasInvestValue = investScale.value.trim() !== ''
const hasLandValue = landScale.value.trim() !== '' const hasLandValue = landScale.value.trim() !== ''
@ -211,58 +236,97 @@ const activeScaleMode = computed<'cost' | 'area' | null>(() => {
if (canUseLandScale.value) return 'area' if (canUseLandScale.value) return 'area'
return null return null
}) })
const benchmarkSplit = computed(() => { const scaleBudgetPreview = computed(() => {
const mode = activeScaleMode.value const mode = activeScaleMode.value
if (!mode) return null 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 formulaText = computed(() => {
const split = benchmarkSplit.value const preview = scaleBudgetPreview.value
if (!split) return '请先选择工程行业、咨询类别、工程专业,并输入对应规模' if (!preview) return '请先选择输入对应规模'
const parts = [split.basicFormula, split.optionalFormula].filter(Boolean) const parts = [preview.basicFormula, preview.optionalFormula].filter(Boolean)
return parts.join(' + ') || '--' return parts.join(' + ') || '--'
}) })
const benchmarkAmountText = computed(() => { const benchmarkAmountText = computed(() => {
const total = benchmarkSplit.value?.total const total = scaleBudgetPreview.value?.benchmarkBudget
if (total == null) return '--' if (total == null) return '--'
return total.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) return total.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
}) })
const computedBudgetAmount = computed(() => { const computedBudgetAmount = computed(() => {
const split = benchmarkSplit.value const total = scaleBudgetPreview.value?.budgetFeeTotal
if (!split) return '--'
const total = getScaleBudgetFeeSplit({
benchmarkBudgetBasic: split.basic,
benchmarkBudgetOptional: split.optional,
majorFactor: engineeringMajorFactor.value,
consultCategoryFactor: consultCategoryFactor.value,
workStageFactor: workEnvCoefficient.value
})?.total
if (total == null) return '--' if (total == null) return '--'
return total.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) 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(() => { const totalSelectedCount = computed(() => {
let count = 0 let count = 0
if (selectedConsultLabel.value) count += 1 if (selectedConsultKey.value) count += 1
if (selectedMajor.value) count += 1 if (selectedMajor.value) count += 1
return count return count
}) })
const isOptionSelected = (groupKey: string, label: string) => { const isOptionSelected = (groupKey: string, optionKey: string) => {
if (groupKey === 'consult') return selectedConsultLabel.value === label if (groupKey === 'consult') return selectedConsultKey.value === optionKey
return selectedMajor.value?.groupKey === groupKey && selectedMajor.value?.label === label return selectedMajor.value?.groupKey === groupKey && selectedMajor.value?.optionKey === optionKey
} }
const toggleItem = (groupKey: string, label: string) => { const toggleItem = (groupKey: string, optionKey: string) => {
if (groupKey === 'consult') { if (groupKey === 'consult') {
selectedConsultLabel.value = selectedConsultLabel.value === label ? '' : label selectedConsultKey.value = selectedConsultKey.value === optionKey ? '' : optionKey
return return
} }
if (isOptionSelected(groupKey, label)) { if (isOptionSelected(groupKey, optionKey)) {
selectedMajor.value = null selectedMajor.value = null
return return
} }
selectedMajor.value = { groupKey, label } selectedMajor.value = { groupKey, optionKey }
} }
const loadFactorDefaults = async () => { const loadFactorDefaults = async () => {
@ -340,7 +404,7 @@ watch(
() => { () => {
if (!selectedMajor.value) return if (!selectedMajor.value) return
const group = visibleGroups.value.find(item => item.key === selectedMajor.value?.groupKey) 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) { if (!stillVisible) {
selectedMajor.value = null selectedMajor.value = null
} }
@ -426,6 +490,10 @@ watch(canUseLandScale, enabled => {
当前咨询类别不适用规模法因此不显示专业分类 当前咨询类别不适用规模法因此不显示专业分类
</div> </div>
<div v-else-if="consultOnlySupportsCostScale" class="quick-calc-empty-state">
当前咨询类别按行业汇总计价工程专业系数已按所选行业自动带入不再显示内部互补专业行
</div>
<div class="quick-calc-catalog"> <div class="quick-calc-catalog">
<article <article
v-for="group in visibleGroups" v-for="group in visibleGroups"
@ -449,26 +517,26 @@ watch(canUseLandScale, enabled => {
class="quick-calc-selection-row" class="quick-calc-selection-row"
> >
<label <label
v-for="label in row" v-for="option in row"
:key="`${group.key}-${label}`" :key="`${group.key}-${option.key}`"
class="quick-calc-inline-option" class="quick-calc-inline-option"
:class="{ 'is-selected': isOptionSelected(group.key, label) }" :class="{ 'is-selected': isOptionSelected(group.key, option.key) }"
> >
<input <input
:checked="isOptionSelected(group.key, label)" :checked="isOptionSelected(group.key, option.key)"
type="radio" type="radio"
:name="group.key === 'consult' ? 'quick-calc-consult-choice' : 'quick-calc-major-choice'" :name="group.key === 'consult' ? 'quick-calc-consult-choice' : 'quick-calc-major-choice'"
class="quick-calc-option__input" 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) }"> <span class="quick-calc-inline-option__icon" :class="{ 'is-selected': isOptionSelected(group.key, option.key) }">
<CircleDot v-if="isOptionSelected(group.key, label)" class="h-3.5 w-3.5" /> <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" /> <Circle v-else class="h-3.5 w-3.5" />
</span> </span>
<span <span
class="quick-calc-inline-option__text" class="quick-calc-inline-option__text"
:class="{ 'is-selected': isOptionSelected(group.key, label) }" :class="{ 'is-selected': isOptionSelected(group.key, option.key) }"
>{{ label }}</span> >{{ option.label }}</span>
</label> </label>
<div v-if="row.length === 0" class="quick-calc-selection-row__blank" /> <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> <span class="quick-calc-field__label">投资规模万元</span>
<input <input
v-model="investScale" v-model="investScale"
type="text"
inputmode="decimal"
class="quick-calc-field__input" class="quick-calc-field__input"
:class="{ 'is-disabled': !canUseInvestScale }" :class="{ 'is-disabled': !canUseInvestScale }"
:disabled="!canUseInvestScale" :disabled="!canUseInvestScale"
:placeholder="investScalePlaceholder" :placeholder="investScalePlaceholder"
@blur="applyScaleInput('invest')"
> >
</label> </label>
@ -531,10 +602,13 @@ watch(canUseLandScale, enabled => {
<span class="quick-calc-field__label">用地规模</span> <span class="quick-calc-field__label">用地规模</span>
<input <input
v-model="landScale" v-model="landScale"
type="text"
inputmode="decimal"
class="quick-calc-field__input" class="quick-calc-field__input"
:class="{ 'is-disabled': !canUseLandScale }" :class="{ 'is-disabled': !canUseLandScale }"
:disabled="!canUseLandScale" :disabled="!canUseLandScale"
:placeholder="landScalePlaceholder" :placeholder="landScalePlaceholder"
@blur="applyScaleInput('land')"
> >
</label> </label>
</div> </div>
@ -898,18 +972,18 @@ watch(canUseLandScale, enabled => {
.quick-calc-group__rows { .quick-calc-group__rows {
display: grid; display: grid;
grid-template-rows: repeat(3, minmax(46px, auto)); grid-auto-rows: minmax(46px, auto);
background: background:
linear-gradient(180deg, color-mix(in srgb, white 18%, transparent) 0%, transparent 100%); linear-gradient(180deg, color-mix(in srgb, white 18%, transparent) 0%, transparent 100%);
} }
.quick-calc-group__rows:has(.quick-calc-selection-row:only-child) { .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,
.quick-calc-group--general .quick-calc-group__rows:has(.quick-calc-selection-row:only-child) { .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 { .quick-calc-selection-row {

View File

@ -12,6 +12,16 @@ export interface ScaleFeeSplitResult {
optionalFormula: string optionalFormula: string
} }
/**
* +
*
*
* 1.
* 2. / /
* 3.
*
*
*/
export const getBenchmarkBudgetSplitByScale = ( export const getBenchmarkBudgetSplitByScale = (
value: unknown, value: unknown,
mode: ScaleMode mode: ScaleMode
@ -40,6 +50,20 @@ export const getBenchmarkBudgetByScale = (value: unknown, mode: ScaleMode) => {
return splitResult ? splitResult.total : null return splitResult ? splitResult.total : null
} }
/**
* +
*
*
* 1. basic / optional
* 2.
* 3. / /
*
* `getBenchmarkBudgetSplitByScale`
* - `getBenchmarkBudgetSplitByScale`
* - `getScaleBudgetFeeSplit`
*
* `basicFormula / optionalFormula`
*/
export const getScaleBudgetFeeSplit = (params: { export const getScaleBudgetFeeSplit = (params: {
benchmarkBudgetBasic: unknown benchmarkBudgetBasic: unknown
benchmarkBudgetOptional: unknown benchmarkBudgetOptional: unknown

View File

@ -675,8 +675,9 @@ export const getServiceDictItemById = (id: string | number): DictItem | undefine
} }
export type QuickCalcOption = { export type QuickCalcOption = {
key: string
label: string label: string
code: string | Record<string, string> id: string | Record<string, string>
} }
export type QuickCalcGroup = { export type QuickCalcGroup = {
@ -691,25 +692,27 @@ export type QuickCalcGroup = {
const getQuickDictLabel = (item: DictItem | undefined, fallback = '') => const getQuickDictLabel = (item: DictItem | undefined, fallback = '') =>
String(item?.quickLabel || item?.name || fallback) String(item?.quickLabel || item?.name || fallback)
const createQuickOptionByServiceCode = ( const createQuickOptionByServiceKey = (
code: string, key: string,
override?: Partial<QuickCalcOption> & { label?: string } override?: Partial<QuickCalcOption> & { label?: string }
): QuickCalcOption => { ): QuickCalcOption => {
const entry = getServiceDictEntries().find(item => String(item.item?.code || '') === code) const entry = getServiceDictEntries().find(item => String(item.id || '') === key)
return { return {
label: override?.label || getQuickDictLabel(entry?.item, code), key: override?.key || key,
code: override?.code || code label: override?.label || getQuickDictLabel(entry?.item, key),
id: override?.id || key
} }
} }
const createQuickOptionByMajorCode = ( const createQuickOptionByMajorKey = (
code: string, key: string,
override?: Partial<QuickCalcOption> & { label?: string } override?: Partial<QuickCalcOption> & { label?: string }
): QuickCalcOption => { ): QuickCalcOption => {
const entry = getMajorDictEntries().find(item => String(item.item?.code || '') === code) const entry = getMajorDictEntries().find(item => String(item.id || '') === key)
return { return {
label: override?.label || getQuickDictLabel(entry?.item, code), key: override?.key || key,
code: override?.code || code label: override?.label || getQuickDictLabel(entry?.item, key),
id: override?.id || key
} }
} }
@ -719,28 +722,42 @@ export const getQuickCalcGroups = (): QuickCalcGroup[] => [
label: '咨询类别(常用)', label: '咨询类别(常用)',
hint: '先选择咨询类别,再补规模和预算参数。', hint: '先选择咨询类别,再补规模和预算参数。',
items: [ items: [
createQuickOptionByServiceCode('D1'), createQuickOptionByServiceKey('0'),
createQuickOptionByServiceCode('D2-1'), createQuickOptionByServiceKey('2'),
createQuickOptionByServiceCode('D2-2-1', { code: { '0': 'D2-2-1', '1': 'D2-2-2', '2': 'D2-2-1' } }), createQuickOptionByServiceKey('3', {
createQuickOptionByServiceCode('D3-1'), key: 'D2-2',
createQuickOptionByServiceCode('D3-2'), id: {//行业不同显示的不同
createQuickOptionByServiceCode('D3-3'), '0': '3',
createQuickOptionByServiceCode('D3-4'), '1': '4',
createQuickOptionByServiceCode('D3-5'), '2': '3'
createQuickOptionByServiceCode('D3-6-1', { code: { '0': 'D3-6-1', '1': 'D3-6-2', '2': 'D3-6-1' } }), }
createQuickOptionByServiceCode('D3-7'), }),
createQuickOptionByServiceCode('D4-6'), createQuickOptionByServiceKey('6'),
createQuickOptionByServiceCode('D4-7'), createQuickOptionByServiceKey('7'),
createQuickOptionByServiceCode('D4-8'), createQuickOptionByServiceKey('8'),
createQuickOptionByServiceCode('D4-9'), createQuickOptionByServiceKey('9'),
createQuickOptionByServiceCode('D4-10'), createQuickOptionByServiceKey('10'),
createQuickOptionByServiceCode('D4-11'), createQuickOptionByServiceKey('11', {
createQuickOptionByServiceCode('D4-12') 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: [ 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: '通用专业', label: '通用专业',
hint: '跨行业共用的补偿与其他费用专业。', hint: '跨行业共用的补偿与其他费用专业。',
items: [ items: [
createQuickOptionByMajorCode('E1-1'), createQuickOptionByMajorKey('1'),
createQuickOptionByMajorCode('E1-2'), createQuickOptionByMajorKey('2'),
createQuickOptionByMajorCode('E1-3'), createQuickOptionByMajorKey('3'),
createQuickOptionByMajorCode('E1-4') createQuickOptionByMajorKey('4')
], ],
rows: [['征地(用海)补偿', '拆迁补偿', '迁改工程等费用', '工程建设其他费']] rows: [['1', '2', '3', '4']]
}, },
{ {
key: 'road', key: 'road',
@ -761,22 +778,22 @@ export const getQuickCalcGroups = (): QuickCalcGroup[] => [
hint: '首页行业为公路工程时默认展示。', hint: '首页行业为公路工程时默认展示。',
industryId: '0', industryId: '0',
items: [ items: [
createQuickOptionByMajorCode('E2'), createQuickOptionByMajorKey('7'),
createQuickOptionByMajorCode('E2-1'), createQuickOptionByMajorKey('8'),
createQuickOptionByMajorCode('E2-2'), createQuickOptionByMajorKey('9'),
createQuickOptionByMajorCode('E2-3'), createQuickOptionByMajorKey('10'),
createQuickOptionByMajorCode('E2-4'), createQuickOptionByMajorKey('11'),
createQuickOptionByMajorCode('E2-5'), createQuickOptionByMajorKey('12'),
createQuickOptionByMajorCode('E2-6'), createQuickOptionByMajorKey('13'),
createQuickOptionByMajorCode('E2-7'), createQuickOptionByMajorKey('14'),
createQuickOptionByMajorCode('E2-8'), createQuickOptionByMajorKey('15'),
createQuickOptionByMajorCode('E2-9'), createQuickOptionByMajorKey('16'),
createQuickOptionByMajorCode('E2-10') createQuickOptionByMajorKey('17')
], ],
rows: [ rows: [
['建设工程项目'], ['7'],
['临时工程', '路基工程', '路面工程', '桥涵工程', '隧道工程', '交叉工程'], ['8', '9', '10', '11', '12', '13'],
['机电工程', '交通安全设施工程', '绿化及环境保护工程', '房建工程'] ['14', '15', '16', '17']
] ]
}, },
{ {
@ -785,21 +802,21 @@ export const getQuickCalcGroups = (): QuickCalcGroup[] => [
hint: '首页行业为铁路工程时默认展示。', hint: '首页行业为铁路工程时默认展示。',
industryId: '1', industryId: '1',
items: [ items: [
createQuickOptionByMajorCode('E3'), createQuickOptionByMajorKey('18'),
createQuickOptionByMajorCode('E3-1'), createQuickOptionByMajorKey('19'),
createQuickOptionByMajorCode('E3-2'), createQuickOptionByMajorKey('20'),
createQuickOptionByMajorCode('E3-3'), createQuickOptionByMajorKey('21'),
createQuickOptionByMajorCode('E3-4'), createQuickOptionByMajorKey('22'),
createQuickOptionByMajorCode('E3-5'), createQuickOptionByMajorKey('23'),
createQuickOptionByMajorCode('E3-6'), createQuickOptionByMajorKey('24'),
createQuickOptionByMajorCode('E3-7'), createQuickOptionByMajorKey('25'),
createQuickOptionByMajorCode('E3-8'), createQuickOptionByMajorKey('26'),
createQuickOptionByMajorCode('E3-9') createQuickOptionByMajorKey('27')
], ],
rows: [ rows: [
['建设工程项目'], ['18'],
['大型临时设施和过渡工程', '路基工程', '桥涵工程', '隧道及明洞工程', '轨道工程'], ['19', '20', '21', '22', '23'],
['通信、信号、信息及灾害监测工程', '电力及电力牵引供电工程', '房建工程(房屋建筑及附属工程)', '装饰装修工程'] ['24', '25', '26', '27']
] ]
}, },
{ {
@ -808,17 +825,17 @@ export const getQuickCalcGroups = (): QuickCalcGroup[] => [
hint: '首页行业为水运工程时默认展示。', hint: '首页行业为水运工程时默认展示。',
industryId: '2', industryId: '2',
items: [ items: [
createQuickOptionByMajorCode('E4'), createQuickOptionByMajorKey('28'),
createQuickOptionByMajorCode('E4-1'), createQuickOptionByMajorKey('29'),
createQuickOptionByMajorCode('E4-2'), createQuickOptionByMajorKey('30'),
createQuickOptionByMajorCode('E4-3'), createQuickOptionByMajorKey('31'),
createQuickOptionByMajorCode('E4-4'), createQuickOptionByMajorKey('32'),
createQuickOptionByMajorCode('E4-5', { label: '房建工程(房屋建筑及附属工程)' }) createQuickOptionByMajorKey('33', { label: '房建工程(房屋建筑及附属工程)' })
], ],
rows: [ rows: [
['建设工程项目'], ['28'],
['临时工程', '土建工程'], ['29', '30'],
['机电与金属结构工程', '设备工程', '房建工程(房屋建筑及附属工程)'] ['31', '32', '33']
] ]
} }
] ]