This commit is contained in:
zwgczx committed 2026-03-02 16:55:27 +08:00
1 parent 757de9a43f
commit 3950057707
17 files changed
+2681 -202

No files matched your search

@@ -10,6 +10,18 @@ import { formatThousands } from '@/lib/numberFormat'
import { syncPricingTotalToZxFw, ZXFW_RELOAD_SERVICE_KEY } from '@/lib/zxFwPricingSync'
import { usePricingPaneReloadStore } from '@/pinia/pricingPaneReload'
import { loadConsultCategoryFactorMap, loadMajorFactorMap } from '@/lib/xmFactorDefaults'
import { Button } from '@/components/ui/button'
import {
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogOverlay,
AlertDialogPortal,
AlertDialogRoot,
AlertDialogTitle,
AlertDialogTrigger,
} from 'reka-ui'
import { AG_GRID_LOCALE_CN } from '@ag-grid-community/locale';
// 精简的边框配置(细线条+浅灰色,弱化分割线视觉)
@@ -59,14 +71,14 @@ const pricingPaneReloadStore = usePricingPaneReloadStore()
const consultCategoryFactorMap = ref<Map<string, number | null>>(new Map())
const majorFactorMap = ref<Map<string, number | null>>(new Map())
let factorDefaultsLoaded = false
const paneInstanceCreatedAt = Date.now()
const getDefaultConsultCategoryFactor = () =>
consultCategoryFactorMap.value.get(String(props.serviceId)) ?? null
const getDefaultMajorFactorById = (id: string): number | null => majorFactorMap.value.get(id) ?? null
const ensureFactorDefaultsLoaded = async () => {
if (factorDefaultsLoaded) return
const loadFactorDefaults = async () => {
const [consultMap, majorMap] = await Promise.all([
loadConsultCategoryFactorMap(),
loadMajorFactorMap()
@@ -76,12 +88,30 @@ const ensureFactorDefaultsLoaded = async () => {
factorDefaultsLoaded = true
}
const ensureFactorDefaultsLoaded = async () => {
if (factorDefaultsLoaded) return
await loadFactorDefaults()
}
const shouldSkipPersist = () => {
const storageKey = `${PRICING_CLEAR_SKIP_PREFIX}${DB_KEY.value}`
const raw = sessionStorage.getItem(storageKey)
if (!raw) return false
const now = Date.now()
if (raw.includes(':')) {
const [issuedRaw, untilRaw] = raw.split(':')
const issuedAt = Number(issuedRaw)
const skipUntil = Number(untilRaw)
if (Number.isFinite(issuedAt) && Number.isFinite(skipUntil) && now <= skipUntil) {
return paneInstanceCreatedAt <= issuedAt
}
sessionStorage.removeItem(storageKey)
return false
}
const skipUntil = Number(raw)
if (Number.isFinite(skipUntil) && Date.now() <= skipUntil) return true
if (Number.isFinite(skipUntil) && now <= skipUntil) return true
sessionStorage.removeItem(storageKey)
return false
}
@@ -165,8 +195,8 @@ const buildDefaultRows = (): DetailRow[] => {
majorName: child.name,
amount: null,
benchmarkBudget: null,
consultCategoryFactor: getDefaultConsultCategoryFactor(),
majorFactor: getDefaultMajorFactorById(child.id),
consultCategoryFactor: null,
majorFactor: null,
budgetFee: null,
remark: '',
path: [group.id, child.id]
@@ -177,7 +207,12 @@ const buildDefaultRows = (): DetailRow[] => {
}
type SourceRow = Pick<DetailRow, 'id'> & Partial<Pick<DetailRow, 'amount' | 'benchmarkBudget' | 'consultCategoryFactor' | 'majorFactor' | 'budgetFee' | 'remark'>>
const mergeWithDictRows = (rowsFromDb: SourceRow[] | undefined): DetailRow[] => {
const mergeWithDictRows = (
rowsFromDb: SourceRow[] | undefined,
options?: { includeAmount?: boolean; includeFactorValues?: boolean }
): DetailRow[] => {
const includeAmount = options?.includeAmount ?? true
const includeFactorValues = options?.includeFactorValues ?? true
const dbValueMap = new Map<string, SourceRow>()
for (const row of rowsFromDb || []) {
dbValueMap.set(row.id, row)
@@ -191,20 +226,24 @@ const mergeWithDictRows = (rowsFromDb: SourceRow[] | undefined): DetailRow[] =>
return {
...row,
amount: typeof fromDb.amount === 'number' ? fromDb.amount : null,
amount: includeAmount && typeof fromDb.amount === 'number' ? fromDb.amount : null,
benchmarkBudget: typeof fromDb.benchmarkBudget === 'number' ? fromDb.benchmarkBudget : null,
consultCategoryFactor:
typeof fromDb.consultCategoryFactor === 'number'
? fromDb.consultCategoryFactor
: hasConsultCategoryFactor
? null
: getDefaultConsultCategoryFactor(),
!includeFactorValues
? null
: typeof fromDb.consultCategoryFactor === 'number'
? fromDb.consultCategoryFactor
: hasConsultCategoryFactor
? null
: getDefaultConsultCategoryFactor(),
majorFactor:
typeof fromDb.majorFactor === 'number'
? fromDb.majorFactor
: hasMajorFactor
? null
: getDefaultMajorFactorById(row.id),
!includeFactorValues
? null
: typeof fromDb.majorFactor === 'number'
? fromDb.majorFactor
: hasMajorFactor
? null
: getDefaultMajorFactorById(row.id),
budgetFee: typeof fromDb.budgetFee === 'number' ? fromDb.budgetFee : null,
remark: typeof fromDb.remark === 'string' ? fromDb.remark : ''
}
@@ -226,21 +265,10 @@ const formatEditableNumber = (params: any) => {
}
const formatConsultCategoryFactor = (params: any) => {
if (params.node?.group) {
const v = getDefaultConsultCategoryFactor()
if (v == null) return ''
return Number(v).toFixed(2)
}
return formatEditableNumber(params)
}
const formatMajorFactor = (params: any) => {
if (params.node?.group) {
const groupId = String(params.node?.key || '')
const v = getDefaultMajorFactorById(groupId)
if (v == null) return ''
return Number(v).toFixed(2)
}
return formatEditableNumber(params)
}
@@ -438,10 +466,13 @@ const saveToIndexedDB = async () => {
const loadFromIndexedDB = async () => {
try {
await ensureFactorDefaultsLoaded()
if (shouldForceDefaultLoad()) {
const htData = await localforage.getItem<{ detailRows: SourceRow[] }>(HT_DB_KEY.value)
detailRows.value = htData?.detailRows ? mergeWithDictRows(htData.detailRows) : buildDefaultRows()
detailRows.value = htData?.detailRows
? mergeWithDictRows(htData.detailRows, { includeAmount: false, includeFactorValues: false })
: buildDefaultRows()
return
}
@@ -453,7 +484,7 @@ const loadFromIndexedDB = async () => {
const htData = await localforage.getItem<{ detailRows: SourceRow[] }>(HT_DB_KEY.value)
if (htData?.detailRows) {
detailRows.value = mergeWithDictRows(htData.detailRows)
detailRows.value = mergeWithDictRows(htData.detailRows, { includeAmount: false, includeFactorValues: false })
return
}
@@ -464,6 +495,25 @@ const loadFromIndexedDB = async () => {
}
}
const importContractData = async () => {
try {
// 使用默认数据时,强制读取最新的项目系数(预算取值优先,空值回退标准系数)
await loadFactorDefaults()
const htData = await localforage.getItem<{ detailRows: SourceRow[] }>(HT_DB_KEY.value)
const hasContractRows = Array.isArray(htData?.detailRows) && htData.detailRows.length > 0
detailRows.value = hasContractRows
? mergeWithDictRows(htData!.detailRows, { includeFactorValues: true })
: buildDefaultRows().map(row => ({
...row,
consultCategoryFactor: getDefaultConsultCategoryFactor(),
majorFactor: getDefaultMajorFactorById(row.id)
}))
await saveToIndexedDB()
} catch (error) {
console.error('importContractData failed:', error)
}
}
watch(
() => pricingPaneReloadStore.getReloadVersion(props.contractId, props.serviceId),
(nextVersion, prevVersion) => {
@@ -521,7 +571,28 @@ const processCellFromClipboard = (params: any) => {
<div class="rounded-lg border bg-card xmMx flex min-h-0 flex-1 flex-col">
<div class="flex items-center justify-between border-b px-4 py-3">
<h3 class="text-sm font-semibold text-foreground">投资规模明细</h3>
<div class="text-xs text-muted-foreground">导入导出</div>
<AlertDialogRoot>
<AlertDialogTrigger as-child>
<Button type="button" variant="outline" size="sm">使用默认数据</Button>
</AlertDialogTrigger>
<AlertDialogPortal>
<AlertDialogOverlay class="fixed inset-0 z-50 bg-black/45" />
<AlertDialogContent class="fixed left-1/2 top-1/2 z-50 w-[92vw] max-w-md -translate-x-1/2 -translate-y-1/2 rounded-lg border bg-background p-5 shadow-xl">
<AlertDialogTitle class="text-base font-semibold">确认覆盖当前明细</AlertDialogTitle>
<AlertDialogDescription class="mt-2 text-sm text-muted-foreground">
将使用合同默认数据覆盖当前投资规模明细,是否继续?
</AlertDialogDescription>
<div class="mt-4 flex items-center justify-end gap-2">
<AlertDialogCancel as-child>
<Button variant="outline">取消</Button>
</AlertDialogCancel>
<AlertDialogAction as-child>
<Button @click="importContractData">确认覆盖</Button>
</AlertDialogAction>
</div>
</AlertDialogContent>
</AlertDialogPortal>
</AlertDialogRoot>
</div>
<div class="ag-theme-quartz h-full min-h-0 w-full flex-1">