This commit is contained in:
zwgczx committed 2026-03-07 15:32:54 +08:00
1 parent ad4e9cdee0
commit 043e1fc879
8 files changed
+529 -112

No files matched your search

@@ -3,7 +3,7 @@ import { computed, onActivated, onBeforeUnmount, onMounted, ref, watch } from 'v
import { AgGridVue } from 'ag-grid-vue3'
import type { ColDef, ColGroupDef } from 'ag-grid-community'
import localforage from 'localforage'
import { getMajorDictEntries, isMajorIdInIndustryScope } from '@/sql'
import { getMajorDictEntries, getServiceDictItemById, isMajorIdInIndustryScope } from '@/sql'
import { myTheme, gridOptions } from '@/lib/diyAgGridOptions'
import { decimalAggSum, roundTo, sumByNumber } from '@/lib/decimal'
import { formatThousands } from '@/lib/numberFormat'
@@ -78,6 +78,10 @@ interface XmBaseInfoState {
projectIndustry?: string
}
interface ServiceLite {
onlyCostScale?: boolean | null
}
const props = defineProps<{
contractId: string,
serviceId: string | number
@@ -95,11 +99,16 @@ 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 ONLY_COST_SCALE_ROW_ID = '__only-cost-scale-total__'
const getDefaultConsultCategoryFactor = () =>
consultCategoryFactorMap.value.get(String(props.serviceId)) ?? null
const getDefaultMajorFactorById = (id: string): number | null => majorFactorMap.value.get(id) ?? null
const isOnlyCostScaleService = computed(() => {
const service = getServiceDictItemById(props.serviceId) as ServiceLite | undefined
return service?.onlyCostScale === true
})
const loadFactorDefaults = async () => {
const [consultMap, majorMap] = await Promise.all([
@@ -184,12 +193,17 @@ const detailDict: DictGroup[] = (() => {
})
}
const hasCost = item.hasCost !== false
const hasArea = item.hasArea !== false
// 特殊规则:投资规模法中,hasCost && hasArea 的专业不参与明细行
if (hasCost && hasArea) continue
groupMap.get(parentCode)!.children.push({
id: key,
code,
name: item.name,
hasCost: item.hasCost !== false,
hasArea: item.hasArea !== false
hasCost,
hasArea
})
}
@@ -241,6 +255,48 @@ const buildDefaultRows = (): DetailRow[] => {
return rows
}
const calcOnlyCostScaleAmountFromRows = (rows?: Array<{ amount?: unknown }>) =>
sumByNumber(rows || [], row => (typeof row?.amount === 'number' ? row.amount : null))
const buildOnlyCostScaleRow = (
amount: number | null,
fromDb?: Partial<Pick<DetailRow, 'consultCategoryFactor' | 'majorFactor' | 'workStageFactor' | 'workRatio' | 'remark'>>
): DetailRow => ({
id: ONLY_COST_SCALE_ROW_ID,
groupCode: 'TOTAL',
groupName: '总投资',
majorCode: 'TOTAL',
majorName: '总投资',
hasCost: true,
hasArea: false,
amount,
benchmarkBudget: null,
benchmarkBudgetBasic: null,
benchmarkBudgetOptional: null,
benchmarkBudgetBasicChecked: true,
benchmarkBudgetOptionalChecked: true,
basicFormula: '',
optionalFormula: '',
consultCategoryFactor:
typeof fromDb?.consultCategoryFactor === 'number' ? fromDb.consultCategoryFactor : getDefaultConsultCategoryFactor(),
majorFactor: typeof fromDb?.majorFactor === 'number' ? fromDb.majorFactor : 1,
workStageFactor: typeof fromDb?.workStageFactor === 'number' ? fromDb.workStageFactor : 1,
workRatio: typeof fromDb?.workRatio === 'number' ? fromDb.workRatio : 100,
budgetFee: null,
budgetFeeBasic: null,
budgetFeeOptional: null,
remark: typeof fromDb?.remark === 'string' ? fromDb.remark : '',
path: [ONLY_COST_SCALE_ROW_ID]
})
const buildOnlyCostScaleRows = (
rowsFromDb?: Array<Partial<DetailRow> & Pick<DetailRow, 'id'>>
): DetailRow[] => {
const totalAmount = calcOnlyCostScaleAmountFromRows(rowsFromDb)
const onlyRow = rowsFromDb?.find(row => String(row.id) === ONLY_COST_SCALE_ROW_ID)
return [buildOnlyCostScaleRow(totalAmount, onlyRow)]
}
type SourceRow = Pick<DetailRow, 'id'> &
Partial<
Pick<
@@ -340,6 +396,11 @@ const formatMajorFactor = (params: any) => {
}
const formatEditableMoney = (params: any) => {
if (isOnlyCostScaleService.value) {
if (!params.node?.rowPinned) return ''
if (params.value == null || params.value === '') return '点击输入'
return formatThousands(params.value)
}
if (!params.node?.group && !params.node?.rowPinned && !params.data?.hasCost) {
return ''
}
@@ -432,14 +493,21 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
minWidth: 90,
flex: 2,
editable: params => !params.node?.group && !params.node?.rowPinned && Boolean(params.data?.hasCost),
editable: params => {
if (isOnlyCostScaleService.value) return Boolean(params.node?.rowPinned)
return !params.node?.group && !params.node?.rowPinned && Boolean(params.data?.hasCost)
},
cellClass: params =>
!params.node?.group && !params.node?.rowPinned && params.data?.hasCost
isOnlyCostScaleService.value && params.node?.rowPinned
? 'ag-right-aligned-cell editable-cell-line'
: !params.node?.group && !params.node?.rowPinned && params.data?.hasCost
? 'ag-right-aligned-cell editable-cell-line'
: 'ag-right-aligned-cell',
cellClassRules: {
'editable-cell-empty': params =>
!params.node?.group && !params.node?.rowPinned && Boolean(params.data?.hasCost) && (params.value == null || params.value === '')
isOnlyCostScaleService.value && params.node?.rowPinned
? params.value == null || params.value === ''
: !params.node?.group && !params.node?.rowPinned && Boolean(params.data?.hasCost) && (params.value == null || params.value === '')
},
aggFunc: decimalAggSum,
valueParser: params => parseNumberOrNull(params.newValue, { precision: 2 }),
@@ -613,13 +681,13 @@ const autoGroupColumnDef: ColDef = {
},
valueFormatter: params => {
if (params.node?.rowPinned) {
return '总合计'
return isOnlyCostScaleService.value ? '总投资' : '总合计'
}
const nodeId = String(params.value || '')
return idLabelMap.get(nodeId) || nodeId
},
tooltipValueGetter: params => {
if (params.node?.rowPinned) return '总合计'
if (params.node?.rowPinned) return isOnlyCostScaleService.value ? '总投资' : '总合计'
const nodeId = String(params.value || '')
return idLabelMap.get(nodeId) || nodeId
}
@@ -627,6 +695,7 @@ const autoGroupColumnDef: ColDef = {
const totalAmount = computed(() => sumByNumber(detailRows.value, row => row.amount))
const visibleDetailRows = computed(() => (isOnlyCostScaleService.value ? [] : detailRows.value))
const totalBenchmarkBudgetBasic = computed(() => sumByNumber(detailRows.value, row => getBenchmarkBudgetSplitByAmount(row)?.basic))
const totalBenchmarkBudgetOptional = computed(() => sumByNumber(detailRows.value, row => getBenchmarkBudgetSplitByAmount(row)?.optional))
@@ -727,13 +796,17 @@ const loadFromIndexedDB = async () => {
const applyContractDefaultRows = async () => {
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)
}))
if (isOnlyCostScaleService.value) {
detailRows.value = hasContractRows ? buildOnlyCostScaleRows(htData!.detailRows as any) : buildOnlyCostScaleRows()
} else {
detailRows.value = hasContractRows
? mergeWithDictRows(htData!.detailRows, { includeFactorValues: true })
: buildDefaultRows().map(row => ({
...row,
consultCategoryFactor: getDefaultConsultCategoryFactor(),
majorFactor: getDefaultMajorFactorById(row.id)
}))
}
syncComputedValuesToDetailRows()
}
if (shouldForceDefaultLoad()) {
@@ -743,7 +816,9 @@ const loadFromIndexedDB = async () => {
const data = await localforage.getItem<XmInfoState>(DB_KEY.value)
if (data) {
detailRows.value = mergeWithDictRows(data.detailRows)
detailRows.value = isOnlyCostScaleService.value
? buildOnlyCostScaleRows(data.detailRows as any)
: mergeWithDictRows(data.detailRows)
syncComputedValuesToDetailRows()
return
}
@@ -751,7 +826,7 @@ const loadFromIndexedDB = async () => {
await applyContractDefaultRows()
} catch (error) {
console.error('loadFromIndexedDB failed:', error)
detailRows.value = buildDefaultRows()
detailRows.value = isOnlyCostScaleService.value ? buildOnlyCostScaleRows() : buildDefaultRows()
syncComputedValuesToDetailRows()
}
}
@@ -766,13 +841,17 @@ const importContractData = async () => {
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)
}))
if (isOnlyCostScaleService.value) {
detailRows.value = hasContractRows ? buildOnlyCostScaleRows(htData!.detailRows as any) : buildOnlyCostScaleRows()
} else {
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)
@@ -781,7 +860,7 @@ const importContractData = async () => {
const clearAllData = async () => {
try {
detailRows.value = buildDefaultRows()
detailRows.value = isOnlyCostScaleService.value ? buildOnlyCostScaleRows() : buildDefaultRows()
await saveToIndexedDB()
} catch (error) {
console.error('clearAllData failed:', error)
@@ -802,7 +881,20 @@ let persistTimer: ReturnType<typeof setTimeout> | null = null
let gridPersistTimer: ReturnType<typeof setTimeout> | null = null
const handleCellValueChanged = () => {
const applyOnlyCostScalePinnedAmount = (rawValue: unknown) => {
const amount = parseNumberOrNull(rawValue, { precision: 2 })
const current = detailRows.value[0]
if (!current) {
detailRows.value = [buildOnlyCostScaleRow(amount)]
return
}
detailRows.value = [{ ...current, amount }]
}
const handleCellValueChanged = (event?: any) => {
if (isOnlyCostScaleService.value && event?.node?.rowPinned && event.colDef?.field === 'amount') {
applyOnlyCostScalePinnedAmount(event.newValue)
}
syncComputedValuesToDetailRows()
if (gridPersistTimer) clearTimeout(gridPersistTimer)
gridPersistTimer = setTimeout(() => {
@@ -899,7 +991,7 @@ const processCellFromClipboard = (params: any) => {
</div>
<div class="ag-theme-quartz h-full min-h-0 w-full flex-1">
<AgGridVue :style="{ height: '100%' }" :rowData="detailRows" :pinnedTopRowData="pinnedTopRowData"
<AgGridVue :style="{ height: '100%' }" :rowData="visibleDetailRows" :pinnedTopRowData="pinnedTopRowData"
:columnDefs="columnDefs" :autoGroupColumnDef="autoGroupColumnDef" :gridOptions="gridOptions" :theme="myTheme"
@cell-value-changed="handleCellValueChanged" :suppressColumnVirtualisation="true"
:suppressRowVirtualisation="true" :cellSelection="{ handle: { mode: 'range' } }" :enableClipboard="true"