优化目录

This commit is contained in:
zwgczx committed 2026-03-17 12:05:22 +08:00
1 parent 4e343cccf1
commit 2b18fe9991
46 files changed
+1061 -1308

No files matched your search

+332
View File
@@ -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>