fix
This commit is contained in:
1 parent
f4f6e5c618
commit
1c600e64d0
8 files changed
+549
-179
No files matched your search
@@ -93,6 +93,7 @@ const isSummaryRow = (row: FeeMethodRow | null | undefined) => row?.id === SUMMA
|
||||
const toFinite = (value: number | null | undefined) =>
|
||||
typeof value === 'number' && Number.isFinite(value) ? value : 0
|
||||
const round3 = (value: number) => Number(value.toFixed(3))
|
||||
const isReserveStorageKey = (key: string) => String(key || '').includes('-reserve')
|
||||
const sumNullableField = (rows: FeeMethodRow[], pick: (row: FeeMethodRow) => number | null | undefined): number | null => {
|
||||
let hasValid = false
|
||||
let total = 0
|
||||
@@ -115,13 +116,36 @@ const toFiniteUnknown = (value: unknown): number | null => {
|
||||
const numeric = Number(value)
|
||||
return Number.isFinite(numeric) ? numeric : null
|
||||
}
|
||||
|
||||
const sumMainStateSubtotal = (rows: FeeMethodRow[] | undefined) => {
|
||||
if (!Array.isArray(rows) || rows.length === 0) return null
|
||||
return sumNullableField(rows, row => getRowSubtotal(row))
|
||||
}
|
||||
|
||||
const loadAdditionalWorkFeeTotal = async (contractId: string): Promise<number | null> => {
|
||||
try {
|
||||
const additionalStorageKey = `htExtraFee-${contractId}-additional-work`
|
||||
const additionalState = await zxFwPricingStore.loadHtFeeMainState<FeeMethodRow>(additionalStorageKey)
|
||||
return sumMainStateSubtotal(additionalState?.detailRows)
|
||||
} catch (error) {
|
||||
console.error('loadAdditionalWorkFeeTotal failed:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const loadContractServiceFeeBase = async (): Promise<number | null> => {
|
||||
const contractId = String(props.contractId || '').trim()
|
||||
if (!contractId) return null
|
||||
try {
|
||||
await zxFwPricingStore.loadContract(contractId)
|
||||
const base = zxFwPricingStore.getBaseSubtotal(contractId)
|
||||
return base == null ? null : round3(base)
|
||||
const serviceBase = zxFwPricingStore.getBaseSubtotal(contractId)
|
||||
if (!isReserveStorageKey(props.storageKey)) {
|
||||
return serviceBase == null ? null : round3(serviceBase)
|
||||
}
|
||||
const additionalFeeTotal = await loadAdditionalWorkFeeTotal(contractId)
|
||||
const hasAnyBase = serviceBase != null || additionalFeeTotal != null
|
||||
if (!hasAnyBase) return null
|
||||
return round3(toFinite(serviceBase) + toFinite(additionalFeeTotal))
|
||||
} catch (error) {
|
||||
console.error('loadContractServiceFeeBase failed:', error)
|
||||
return null
|
||||
@@ -595,6 +619,13 @@ onActivated(() => {
|
||||
})
|
||||
|
||||
const storageKeyRef = computed(() => props.storageKey)
|
||||
const reserveAdditionalWorkKeyVersion = computed(() => {
|
||||
if (!isReserveStorageKey(props.storageKey)) return 0
|
||||
const contractId = String(props.contractId || '').trim()
|
||||
if (!contractId) return 0
|
||||
const additionalStorageKey = `htExtraFee-${contractId}-additional-work`
|
||||
return zxFwPricingStore.getKeyVersion(additionalStorageKey)
|
||||
})
|
||||
watch(storageKeyRef, () => {
|
||||
scheduleReloadFromStorage()
|
||||
})
|
||||
@@ -628,6 +659,14 @@ watch(
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
reserveAdditionalWorkKeyVersion,
|
||||
(nextVersion, prevVersion) => {
|
||||
if (nextVersion === prevVersion) return
|
||||
scheduleReloadFromStorage()
|
||||
}
|
||||
)
|
||||
|
||||
watch([hasFixedNames], () => {
|
||||
if (!detailGridOptions.context) return
|
||||
detailGridOptions.context.fixedNames = hasFixedNames.value
|
||||
|
||||
+225
-2
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onActivated, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { computed, nextTick, onActivated, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import draggable from 'vuedraggable'
|
||||
import { Card, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
@@ -11,6 +11,8 @@ import { useKvStore } from '@/pinia/kv'
|
||||
import { ArrowUp, Edit3, GripVertical, MoreHorizontal, Plus, Trash2, X } from 'lucide-vue-next'
|
||||
import { decodeZwArchive, encodeZwArchive } from '@/lib/zwArchive'
|
||||
import { industryTypeList } from '@/sql'
|
||||
import { roundTo } from '@/lib/decimal'
|
||||
import { formatThousands } from '@/lib/numberFormat'
|
||||
import {
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
@@ -65,6 +67,36 @@ interface XmScaleState {
|
||||
totalAmount?: number | null
|
||||
}
|
||||
|
||||
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 STORAGE_KEY = 'ht-card-v1'
|
||||
const CONTRACT_SEGMENT_FILE_EXTENSION = '.htzw'
|
||||
const CONTRACT_SEGMENT_VERSION = 2
|
||||
@@ -112,10 +144,14 @@ const showScrollTopFab = ref(false)
|
||||
const isDraggingContracts = ref(false)
|
||||
const cardMotionState = ref<'enter' | 'ready'>('ready')
|
||||
const canManageContracts = ref(true)
|
||||
const contractBudgetById = ref<Record<string, number | null>>({})
|
||||
const contractBudgetLoading = ref(false)
|
||||
let contractBudgetRefreshSeq = 0
|
||||
let contractAutoScrollRaf = 0
|
||||
let dragPointerClientY: number | null = null
|
||||
let cardEnterTimer: ReturnType<typeof setTimeout> | null = null
|
||||
let contractListScrollBoundEl: HTMLElement | null = null
|
||||
let budgetRefreshTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
const CARD_ENTER_STEP_MS = 58
|
||||
const CARD_ENTER_DURATION_MS = 560
|
||||
@@ -140,6 +176,36 @@ const filteredContracts = computed(() => {
|
||||
})
|
||||
})
|
||||
const isSearchingContracts = computed(() => Boolean(normalizedSearchKeyword.value))
|
||||
const projectTotalBudget = computed(() => {
|
||||
let hasValid = false
|
||||
let total = 0
|
||||
for (const contract of contracts.value) {
|
||||
const fee = contractBudgetById.value[contract.id]
|
||||
if (typeof fee !== 'number' || !Number.isFinite(fee)) continue
|
||||
total += fee
|
||||
hasValid = true
|
||||
}
|
||||
return hasValid ? roundTo(total, 2) : null
|
||||
})
|
||||
const budgetRefreshSignature = computed(() => {
|
||||
const ids = contracts.value.map(item => String(item.id || '').trim()).filter(Boolean)
|
||||
if (ids.length === 0) return ''
|
||||
const keyVersionEntries = Object.entries(zxFwPricingStore.keyVersions)
|
||||
return ids
|
||||
.map(id => {
|
||||
const contractVersion = zxFwPricingStore.contractVersions[id] || 0
|
||||
const additionalMainKey = `htExtraFee-${id}-additional-work`
|
||||
const reserveMainKey = `htExtraFee-${id}-reserve`
|
||||
const mainKeySig = `${zxFwPricingStore.getKeyVersion(additionalMainKey)}:${zxFwPricingStore.getKeyVersion(reserveMainKey)}`
|
||||
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 `${id}:${contractVersion}:${mainKeySig}:${methodKeySig}`
|
||||
})
|
||||
.join('|')
|
||||
})
|
||||
|
||||
const normalizeOrder = (list: ContractItem[]): ContractItem[] =>
|
||||
list.map((item, index) => ({
|
||||
@@ -207,6 +273,138 @@ const getCurrentProjectIndustry = async (): Promise<string> => {
|
||||
return typeof data?.projectIndustry === 'string' ? data.projectIndustry.trim() : ''
|
||||
}
|
||||
|
||||
const toFiniteNumber = (value: unknown): number | 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 loadContractBudgetFee = async (contractId: string) => {
|
||||
await zxFwPricingStore.loadContract(contractId)
|
||||
const serviceFee = zxFwPricingStore.getBaseSubtotal(contractId)
|
||||
const [additionalFee, reserveFee] = await Promise.all([
|
||||
loadHtMainTotalFee(`htExtraFee-${contractId}-additional-work`),
|
||||
loadHtMainTotalFee(`htExtraFee-${contractId}-reserve`)
|
||||
])
|
||||
const parts = [serviceFee, additionalFee, reserveFee]
|
||||
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 refreshContractBudgets = async () => {
|
||||
const seq = ++contractBudgetRefreshSeq
|
||||
const ids = contracts.value.map(item => String(item.id || '').trim()).filter(Boolean)
|
||||
if (ids.length === 0) {
|
||||
contractBudgetById.value = {}
|
||||
contractBudgetLoading.value = false
|
||||
return
|
||||
}
|
||||
contractBudgetLoading.value = true
|
||||
try {
|
||||
const nextPairs = await Promise.all(
|
||||
ids.map(async id => {
|
||||
const fee = await loadContractBudgetFee(id)
|
||||
return [id, fee] as const
|
||||
})
|
||||
)
|
||||
if (seq !== contractBudgetRefreshSeq) return
|
||||
contractBudgetById.value = Object.fromEntries(nextPairs)
|
||||
} catch (error) {
|
||||
console.error('refresh contract budgets failed:', error)
|
||||
} finally {
|
||||
if (seq === contractBudgetRefreshSeq) {
|
||||
contractBudgetLoading.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const scheduleRefreshContractBudgets = () => {
|
||||
if (budgetRefreshTimer) clearTimeout(budgetRefreshTimer)
|
||||
budgetRefreshTimer = setTimeout(() => {
|
||||
void refreshContractBudgets()
|
||||
}, 80)
|
||||
}
|
||||
|
||||
const formatExportTimestamp = (date: Date): string => {
|
||||
const yyyy = date.getFullYear()
|
||||
const mm = String(date.getMonth() + 1).padStart(2, '0')
|
||||
@@ -695,6 +893,7 @@ const importContractSegments = async (event: Event) => {
|
||||
|
||||
contracts.value = [...contracts.value, ...nextContracts]
|
||||
await saveContracts()
|
||||
await refreshContractBudgets()
|
||||
notify(`导入成功(${nextContracts.length} 个合同段)`)
|
||||
await nextTick()
|
||||
scrollContractsToBottom()
|
||||
@@ -829,6 +1028,7 @@ const createContract = async () => {
|
||||
} catch (error) {
|
||||
console.error('initialize contract scale failed:', error)
|
||||
}
|
||||
await refreshContractBudgets()
|
||||
notify('新建成功')
|
||||
closeCreateModal()
|
||||
await nextTick()
|
||||
@@ -847,6 +1047,7 @@ const deleteContract = async (id: string) => {
|
||||
contracts.value = contracts.value.filter(item => item.id !== id)
|
||||
selectedContractIds.value = selectedContractIds.value.filter(item => item !== id)
|
||||
await saveContracts()
|
||||
await refreshContractBudgets()
|
||||
notify('删除成功')
|
||||
}
|
||||
|
||||
@@ -885,6 +1086,7 @@ const deleteSelectedContracts = async () => {
|
||||
contracts.value = contracts.value.filter(item => !selectedSet.has(item.id))
|
||||
selectedContractIds.value = selectedContractIds.value.filter(item => !selectedSet.has(item))
|
||||
await saveContracts()
|
||||
await refreshContractBudgets()
|
||||
notify(`删除成功(${targetIds.length} 个合同段)`)
|
||||
exitContractSelectionMode()
|
||||
} catch (error) {
|
||||
@@ -1016,6 +1218,7 @@ const handleGlobalMouseDown = (event: MouseEvent) => {
|
||||
onMounted(async () => {
|
||||
await loadProjectBaseState()
|
||||
await loadContracts()
|
||||
await refreshContractBudgets()
|
||||
triggerCardEnterAnimation()
|
||||
await nextTick()
|
||||
bindContractListScroll()
|
||||
@@ -1024,6 +1227,7 @@ onMounted(async () => {
|
||||
|
||||
onActivated(() => {
|
||||
void loadProjectBaseState()
|
||||
void refreshContractBudgets()
|
||||
triggerCardEnterAnimation()
|
||||
void nextTick(() => {
|
||||
bindContractListScroll()
|
||||
@@ -1033,11 +1237,17 @@ onActivated(() => {
|
||||
onBeforeUnmount(() => {
|
||||
stopDrag()
|
||||
stopContractAutoScroll()
|
||||
if (budgetRefreshTimer) clearTimeout(budgetRefreshTimer)
|
||||
unbindContractListScroll()
|
||||
window.removeEventListener('mousedown', handleGlobalMouseDown)
|
||||
if (cardEnterTimer) clearTimeout(cardEnterTimer)
|
||||
void saveContracts()
|
||||
})
|
||||
|
||||
watch(budgetRefreshSignature, (next, prev) => {
|
||||
if (next === prev) return
|
||||
scheduleRefreshContractBudgets()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -1046,7 +1256,12 @@ onBeforeUnmount(() => {
|
||||
<div class="flex h-full min-h-0 flex-col overflow-hidden">
|
||||
<div class="shrink-0 border-b bg-background/95 px-1 pb-4 backdrop-blur supports-[backdrop-filter]:bg-background/80">
|
||||
<div class="mb-6 flex items-center justify-between pt-1">
|
||||
<h3 class="text-lg font-bold">合同段列表</h3>
|
||||
<div class="space-y-1">
|
||||
<h3 class="text-lg font-bold">合同段列表</h3>
|
||||
<div class="text-xs text-muted-foreground">
|
||||
项目总预算金额:{{ contractBudgetLoading ? '计算中...' : formatBudgetAmount(projectTotalBudget) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<template v-if="isSelectingContracts">
|
||||
<div class="text-xs text-muted-foreground">已选 {{ selectedContractCount }} 个</div>
|
||||
@@ -1232,6 +1447,9 @@ onBeforeUnmount(() => {
|
||||
<span class="min-w-0 shrink text-[11px] leading-none font-normal text-muted-foreground truncate">
|
||||
ID: {{ element.id }}
|
||||
</span>
|
||||
<span class="shrink-0 text-[11px] leading-none font-normal text-muted-foreground">
|
||||
预算:{{ formatBudgetAmount(contractBudgetById[element.id]) }}
|
||||
</span>
|
||||
<span class="shrink-0 text-[11px] leading-none font-normal text-muted-foreground">
|
||||
创建时间:{{ formatDateTime(element.createdAt) }}
|
||||
</span>
|
||||
@@ -1292,6 +1510,7 @@ onBeforeUnmount(() => {
|
||||
]"
|
||||
>
|
||||
<div class="break-all">ID:{{ element.id }}</div>
|
||||
<div>本合同预算金额:{{ formatBudgetAmount(contractBudgetById[element.id]) }}</div>
|
||||
<div>创建时间:{{ formatDateTime(element.createdAt) }}</div>
|
||||
</div>
|
||||
</Card>
|
||||
@@ -1352,6 +1571,9 @@ onBeforeUnmount(() => {
|
||||
<span class="min-w-0 shrink text-[11px] leading-none font-normal text-muted-foreground truncate">
|
||||
ID: {{ element.id }}
|
||||
</span>
|
||||
<span class="shrink-0 text-[11px] leading-none font-normal text-muted-foreground">
|
||||
预算:{{ formatBudgetAmount(contractBudgetById[element.id]) }}
|
||||
</span>
|
||||
<span class="shrink-0 text-[11px] leading-none font-normal text-muted-foreground">
|
||||
创建时间:{{ formatDateTime(element.createdAt) }}
|
||||
</span>
|
||||
@@ -1410,6 +1632,7 @@ onBeforeUnmount(() => {
|
||||
]"
|
||||
>
|
||||
<div class="break-all">ID:{{ element.id }}</div>
|
||||
<div>本合同预算金额:{{ formatBudgetAmount(contractBudgetById[element.id]) }}</div>
|
||||
<div>创建时间:{{ formatDateTime(element.createdAt) }}</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@@ -9,7 +9,9 @@ const props = defineProps<{
|
||||
}>()
|
||||
|
||||
const STORAGE_KEY = computed(() => `htExtraFee-${props.contractId}-additional-work`)
|
||||
const additionalWorkNames = computed(() => additionalWorkList.map(item => String(item)))
|
||||
const additionalWorkNames = computed(() =>
|
||||
additionalWorkList.map(item => String(item?.name || '').trim()).filter(Boolean)
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -22,6 +22,7 @@ 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)
|
||||
@@ -30,15 +31,30 @@ const contractIdText = computed(() => {
|
||||
const contractId = String(props.contractId || '').trim()
|
||||
return contractId
|
||||
})
|
||||
const base = computed<number | null>(() => {
|
||||
const contractId = contractIdText.value
|
||||
if (!contractId) return null
|
||||
return zxFwPricingStore.getBaseSubtotal(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 (base.value == null || rate.value == null) return null
|
||||
return Number((base.value * rate.value).toFixed(3))
|
||||
if (baseValue.value == null || rate.value == null) return null
|
||||
return Number((baseValue.value * rate.value).toFixed(3))
|
||||
})
|
||||
|
||||
const formatAmount = (value: number | null) =>
|
||||
@@ -49,8 +65,32 @@ const ensureContractLoaded = async () => {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,6 +173,18 @@ watch(
|
||||
}
|
||||
)
|
||||
|
||||
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()])
|
||||
})
|
||||
@@ -151,8 +203,8 @@ onBeforeUnmount(() => {
|
||||
<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">基数(所有服务费预算合计)</div>
|
||||
<input type="text" :value="base" readonly disabled tabindex="-1"
|
||||
<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>
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import HtFeeMethodGrid from '@/components/common/HtFeeMethodGrid.vue'
|
||||
import { reserveList } from '@/sql'
|
||||
|
||||
const props = defineProps<{
|
||||
contractId: string
|
||||
@@ -8,7 +9,9 @@ const props = defineProps<{
|
||||
}>()
|
||||
|
||||
const STORAGE_KEY = computed(() => `htExtraFee-${props.contractId}-reserve`)
|
||||
const reserveFeeNames = computed(() => ['预算费'])
|
||||
const reserveFeeNames = computed(() =>
|
||||
reserveList.map(item => String(item?.name || '').trim()).filter(Boolean)
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
+41
-20
@@ -28,7 +28,7 @@ import {
|
||||
} from 'reka-ui'
|
||||
import { decodeZwArchive, encodeZwArchive, ZW_FILE_EXTENSION } from '@/lib/zwArchive'
|
||||
import { addNumbers, roundTo } from '@/lib/decimal'
|
||||
import { exportFile, serviceList, additionalWorkList } from '@/sql'
|
||||
import { exportFile, serviceList, additionalWorkList, reserveList } from '@/sql'
|
||||
|
||||
interface DataEntry {
|
||||
key: string
|
||||
@@ -104,6 +104,9 @@ interface ZxFwStorageLike {
|
||||
interface ScaleMethodRowLike extends ScaleRowLike {
|
||||
basicFormula?: unknown
|
||||
optionalFormula?: unknown
|
||||
benchmarkBudget?: unknown
|
||||
benchmarkBudgetBasic?: unknown
|
||||
benchmarkBudgetOptional?: unknown
|
||||
budgetFee?: unknown
|
||||
budgetFeeBasic?: unknown
|
||||
budgetFeeOptional?: unknown
|
||||
@@ -878,6 +881,14 @@ const getPiniaPersistStores = () =>
|
||||
}
|
||||
})
|
||||
|
||||
const flushPiniaPersistNow = async () => {
|
||||
await Promise.all([
|
||||
tabStore.$persistNow?.(),
|
||||
zxFwPricingStore.$persistNow?.(),
|
||||
kvStore.$persistNow?.()
|
||||
])
|
||||
}
|
||||
|
||||
const readForage = async (store: ForageStore): Promise<DataEntry[]> => {
|
||||
const keys = await store.keys()
|
||||
const entries: DataEntry[] = []
|
||||
@@ -1070,18 +1081,20 @@ const buildMethod1 = (rows: ScaleMethodRowLike[] | undefined): ExportMethod1 | n
|
||||
const det = rows
|
||||
.map(row => {
|
||||
const major = toSafeInteger(row.id)
|
||||
if (major == null || row.budgetFee == null) return null
|
||||
if (major == null) return null
|
||||
const cost = toFiniteNumber(row.amount)
|
||||
const basicFee = toFiniteNumber(row.budgetFee)
|
||||
if (basicFee != null) hasTotalValue = true
|
||||
const basicFeeBasic = toFiniteNumber(row.budgetFeeBasic)
|
||||
const basicFeeOptional = toFiniteNumber(row.budgetFeeOptional)
|
||||
const basicFee = toFiniteNumber(row.benchmarkBudget)
|
||||
const basicFeeBasic = toFiniteNumber(row.benchmarkBudgetBasic)
|
||||
const basicFeeOptional = toFiniteNumber(row.benchmarkBudgetOptional)
|
||||
const fee = toFiniteNumber(row.budgetFee)
|
||||
if (basicFee != null || fee != null) hasTotalValue = true
|
||||
const remark = typeof row.remark === 'string' ? row.remark : ''
|
||||
const hasValue =
|
||||
cost != null ||
|
||||
basicFee != null ||
|
||||
basicFeeBasic != null ||
|
||||
basicFeeOptional != null ||
|
||||
fee != null ||
|
||||
isNonEmptyString(remark)
|
||||
if (!hasValue) return null
|
||||
return {
|
||||
@@ -1096,7 +1109,7 @@ const buildMethod1 = (rows: ScaleMethodRowLike[] | undefined): ExportMethod1 | n
|
||||
majorCoe: toFiniteNumberOrZero(row.majorFactor),
|
||||
processCoe: toFiniteNumber(row.workStageFactor) ?? 1,
|
||||
proportion: toFiniteNumber(row.workRatio) ?? 1,
|
||||
fee: basicFee ?? 0,
|
||||
fee: fee ?? 0,
|
||||
remark
|
||||
}
|
||||
})
|
||||
@@ -1119,18 +1132,20 @@ const buildMethod2 = (rows: ScaleMethodRowLike[] | undefined): ExportMethod2 | n
|
||||
const det = rows
|
||||
.map(row => {
|
||||
const major = toSafeInteger(row.id)
|
||||
if (major == null || row.budgetFee == null) return null
|
||||
if (major == null) return null
|
||||
const area = toFiniteNumber(row.landArea)
|
||||
const basicFee = toFiniteNumber(row.budgetFee)
|
||||
if (basicFee != null) hasTotalValue = true
|
||||
const basicFeeBasic = toFiniteNumber(row.budgetFeeBasic)
|
||||
const basicFeeOptional = toFiniteNumber(row.budgetFeeOptional)
|
||||
const basicFee = toFiniteNumber(row.benchmarkBudget)
|
||||
const basicFeeBasic = toFiniteNumber(row.benchmarkBudgetBasic)
|
||||
const basicFeeOptional = toFiniteNumber(row.benchmarkBudgetOptional)
|
||||
const fee = toFiniteNumber(row.budgetFee)
|
||||
if (basicFee != null || fee != null) hasTotalValue = true
|
||||
const remark = typeof row.remark === 'string' ? row.remark : ''
|
||||
const hasValue =
|
||||
area != null ||
|
||||
basicFee != null ||
|
||||
basicFeeBasic != null ||
|
||||
basicFeeOptional != null ||
|
||||
fee != null ||
|
||||
isNonEmptyString(remark)
|
||||
if (!hasValue) return null
|
||||
return {
|
||||
@@ -1145,7 +1160,7 @@ const buildMethod2 = (rows: ScaleMethodRowLike[] | undefined): ExportMethod2 | n
|
||||
majorCoe: toFiniteNumberOrZero(row.majorFactor),
|
||||
processCoe: toFiniteNumber(row.workStageFactor) ?? 1,
|
||||
proportion: toFiniteNumber(row.workRatio) ?? 1,
|
||||
fee: basicFee ?? 0,
|
||||
fee: fee ?? 0,
|
||||
remark
|
||||
}
|
||||
})
|
||||
@@ -1337,8 +1352,9 @@ const loadHtFeeMethodsByRow = async (mainStorageKey: string, rowId: string) => {
|
||||
}
|
||||
|
||||
const buildAdditionalDetailCode = (index: number, name: string): unknown => {
|
||||
const dictionaryIndex = additionalWorkList.findIndex(item => item === name)
|
||||
const useIndex = dictionaryIndex >= 0 ? dictionaryIndex : index
|
||||
const dictItem = additionalWorkList.find(item => String(item?.name || '').trim() === String(name || '').trim())
|
||||
if (dictItem?.code) return dictItem.code
|
||||
const useIndex = index
|
||||
const suffix = useIndex === 0 ? 'F' : useIndex === 1 ? 'X' : String(useIndex + 1)
|
||||
return createRichTextCode('C', suffix)
|
||||
}
|
||||
@@ -1356,8 +1372,7 @@ const buildAdditionalExport = async (contractId: string): Promise<ExportAddition
|
||||
if (!methodPayload) return null
|
||||
const item: ExportAdditionalDetail = {
|
||||
id: index,
|
||||
code: { richText: [{ font: { charset: 134, color: { theme: 1 }, italic: true, name: '宋体', size: 10 }, text: 'C' }, { font: { charset: 134, color: { theme: 1 }, italic: true, name: 'Calibri', size: 10, vertAlign: 'subscript' }, text: 'F' }] },
|
||||
|
||||
code: buildAdditionalDetailCode(index, row.name),
|
||||
name: row.name,
|
||||
fee: methodPayload.fee
|
||||
}
|
||||
@@ -1371,8 +1386,7 @@ const buildAdditionalExport = async (contractId: string): Promise<ExportAddition
|
||||
|
||||
if (det.length === 0) return null
|
||||
return {
|
||||
code: { richText: [{ font: { charset: 134, color: { theme: 1 }, italic: true, name: '宋体', size: 10 }, text: 'C' }, { font: { charset: 134, color: { theme: 1 }, italic: true, name: 'Calibri', size: 10, vertAlign: 'subscript' }, text: 'C' }] },
|
||||
|
||||
code: createRichTextCode('C', 'C'),
|
||||
name: '附加工作',
|
||||
fee: sumNumbers(det.map(item => item.fee)),
|
||||
det
|
||||
@@ -1389,6 +1403,7 @@ const buildReserveExport = async (contractId: string): Promise<ExportReserve | n
|
||||
const methodPayload = await loadHtFeeMethodsByRow(storageKey, row.id)
|
||||
if (!methodPayload) continue
|
||||
const reserve: ExportReserve = {
|
||||
code: reserveList[0]?.code || createRichTextCode('Y', 'B'),
|
||||
name: row.name || '预备费',
|
||||
fee: methodPayload.fee
|
||||
}
|
||||
@@ -1577,6 +1592,8 @@ const buildExportReportPayload = async (): Promise<ExportReportPayload> => {
|
||||
|
||||
const exportData = async () => {
|
||||
try {
|
||||
// 先把 Pinia 内存态强制落盘,避免导出快照缺失附加工作费/预备费最新值。
|
||||
await flushPiniaPersistNow()
|
||||
const now = new Date()
|
||||
const piniaForageStores = await Promise.all(
|
||||
getPiniaPersistStores().map(async ({ storeName, store }) => ({
|
||||
@@ -1623,6 +1640,7 @@ const exportReport = async () => {
|
||||
const payload = await buildExportReportPayload()
|
||||
showReportExportProgress(80, '正在生成并写出报表文件...')
|
||||
const fileName = `${sanitizeFileNamePart(payload.name)}-报表-${formatExportTimestamp(now)}`
|
||||
console.log(payload)
|
||||
await exportFile(fileName, payload)
|
||||
finishReportExportProgress(true, '报表导出完成')
|
||||
} catch (error) {
|
||||
@@ -1683,7 +1701,10 @@ const confirmImportOverride = async () => {
|
||||
const readPersistedState = (storeId: string) => {
|
||||
const storeName = getPiniaPersistStoreName(storeId)
|
||||
const entries = snapshotMap.get(storeName) || []
|
||||
const hit = entries.find(entry => entry.key === storeName)
|
||||
const hit =
|
||||
entries.find(entry => entry.key === storeName) ||
|
||||
entries.find(entry => entry.key === storeId) ||
|
||||
entries.find(entry => entry.value && typeof entry.value === 'object')
|
||||
return hit && hit.value && typeof hit.value === 'object' ? hit.value : null
|
||||
}
|
||||
|
||||
|
||||
+31
-1
@@ -151,8 +151,38 @@ export const expertList = {
|
||||
7: { code: 'C9-3-2', name: '一级造价工程师且具备高级工程师资格', maxPrice: 2000, minPrice: 1800, defPrice: 1900, manageCoe: 2.05 },
|
||||
};
|
||||
|
||||
export const additionalWorkList = ['人员驻场服务及其他附加工作', '咨询服务协调工作'];
|
||||
export const additionalWorkList = [
|
||||
{
|
||||
code: {
|
||||
richText: [
|
||||
{ font: { charset: 134, color: { theme: 1 }, italic: true, name: '宋体', size: 10 }, text: 'C' },
|
||||
{ font: { charset: 134, color: { theme: 1 }, italic: true, name: 'Calibri', size: 10, vertAlign: 'subscript' }, text: 'F' }
|
||||
]
|
||||
},
|
||||
name: '人员驻场服务及其他附加工作'
|
||||
},
|
||||
{
|
||||
code: {
|
||||
richText: [
|
||||
{ font: { charset: 134, color: { theme: 1 }, italic: true, name: '宋体', size: 10 }, text: 'C' },
|
||||
{ font: { charset: 134, color: { theme: 1 }, italic: true, name: 'Calibri', size: 10, vertAlign: 'subscript' }, text: 'X' }
|
||||
]
|
||||
},
|
||||
name: '咨询服务协调工作'
|
||||
}
|
||||
]
|
||||
|
||||
export const reserveList = [
|
||||
{
|
||||
code: {
|
||||
richText: [
|
||||
{ font: { charset: 134, color: { theme: 1 }, italic: true, name: '宋体', size: 10 }, text: 'Y' },
|
||||
{ font: { charset: 134, color: { theme: 1 }, italic: true, name: 'Calibri', size: 10, vertAlign: 'subscript' }, text: 'B' }
|
||||
]
|
||||
},
|
||||
name: '预备费'
|
||||
}
|
||||
]
|
||||
let costScaleCal = [
|
||||
{ code: 'C1-1', staLine: 0, endLine: 100, basic: { staPrice: 0, rate: 0.01 }, optional: { staPrice: 0, rate: 0.002 } },
|
||||
{ code: 'C1-2', staLine: 100, endLine: 300, basic: { staPrice: 10000, rate: 0.008 }, optional: { staPrice: 2000, rate: 0.0016 } },
|
||||
|
||||
Reference in new issue
Block a user