This commit is contained in:
zwgczx committed 2026-03-12 16:52:39 +08:00
1 parent f4f6e5c618
commit 1c600e64d0
8 files changed
+549 -179

No files matched your search

+41 -2
View File
@@ -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
View File
@@ -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>
+3 -1
View File
@@ -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>
+60 -8
View File
@@ -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>
+4 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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 } },