优化目录
This commit is contained in:
1 parent
4e343cccf1
commit
2b18fe9991
46 files changed
+1061
-1308
No files matched your search
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import HtFeeMethodGrid from '@/components/shared/HtFeeMethodGrid.vue'
|
||||
import { additionalWorkList } from '@/sql'
|
||||
|
||||
const props = defineProps<{
|
||||
contractId: string
|
||||
contractName?: string
|
||||
}>()
|
||||
|
||||
const STORAGE_KEY = computed(() => `htExtraFee-${props.contractId}-additional-work`)
|
||||
const additionalWorkNames = computed(() =>
|
||||
additionalWorkList.map(item => String(item?.name || '').trim()).filter(Boolean)
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<HtFeeMethodGrid
|
||||
title="附加工作费"
|
||||
:storageKey="STORAGE_KEY"
|
||||
:contract-id="props.contractId"
|
||||
:contract-name="props.contractName"
|
||||
:fixed-names="additionalWorkNames"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,68 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onActivated, onMounted, ref } from 'vue'
|
||||
import { getServiceDictEntries, isIndustryEnabledByType, getIndustryTypeValue } from '@/sql'
|
||||
import XmFactorGrid from '@/components/shared/XmFactorGrid.vue'
|
||||
import { useKvStore } from '@/pinia/kv'
|
||||
|
||||
const props = defineProps<{
|
||||
contractId: string
|
||||
projectInfoKey?: string
|
||||
parentStorageKey?: string
|
||||
}>()
|
||||
|
||||
interface XmBaseInfoState {
|
||||
projectIndustry?: string
|
||||
}
|
||||
|
||||
type ServiceItem = {
|
||||
code: string
|
||||
name: string
|
||||
defCoe: number | null
|
||||
desc?: string | null
|
||||
notshowByzxflxs?: boolean
|
||||
}
|
||||
|
||||
const PROJECT_INFO_KEY = computed(() => props.projectInfoKey || 'xm-base-info-v1')
|
||||
const projectIndustry = ref('')
|
||||
const kvStore = useKvStore()
|
||||
|
||||
const loadProjectIndustry = async () => {
|
||||
try {
|
||||
const data = await kvStore.getItem<XmBaseInfoState>(PROJECT_INFO_KEY.value)
|
||||
projectIndustry.value =
|
||||
typeof data?.projectIndustry === 'string' ? data.projectIndustry.trim() : ''
|
||||
} catch (error) {
|
||||
console.error('loadProjectIndustry failed:', error)
|
||||
projectIndustry.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
const filteredServiceDict = computed<Record<string, ServiceItem>>(() => {
|
||||
const industry = projectIndustry.value
|
||||
if (!industry) return {}
|
||||
const entries = getServiceDictEntries()
|
||||
.filter(({ item }) => isIndustryEnabledByType(item, getIndustryTypeValue(industry)))
|
||||
.map(({ id, item }) => [id, item as ServiceItem] as const)
|
||||
return Object.fromEntries(entries)
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
void loadProjectIndustry()
|
||||
})
|
||||
|
||||
onActivated(() => {
|
||||
void loadProjectIndustry()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<XmFactorGrid
|
||||
title="咨询分类系数明细"
|
||||
:storage-key="`ht-consult-category-factor-v1-${props.contractId}`"
|
||||
:parent-storage-key="props.parentStorageKey || 'xm-consult-category-factor-v1'"
|
||||
:dict="filteredServiceDict"
|
||||
:disable-budget-edit-when-standard-null="true"
|
||||
:exclude-notshow-by-zxflxs="true"
|
||||
:init-budget-value-from-standard="true"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,232 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onActivated, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { parseNumberOrNull } from '@/lib/number'
|
||||
import { formatThousandsFlexible } from '@/lib/numberFormat'
|
||||
import { useZxFwPricingStore } from '@/pinia/zxFwPricing'
|
||||
|
||||
interface RateMethodState {
|
||||
rate: number | null
|
||||
budgetFee?: number | null
|
||||
remark: string
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
storageKey: string
|
||||
contractId?: string
|
||||
htMainStorageKey?: string
|
||||
htRowId?: string
|
||||
htMethodType?: 'rate-fee'
|
||||
}>()
|
||||
const zxFwPricingStore = useZxFwPricingStore()
|
||||
|
||||
const rate = ref<number | null>(null)
|
||||
const remark = ref('')
|
||||
const rateInput = ref('')
|
||||
const baseValue = ref<number | null>(null)
|
||||
const lastSavedSnapshot = ref('')
|
||||
const useHtMethodState = computed(
|
||||
() => Boolean(props.htMainStorageKey && props.htRowId && props.htMethodType)
|
||||
)
|
||||
const contractIdText = computed(() => {
|
||||
const contractId = String(props.contractId || '').trim()
|
||||
return contractId
|
||||
})
|
||||
const isReserveFee = computed(() => {
|
||||
const mainKey = String(props.htMainStorageKey || '').trim()
|
||||
if (mainKey) return mainKey.endsWith('-reserve')
|
||||
return String(props.storageKey || '').includes('-reserve')
|
||||
})
|
||||
const contractVersion = computed(() => {
|
||||
const contractId = contractIdText.value
|
||||
if (!contractId) return 0
|
||||
return zxFwPricingStore.contractVersions[contractId] || 0
|
||||
})
|
||||
const additionalWorkKeyVersion = computed(() => {
|
||||
if (!isReserveFee.value) return 0
|
||||
const contractId = contractIdText.value
|
||||
if (!contractId) return 0
|
||||
const additionalStorageKey = `htExtraFee-${contractId}-additional-work`
|
||||
return zxFwPricingStore.getKeyVersion(additionalStorageKey)
|
||||
})
|
||||
const baseLabel = computed(() =>
|
||||
isReserveFee.value ? '基数(咨询服务总计 + 附加工作费总计)' : '基数(所有服务费预算合计)'
|
||||
)
|
||||
|
||||
const budgetFee = computed<number | null>(() => {
|
||||
if (baseValue.value == null || rate.value == null) return null
|
||||
return Number((baseValue.value * rate.value).toFixed(3))
|
||||
})
|
||||
|
||||
const formatAmount = (value: number | null) =>
|
||||
value == null ? '' : formatThousandsFlexible(value, 3)
|
||||
|
||||
const ensureContractLoaded = async () => {
|
||||
const contractId = contractIdText.value
|
||||
if (!contractId) return
|
||||
try {
|
||||
await zxFwPricingStore.loadContract(contractId)
|
||||
const serviceBase = zxFwPricingStore.getBaseSubtotal(contractId)
|
||||
if (!isReserveFee.value) {
|
||||
baseValue.value = serviceBase == null ? null : Number(serviceBase.toFixed(3))
|
||||
return
|
||||
}
|
||||
const additionalStorageKey = `htExtraFee-${contractId}-additional-work`
|
||||
const additionalState = await zxFwPricingStore.loadHtFeeMainState<{
|
||||
rateFee?: unknown
|
||||
hourlyFee?: unknown
|
||||
quantityUnitPriceFee?: unknown
|
||||
}>(additionalStorageKey)
|
||||
const additionalTotal = (additionalState?.detailRows || []).reduce((total, row) => {
|
||||
const rateFee = Number(row?.rateFee)
|
||||
const hourlyFee = Number(row?.hourlyFee)
|
||||
const quantityFee = Number(row?.quantityUnitPriceFee)
|
||||
const safeRateFee = Number.isFinite(rateFee) ? rateFee : 0
|
||||
const safeHourlyFee = Number.isFinite(hourlyFee) ? hourlyFee : 0
|
||||
const safeQuantityFee = Number.isFinite(quantityFee) ? quantityFee : 0
|
||||
return total + safeRateFee + safeHourlyFee + safeQuantityFee
|
||||
}, 0)
|
||||
const serviceBaseSafe = typeof serviceBase === 'number' && Number.isFinite(serviceBase) ? serviceBase : 0
|
||||
const hasAny = (serviceBase != null) || additionalTotal !== 0
|
||||
baseValue.value = hasAny ? Number((serviceBaseSafe + additionalTotal).toFixed(3)) : null
|
||||
} catch (error) {
|
||||
console.error('load contract for rate base failed:', error)
|
||||
baseValue.value = null
|
||||
}
|
||||
}
|
||||
|
||||
const loadForm = async () => {
|
||||
try {
|
||||
const data = useHtMethodState.value
|
||||
? await zxFwPricingStore.loadHtFeeMethodState<RateMethodState>(
|
||||
props.htMainStorageKey!,
|
||||
props.htRowId!,
|
||||
props.htMethodType!
|
||||
)
|
||||
: await zxFwPricingStore.loadKeyState<RateMethodState>(props.storageKey)
|
||||
rate.value = typeof data?.rate === 'number' ? data.rate : null
|
||||
remark.value = typeof data?.remark === 'string' ? data.remark : ''
|
||||
rateInput.value = rate.value == null ? '' : String(rate.value)
|
||||
const snapshot: RateMethodState = {
|
||||
rate: rate.value,
|
||||
budgetFee: budgetFee.value,
|
||||
remark: remark.value
|
||||
}
|
||||
lastSavedSnapshot.value = JSON.stringify(snapshot)
|
||||
} catch (error) {
|
||||
console.error('load rate form failed:', error)
|
||||
rate.value = null
|
||||
remark.value = ''
|
||||
rateInput.value = ''
|
||||
lastSavedSnapshot.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
const saveForm = async (force = false) => {
|
||||
try {
|
||||
const payload: RateMethodState = {
|
||||
rate: rate.value,
|
||||
budgetFee: budgetFee.value,
|
||||
remark: remark.value
|
||||
}
|
||||
const snapshot = JSON.stringify(payload)
|
||||
if (!force && snapshot === lastSavedSnapshot.value) return
|
||||
if (useHtMethodState.value) {
|
||||
zxFwPricingStore.setHtFeeMethodState(
|
||||
props.htMainStorageKey!,
|
||||
props.htRowId!,
|
||||
props.htMethodType!,
|
||||
payload,
|
||||
{ force }
|
||||
)
|
||||
} else {
|
||||
zxFwPricingStore.setKeyState(props.storageKey, payload, { force })
|
||||
}
|
||||
lastSavedSnapshot.value = snapshot
|
||||
} catch (error) {
|
||||
console.error('save rate form failed:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const applyRateInput = () => {
|
||||
const next = parseNumberOrNull(rateInput.value, { sanitize: true, precision: 2 })
|
||||
rate.value = next
|
||||
rateInput.value = next == null ? '' : String(next)
|
||||
}
|
||||
|
||||
|
||||
|
||||
watch([rate, remark, budgetFee], () => {
|
||||
void saveForm()
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.storageKey,
|
||||
() => {
|
||||
void loadForm()
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => contractIdText.value,
|
||||
() => {
|
||||
void ensureContractLoaded()
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => [props.storageKey, props.htMainStorageKey, props.htRowId, props.htMethodType],
|
||||
() => {
|
||||
void ensureContractLoaded()
|
||||
}
|
||||
)
|
||||
|
||||
watch([contractVersion, additionalWorkKeyVersion], ([nextContract, nextAdditional], [prevContract, prevAdditional]) => {
|
||||
if (nextContract === prevContract && nextAdditional === prevAdditional) return
|
||||
void ensureContractLoaded()
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
await Promise.all([ensureContractLoaded(), loadForm()])
|
||||
})
|
||||
|
||||
onActivated(async () => {
|
||||
await Promise.all([ensureContractLoaded(), loadForm()])
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
void saveForm(true)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-full min-h-0 flex flex-col">
|
||||
<div class="rounded-lg border bg-card p-4">
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<label class="space-y-1.5">
|
||||
<div class="text-xs text-muted-foreground">{{ baseLabel }}</div>
|
||||
<input type="text" :value="baseValue" readonly disabled tabindex="-1"
|
||||
class="h-9 w-full cursor-not-allowed rounded-md border bg-muted/40 px-3 text-sm text-foreground select-none" />
|
||||
</label>
|
||||
|
||||
<label class="space-y-1.5">
|
||||
<div class="text-xs text-muted-foreground">费率(%)</div>
|
||||
<input v-model="rateInput" type="text" inputmode="decimal" placeholder="请输入费率,建议1 ~ 5"
|
||||
class="h-9 w-full rounded-md border bg-background px-3 text-sm text-foreground outline-none focus:ring-2 focus:ring-primary/30"
|
||||
@blur="applyRateInput" />
|
||||
</label>
|
||||
|
||||
<label class="space-y-1.5">
|
||||
<div class="text-xs text-muted-foreground">预算费用(自动计算)</div>
|
||||
<input type="text" :value="formatAmount(budgetFee)" readonly disabled tabindex="-1"
|
||||
class="h-9 w-full cursor-not-allowed rounded-md border bg-muted/40 px-3 text-sm text-foreground select-none" />
|
||||
</label>
|
||||
|
||||
<label class="space-y-1.5 md:col-span-2">
|
||||
<div class="text-xs text-muted-foreground">说明</div>
|
||||
<textarea v-model="remark" rows="4" placeholder="请输入说明"
|
||||
class="w-full rounded-md border bg-background px-3 py-2 text-sm text-foreground outline-none focus:ring-2 focus:ring-primary/30" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,68 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onActivated, onMounted, ref } from 'vue'
|
||||
import { getMajorDictEntries, isMajorIdInIndustryScope } from '@/sql'
|
||||
import XmFactorGrid from '@/components/shared/XmFactorGrid.vue'
|
||||
import { useKvStore } from '@/pinia/kv'
|
||||
|
||||
interface XmBaseInfoState {
|
||||
projectIndustry?: string
|
||||
}
|
||||
|
||||
type MajorItem = {
|
||||
code: string
|
||||
name: string
|
||||
defCoe: number | null
|
||||
desc?: string | null
|
||||
notshowByzxflxs?: boolean
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
contractId: string
|
||||
projectInfoKey?: string
|
||||
parentStorageKey?: string
|
||||
}>()
|
||||
|
||||
const PROJECT_INFO_KEY = computed(() => props.projectInfoKey || 'xm-base-info-v1')
|
||||
const projectIndustry = ref('')
|
||||
const kvStore = useKvStore()
|
||||
|
||||
const loadProjectIndustry = async () => {
|
||||
try {
|
||||
const data = await kvStore.getItem<XmBaseInfoState>(PROJECT_INFO_KEY.value)
|
||||
projectIndustry.value =
|
||||
typeof data?.projectIndustry === 'string' ? data.projectIndustry.trim() : ''
|
||||
} catch (error) {
|
||||
console.error('loadProjectIndustry failed:', error)
|
||||
projectIndustry.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
const filteredMajorDict = computed<Record<string, MajorItem>>(() => {
|
||||
const industry = projectIndustry.value
|
||||
if (!industry) return {}
|
||||
const entries = getMajorDictEntries()
|
||||
.filter(({ id }) => isMajorIdInIndustryScope(id, industry))
|
||||
.map(({ id, item }) => [id, item as MajorItem] as const)
|
||||
return Object.fromEntries(entries)
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
void loadProjectIndustry()
|
||||
})
|
||||
|
||||
onActivated(() => {
|
||||
void loadProjectIndustry()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<XmFactorGrid
|
||||
title="工程专业系数明细"
|
||||
:storage-key="`ht-major-factor-v1-${props.contractId}`"
|
||||
:parent-storage-key="props.parentStorageKey || 'xm-major-factor-v1'"
|
||||
:dict="filteredMajorDict"
|
||||
:disable-budget-edit-when-standard-null="true"
|
||||
:exclude-notshow-by-zxflxs="true"
|
||||
:init-budget-value-from-standard="true"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import HtFeeMethodGrid from '@/components/shared/HtFeeMethodGrid.vue'
|
||||
import { reserveList } from '@/sql'
|
||||
|
||||
const props = defineProps<{
|
||||
contractId: string
|
||||
contractName?: string
|
||||
}>()
|
||||
|
||||
const STORAGE_KEY = computed(() => `htExtraFee-${props.contractId}-reserve`)
|
||||
const reserveFeeNames = computed(() =>
|
||||
reserveList.map(item => String(item?.name || '').trim()).filter(Boolean)
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<HtFeeMethodGrid
|
||||
title="预备费"
|
||||
:storageKey="STORAGE_KEY"
|
||||
:contract-id="props.contractId"
|
||||
:contract-name="props.contractName"
|
||||
:fixed-names="reserveFeeNames"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,332 @@
|
||||
<template>
|
||||
<!-- 修复模板字符串语法(反引号需用 v-bind 或模板插值)+ 补充属性格式 -->
|
||||
<TypeLine
|
||||
scene="ht-tab"
|
||||
:title="`合同段:${contractName}`"
|
||||
:subtitle="`合同段ID:${contractId}`"
|
||||
:meta-text="`合同段预算金额:${formatBudgetAmount(contractBudget)}`"
|
||||
:copy-text="contractId"
|
||||
:storage-key="`project-active-cat-${contractId}`"
|
||||
default-category="info"
|
||||
:categories="xmCategories"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, markRaw, defineAsyncComponent, defineComponent, h, onActivated, onBeforeUnmount, onMounted, ref, watch, type Component } from 'vue';
|
||||
import TypeLine from '@/layout/typeLine.vue';
|
||||
import { useZxFwPricingStore } from '@/pinia/zxFwPricing'
|
||||
import { roundTo } from '@/lib/decimal'
|
||||
import { formatThousands } from '@/lib/numberFormat'
|
||||
|
||||
// 1. 完善 Props 类型 + 添加校验(可选但推荐)
|
||||
const props = defineProps<{
|
||||
contractId: string; // 合同ID(必传)
|
||||
contractName: string; // 合同名称(必传)
|
||||
projectInfoKey?: string; // 工作区基础信息键
|
||||
projectScaleKey?: string | null; // 工作区规模信息键
|
||||
projectConsultCategoryFactorKey?: string; // 工作区咨询分类系数键
|
||||
projectMajorFactorKey?: string; // 工作区工程专业系数键
|
||||
}>();
|
||||
const zxFwPricingStore = useZxFwPricingStore()
|
||||
|
||||
interface HtFeeMainRowLike {
|
||||
id?: unknown
|
||||
}
|
||||
|
||||
interface RateMethodStateLike {
|
||||
budgetFee?: unknown
|
||||
}
|
||||
|
||||
interface HourlyMethodRowLike {
|
||||
serviceBudget?: unknown
|
||||
adoptedBudgetUnitPrice?: unknown
|
||||
personnelCount?: unknown
|
||||
workdayCount?: unknown
|
||||
}
|
||||
|
||||
interface HourlyMethodStateLike {
|
||||
detailRows?: HourlyMethodRowLike[]
|
||||
}
|
||||
|
||||
interface QuantityMethodRowLike {
|
||||
id?: unknown
|
||||
budgetFee?: unknown
|
||||
quantity?: unknown
|
||||
unitPrice?: unknown
|
||||
}
|
||||
|
||||
interface QuantityMethodStateLike {
|
||||
detailRows?: QuantityMethodRowLike[]
|
||||
}
|
||||
|
||||
const contractBudget = ref<number | null>(null)
|
||||
let budgetRefreshTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
const toFiniteNumber = (value: unknown): number | null => {
|
||||
if (value == null || value === '') return null
|
||||
const num = Number(value)
|
||||
return Number.isFinite(num) ? num : null
|
||||
}
|
||||
|
||||
const formatBudgetAmount = (value: number | null | undefined) =>
|
||||
typeof value === 'number' && Number.isFinite(value) ? `${formatThousands(value, 2)} 元` : '--'
|
||||
|
||||
const sumHourlyMethodFee = (state: HourlyMethodStateLike | null): number | null => {
|
||||
const rows = Array.isArray(state?.detailRows) ? state.detailRows : []
|
||||
if (rows.length === 0) return null
|
||||
let hasValid = false
|
||||
let total = 0
|
||||
for (const row of rows) {
|
||||
const serviceBudget = toFiniteNumber(row?.serviceBudget)
|
||||
if (serviceBudget != null) {
|
||||
total += serviceBudget
|
||||
hasValid = true
|
||||
continue
|
||||
}
|
||||
const adopted = toFiniteNumber(row?.adoptedBudgetUnitPrice)
|
||||
const personnel = toFiniteNumber(row?.personnelCount)
|
||||
const workday = toFiniteNumber(row?.workdayCount)
|
||||
if (adopted == null || personnel == null || workday == null) continue
|
||||
total += adopted * personnel * workday
|
||||
hasValid = true
|
||||
}
|
||||
return hasValid ? roundTo(total, 2) : null
|
||||
}
|
||||
|
||||
const sumQuantityMethodFee = (state: QuantityMethodStateLike | null): number | null => {
|
||||
const rows = Array.isArray(state?.detailRows) ? state.detailRows : []
|
||||
if (rows.length === 0) return null
|
||||
const subtotalRow = rows.find(row => String(row?.id || '') === 'fee-subtotal-fixed')
|
||||
const subtotal = toFiniteNumber(subtotalRow?.budgetFee)
|
||||
if (subtotal != null) return roundTo(subtotal, 2)
|
||||
let hasValid = false
|
||||
let total = 0
|
||||
for (const row of rows) {
|
||||
if (String(row?.id || '') === 'fee-subtotal-fixed') continue
|
||||
const budget = toFiniteNumber(row?.budgetFee)
|
||||
if (budget != null) {
|
||||
total += budget
|
||||
hasValid = true
|
||||
continue
|
||||
}
|
||||
const quantity = toFiniteNumber(row?.quantity)
|
||||
const unitPrice = toFiniteNumber(row?.unitPrice)
|
||||
if (quantity == null || unitPrice == null) continue
|
||||
total += quantity * unitPrice
|
||||
hasValid = true
|
||||
}
|
||||
return hasValid ? roundTo(total, 2) : null
|
||||
}
|
||||
|
||||
const loadHtMethodTotalByRow = async (mainStorageKey: string, rowId: string) => {
|
||||
const [rateState, hourlyState, quantityState] = await Promise.all([
|
||||
zxFwPricingStore.loadHtFeeMethodState<RateMethodStateLike>(mainStorageKey, rowId, 'rate-fee'),
|
||||
zxFwPricingStore.loadHtFeeMethodState<HourlyMethodStateLike>(mainStorageKey, rowId, 'hourly-fee'),
|
||||
zxFwPricingStore.loadHtFeeMethodState<QuantityMethodStateLike>(mainStorageKey, rowId, 'quantity-unit-price-fee')
|
||||
])
|
||||
const parts = [
|
||||
toFiniteNumber(rateState?.budgetFee),
|
||||
sumHourlyMethodFee(hourlyState),
|
||||
sumQuantityMethodFee(quantityState)
|
||||
]
|
||||
const validParts = parts.filter((item): item is number => typeof item === 'number' && Number.isFinite(item))
|
||||
if (validParts.length === 0) return null
|
||||
return roundTo(validParts.reduce((sum, value) => sum + value, 0), 2)
|
||||
}
|
||||
|
||||
const loadHtMainTotalFee = async (mainStorageKey: string) => {
|
||||
const mainState = await zxFwPricingStore.loadHtFeeMainState<HtFeeMainRowLike>(mainStorageKey)
|
||||
const rows = Array.isArray(mainState?.detailRows) ? mainState.detailRows : []
|
||||
const rowIds = rows.map(row => String(row?.id || '').trim()).filter(Boolean)
|
||||
if (rowIds.length === 0) return null
|
||||
const rowTotals = await Promise.all(rowIds.map(rowId => loadHtMethodTotalByRow(mainStorageKey, rowId)))
|
||||
const validTotals = rowTotals.filter((item): item is number => typeof item === 'number' && Number.isFinite(item))
|
||||
if (validTotals.length === 0) return null
|
||||
return roundTo(validTotals.reduce((sum, value) => sum + value, 0), 2)
|
||||
}
|
||||
|
||||
const refreshContractBudget = async () => {
|
||||
await zxFwPricingStore.loadContract(props.contractId)
|
||||
const serviceFee = zxFwPricingStore.getBaseSubtotal(props.contractId)
|
||||
const [additionalFee, reserveFee] = await Promise.all([
|
||||
loadHtMainTotalFee(`htExtraFee-${props.contractId}-additional-work`),
|
||||
loadHtMainTotalFee(`htExtraFee-${props.contractId}-reserve`)
|
||||
])
|
||||
const parts = [serviceFee, additionalFee, reserveFee]
|
||||
const validParts = parts.filter((item): item is number => typeof item === 'number' && Number.isFinite(item))
|
||||
contractBudget.value = validParts.length === 0 ? null : roundTo(validParts.reduce((sum, value) => sum + value, 0), 2)
|
||||
}
|
||||
|
||||
const budgetRefreshSignature = computed(() => {
|
||||
const contractVersion = zxFwPricingStore.contractVersions[props.contractId] || 0
|
||||
const additionalMainKey = `htExtraFee-${props.contractId}-additional-work`
|
||||
const reserveMainKey = `htExtraFee-${props.contractId}-reserve`
|
||||
const keyVersionEntries = Object.entries(zxFwPricingStore.keyVersions)
|
||||
const methodKeySig = keyVersionEntries
|
||||
.filter(([key]) => key.startsWith(`${additionalMainKey}-`) || key.startsWith(`${reserveMainKey}-`))
|
||||
.sort((a, b) => a[0].localeCompare(b[0]))
|
||||
.map(([key, version]) => `${key}:${version}`)
|
||||
.join(',')
|
||||
return `${contractVersion}:${zxFwPricingStore.getKeyVersion(additionalMainKey)}:${zxFwPricingStore.getKeyVersion(reserveMainKey)}:${methodKeySig}`
|
||||
})
|
||||
|
||||
const scheduleRefreshContractBudget = () => {
|
||||
if (budgetRefreshTimer) clearTimeout(budgetRefreshTimer)
|
||||
budgetRefreshTimer = setTimeout(() => {
|
||||
void refreshContractBudget()
|
||||
}, 80)
|
||||
}
|
||||
|
||||
// 2. 定义分类项的 TS 类型(核心:明确 categories 结构)
|
||||
interface XmCategoryItem {
|
||||
key:
|
||||
| 'info'
|
||||
| 'consult-category-factor'
|
||||
| 'major-factor'
|
||||
| 'work-grid'
|
||||
| 'contract'
|
||||
| 'additional-work-fee'
|
||||
| 'reserve-fee';
|
||||
label: string;
|
||||
component: Component; // 标记为 Vue 组件类型
|
||||
}
|
||||
|
||||
// 3. 优化异步组件导入(添加加载失败兜底 + 类型标注)
|
||||
const htView = markRaw(
|
||||
defineComponent({
|
||||
name: 'HtInfoWithProps',
|
||||
setup() {
|
||||
const AsyncHtInfo = defineAsyncComponent({
|
||||
loader: () => import('@/components/ht/htInfo.vue'),
|
||||
onError: (err) => {
|
||||
console.error('加载 htInfo 组件失败:', err);
|
||||
}
|
||||
});
|
||||
return () => h(AsyncHtInfo, {
|
||||
contractId: props.contractId,
|
||||
projectScaleKey: props.projectScaleKey,
|
||||
projectInfoKey: props.projectInfoKey
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
const zxfwView = markRaw(
|
||||
defineComponent({
|
||||
name: 'ZxFwWithProps',
|
||||
setup() {
|
||||
const AsyncZxFw = defineAsyncComponent({
|
||||
loader: () => import('@/components/ht/zxFw.vue'),
|
||||
onError: (err) => {
|
||||
console.error('加载 zxFw 组件失败:', err);
|
||||
}
|
||||
});
|
||||
return () => h(AsyncZxFw, {
|
||||
contractId: props.contractId,
|
||||
contractName: props.contractName,
|
||||
projectInfoKey: props.projectInfoKey
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
const consultCategoryFactorView = markRaw(
|
||||
defineComponent({
|
||||
name: 'HtConsultCategoryFactorWithProps',
|
||||
setup() {
|
||||
const AsyncHtConsultCategoryFactor = defineAsyncComponent({
|
||||
loader: () => import('@/components/ht/HtConsultCategoryFactor.vue'),
|
||||
onError: (err) => {
|
||||
console.error('加载 HtConsultCategoryFactor 组件失败:', err);
|
||||
}
|
||||
});
|
||||
return () => h(AsyncHtConsultCategoryFactor, {
|
||||
contractId: props.contractId,
|
||||
projectInfoKey: props.projectInfoKey,
|
||||
parentStorageKey: props.projectConsultCategoryFactorKey
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
const majorFactorView = markRaw(
|
||||
defineComponent({
|
||||
name: 'HtMajorFactorWithProps',
|
||||
setup() {
|
||||
const AsyncHtMajorFactor = defineAsyncComponent({
|
||||
loader: () => import('@/components/ht/HtMajorFactor.vue'),
|
||||
onError: (err) => {
|
||||
console.error('加载 HtMajorFactor 组件失败:', err);
|
||||
}
|
||||
});
|
||||
return () => h(AsyncHtMajorFactor, {
|
||||
contractId: props.contractId,
|
||||
projectInfoKey: props.projectInfoKey,
|
||||
parentStorageKey: props.projectMajorFactorKey
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
|
||||
const additionalWorkFeeView = markRaw(
|
||||
defineComponent({
|
||||
name: 'HtAdditionalWorkFeeWithProps',
|
||||
setup() {
|
||||
const AsyncHtAdditionalWorkFee = defineAsyncComponent({
|
||||
loader: () => import('@/components/ht/HtAdditionalWorkFee.vue'),
|
||||
onError: (err) => {
|
||||
console.error('加载 HtAdditionalWorkFee 组件失败:', err);
|
||||
}
|
||||
});
|
||||
return () => h(AsyncHtAdditionalWorkFee, { contractId: props.contractId, contractName: props.contractName });
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
const reserveFeeView = markRaw(
|
||||
defineComponent({
|
||||
name: 'HtReserveFeeWithProps',
|
||||
setup() {
|
||||
const AsyncHtReserveFee = defineAsyncComponent({
|
||||
loader: () => import('@/components/ht/HtReserveFee.vue'),
|
||||
onError: (err) => {
|
||||
console.error('加载 HtReserveFee 组件失败:', err);
|
||||
}
|
||||
});
|
||||
return () => h(AsyncHtReserveFee, { contractId: props.contractId, contractName: props.contractName });
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// 4. 给分类数组添加严格类型标注
|
||||
const xmCategories: XmCategoryItem[] = [
|
||||
{ key: 'info', label: '规模信息', component: htView },
|
||||
{ key: 'consult-category-factor', label: '咨询分类系数', component: consultCategoryFactorView },
|
||||
{ key: 'major-factor', label: '工程专业系数', component: majorFactorView },
|
||||
{ key: 'contract', label: '咨询服务', component: zxfwView },
|
||||
{ key: 'additional-work-fee', label: '附加工作费', component: additionalWorkFeeView },
|
||||
{ key: 'reserve-fee', label: '预备费', component: reserveFeeView },
|
||||
|
||||
|
||||
];
|
||||
|
||||
watch(budgetRefreshSignature, (next, prev) => {
|
||||
if (next === prev) return
|
||||
scheduleRefreshContractBudget()
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
void refreshContractBudget()
|
||||
})
|
||||
|
||||
onActivated(() => {
|
||||
void refreshContractBudget()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (budgetRefreshTimer) clearTimeout(budgetRefreshTimer)
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import CommonAgGrid from '@/components/shared/xmCommonAgGrid.vue'
|
||||
|
||||
|
||||
const props = defineProps<{
|
||||
contractId: string
|
||||
projectScaleKey?: string | null
|
||||
projectInfoKey?: string
|
||||
}>()
|
||||
const DB_KEY = computed(() => `ht-info-v3-${props.contractId}`)
|
||||
const XM_DB_KEY = computed(() => {
|
||||
if (props.projectScaleKey === null) return undefined
|
||||
return props.projectScaleKey || 'xm-info-v3'
|
||||
})
|
||||
const BASE_INFO_KEY = computed(() => props.projectInfoKey || 'xm-base-info-v1')
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CommonAgGrid title="合同规模明细" :dbKey="DB_KEY" :xmInfoKey="XM_DB_KEY" :base-info-key="BASE_INFO_KEY"/>
|
||||
</template>
|
||||
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user