1
This commit is contained in:
1 parent
d1dda7f9fa
commit
ab310b49e9
6 files changed
+765
-327
No files matched your search
@@ -49,6 +49,8 @@ interface DictGroup {
|
||||
|
||||
interface DetailRow {
|
||||
id: string
|
||||
projectIndex?: number
|
||||
majorDictId?: string
|
||||
groupCode: string
|
||||
groupName: string
|
||||
majorCode: string
|
||||
@@ -125,6 +127,64 @@ const isMutipleService = computed(() => {
|
||||
return service?.mutiple === true
|
||||
})
|
||||
const projectCount = ref<number>(1)
|
||||
const PROJECT_PATH_PREFIX = 'project-'
|
||||
const PROJECT_ROW_ID_SEPARATOR = '::'
|
||||
const normalizeProjectCount = (value: unknown) => {
|
||||
const parsed = Number(value)
|
||||
if (!Number.isFinite(parsed)) return 1
|
||||
return Math.max(1, Math.floor(parsed))
|
||||
}
|
||||
const getTargetProjectCount = () => (isMutipleService.value ? normalizeProjectCount(projectCount.value) : 1)
|
||||
const buildProjectGroupPathKey = (projectIndex: number) => `${PROJECT_PATH_PREFIX}${projectIndex}`
|
||||
const parseProjectIndexFromPathKey = (value: string) => {
|
||||
const match = /^project-(\d+)$/.exec(value)
|
||||
if (!match) return null
|
||||
return normalizeProjectCount(Number(match[1]))
|
||||
}
|
||||
const buildScopedRowId = (projectIndex: number, majorId: string) =>
|
||||
isMutipleService.value ? `${projectIndex}${PROJECT_ROW_ID_SEPARATOR}${majorId}` : majorId
|
||||
const parseScopedRowId = (id: unknown) => {
|
||||
const rawId = String(id || '')
|
||||
const match = /^(\d+)::(.+)$/.exec(rawId)
|
||||
if (!match) {
|
||||
return {
|
||||
projectIndex: 1,
|
||||
majorDictId: rawId
|
||||
}
|
||||
}
|
||||
return {
|
||||
projectIndex: normalizeProjectCount(Number(match[1])),
|
||||
majorDictId: String(match[2] || '').trim()
|
||||
}
|
||||
}
|
||||
const resolveRowProjectIndex = (row: Partial<DetailRow> | undefined) => {
|
||||
if (!row) return 1
|
||||
if (typeof row.projectIndex === 'number' && Number.isFinite(row.projectIndex)) {
|
||||
return normalizeProjectCount(row.projectIndex)
|
||||
}
|
||||
if (Array.isArray(row.path) && row.path.length > 0) {
|
||||
const projectIndexFromPath = parseProjectIndexFromPathKey(String(row.path[0] || ''))
|
||||
if (projectIndexFromPath != null) return projectIndexFromPath
|
||||
}
|
||||
return parseScopedRowId(row.id).projectIndex
|
||||
}
|
||||
const resolveRowMajorDictId = (row: Partial<DetailRow> | undefined) => {
|
||||
if (!row) return ''
|
||||
const direct = String(row.majorDictId || '').trim()
|
||||
if (direct) return majorIdAliasMap.get(direct) || direct
|
||||
const parsed = parseScopedRowId(row.id).majorDictId
|
||||
return majorIdAliasMap.get(parsed) || parsed
|
||||
}
|
||||
const makeProjectMajorKey = (projectIndex: number, majorDictId: string) =>
|
||||
`${normalizeProjectCount(projectIndex)}:${String(majorDictId || '').trim()}`
|
||||
const inferProjectCountFromRows = (rows?: Array<Partial<DetailRow>>) => {
|
||||
if (!isMutipleService.value) return 1
|
||||
let maxProjectIndex = 1
|
||||
for (const row of rows || []) {
|
||||
maxProjectIndex = Math.max(maxProjectIndex, resolveRowProjectIndex(row))
|
||||
}
|
||||
return maxProjectIndex
|
||||
}
|
||||
const totalLabel = computed(() => {
|
||||
const industryName = industryNameMap.get(activeIndustryCode.value.trim()) || ''
|
||||
return industryName ? `${industryName}总投资` : '总投资'
|
||||
@@ -246,38 +306,45 @@ for (const group of detailDict) {
|
||||
}
|
||||
}
|
||||
|
||||
const buildDefaultRows = (): DetailRow[] => {
|
||||
const buildDefaultRows = (projectCountValue = getTargetProjectCount()): DetailRow[] => {
|
||||
if (!activeIndustryCode.value) return []
|
||||
const rows: DetailRow[] = []
|
||||
for (const group of detailDict) {
|
||||
if (activeIndustryCode.value && !isMajorIdInIndustryScope(group.id, activeIndustryCode.value)) continue
|
||||
for (const child of group.children) {
|
||||
rows.push({
|
||||
id: child.id,
|
||||
groupCode: group.code,
|
||||
groupName: group.name,
|
||||
majorCode: child.code,
|
||||
majorName: child.name,
|
||||
hasCost: child.hasCost,
|
||||
hasArea: child.hasArea,
|
||||
amount: null,
|
||||
benchmarkBudget: null,
|
||||
benchmarkBudgetBasic: null,
|
||||
benchmarkBudgetOptional: null,
|
||||
benchmarkBudgetBasicChecked: true,
|
||||
benchmarkBudgetOptionalChecked: true,
|
||||
basicFormula: '',
|
||||
optionalFormula: '',
|
||||
consultCategoryFactor: null,
|
||||
majorFactor: null,
|
||||
workStageFactor: 1,
|
||||
workRatio: 100,
|
||||
budgetFee: null,
|
||||
budgetFeeBasic: null,
|
||||
budgetFeeOptional: null,
|
||||
remark: '',
|
||||
path: [group.id, child.id]
|
||||
})
|
||||
for (let projectIndex = 1; projectIndex <= projectCountValue; projectIndex++) {
|
||||
for (const group of detailDict) {
|
||||
if (activeIndustryCode.value && !isMajorIdInIndustryScope(group.id, activeIndustryCode.value)) continue
|
||||
for (const child of group.children) {
|
||||
const rowId = buildScopedRowId(projectIndex, child.id)
|
||||
rows.push({
|
||||
id: rowId,
|
||||
projectIndex,
|
||||
majorDictId: child.id,
|
||||
groupCode: group.code,
|
||||
groupName: group.name,
|
||||
majorCode: child.code,
|
||||
majorName: child.name,
|
||||
hasCost: child.hasCost,
|
||||
hasArea: child.hasArea,
|
||||
amount: null,
|
||||
benchmarkBudget: null,
|
||||
benchmarkBudgetBasic: null,
|
||||
benchmarkBudgetOptional: null,
|
||||
benchmarkBudgetBasicChecked: true,
|
||||
benchmarkBudgetOptionalChecked: true,
|
||||
basicFormula: '',
|
||||
optionalFormula: '',
|
||||
consultCategoryFactor: null,
|
||||
majorFactor: null,
|
||||
workStageFactor: 1,
|
||||
workRatio: 100,
|
||||
budgetFee: null,
|
||||
budgetFeeBasic: null,
|
||||
budgetFeeOptional: null,
|
||||
remark: '',
|
||||
path: isMutipleService.value
|
||||
? [buildProjectGroupPathKey(projectIndex), group.id, rowId]
|
||||
: [group.id, rowId]
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return rows
|
||||
@@ -286,15 +353,23 @@ const buildDefaultRows = (): DetailRow[] => {
|
||||
const calcOnlyCostScaleAmountFromRows = (rows?: Array<{ amount?: unknown }>) =>
|
||||
sumByNumber(rows || [], row => (typeof row?.amount === 'number' ? row.amount : null))
|
||||
|
||||
const getOnlyCostScaleMajorFactorDefault = () => {
|
||||
const getOnlyCostScaleMajorEntry = () => {
|
||||
const industryId = String(activeIndustryCode.value || '').trim()
|
||||
if (!industryId) return 1
|
||||
if (!industryId) return null
|
||||
const industryMajor = serviceEntries.find(([, item]) => {
|
||||
const majorIndustryId = String(item?.industryId ?? '').trim()
|
||||
return majorIndustryId === industryId && !String(item?.code || '').includes('-')
|
||||
})
|
||||
if (!industryMajor) return null
|
||||
const [id, item] = industryMajor
|
||||
return { id, item }
|
||||
}
|
||||
|
||||
const getOnlyCostScaleMajorFactorDefault = () => {
|
||||
const industryMajor = getOnlyCostScaleMajorEntry()
|
||||
if (!industryMajor) return 1
|
||||
const [majorId, majorItem] = industryMajor
|
||||
const majorId = industryMajor.id
|
||||
const majorItem = industryMajor.item
|
||||
const fromMap = majorFactorMap.value.get(String(majorId))
|
||||
if (typeof fromMap === 'number' && Number.isFinite(fromMap)) return fromMap
|
||||
if (typeof majorItem?.defCoe === 'number' && Number.isFinite(majorItem.defCoe)) return majorItem.defCoe
|
||||
@@ -303,6 +378,7 @@ const getOnlyCostScaleMajorFactorDefault = () => {
|
||||
|
||||
const buildOnlyCostScaleRow = (
|
||||
amount: number | null,
|
||||
projectIndex: number,
|
||||
fromDb?: Partial<
|
||||
Pick<
|
||||
DetailRow,
|
||||
@@ -316,11 +392,13 @@ const buildOnlyCostScaleRow = (
|
||||
>
|
||||
>
|
||||
): DetailRow => ({
|
||||
id: ONLY_COST_SCALE_ROW_ID,
|
||||
groupCode: 'TOTAL',
|
||||
groupName: '总投资',
|
||||
majorCode: 'TOTAL',
|
||||
majorName: '总投资',
|
||||
id: buildScopedRowId(projectIndex, getOnlyCostScaleMajorEntry()?.id || ONLY_COST_SCALE_ROW_ID),
|
||||
projectIndex,
|
||||
majorDictId: getOnlyCostScaleMajorEntry()?.id || ONLY_COST_SCALE_ROW_ID,
|
||||
groupCode: getOnlyCostScaleMajorEntry()?.item?.code || 'TOTAL',
|
||||
groupName: getOnlyCostScaleMajorEntry()?.item?.name || totalLabel.value,
|
||||
majorCode: getOnlyCostScaleMajorEntry()?.item?.code || 'TOTAL',
|
||||
majorName: getOnlyCostScaleMajorEntry()?.item?.name || totalLabel.value,
|
||||
hasCost: true,
|
||||
hasArea: false,
|
||||
amount,
|
||||
@@ -342,21 +420,56 @@ const buildOnlyCostScaleRow = (
|
||||
budgetFeeBasic: null,
|
||||
budgetFeeOptional: null,
|
||||
remark: typeof fromDb?.remark === 'string' ? fromDb.remark : '',
|
||||
path: [ONLY_COST_SCALE_ROW_ID]
|
||||
path: isMutipleService.value
|
||||
? [buildProjectGroupPathKey(projectIndex), buildScopedRowId(projectIndex, getOnlyCostScaleMajorEntry()?.id || ONLY_COST_SCALE_ROW_ID)]
|
||||
: [buildScopedRowId(projectIndex, getOnlyCostScaleMajorEntry()?.id || ONLY_COST_SCALE_ROW_ID)]
|
||||
})
|
||||
|
||||
const buildOnlyCostScaleRows = (
|
||||
rowsFromDb?: Array<Partial<DetailRow> & Pick<DetailRow, 'id'>>
|
||||
rowsFromDb?: Array<Partial<DetailRow> & Pick<DetailRow, 'id'>>,
|
||||
options?: { projectCount?: number; cloneFromProjectOne?: boolean }
|
||||
): DetailRow[] => {
|
||||
const totalAmount = calcOnlyCostScaleAmountFromRows(rowsFromDb)
|
||||
const onlyRow = rowsFromDb?.find(row => String(row.id) === ONLY_COST_SCALE_ROW_ID)
|
||||
return [buildOnlyCostScaleRow(totalAmount, onlyRow)]
|
||||
const targetProjectCount = normalizeProjectCount(options?.projectCount ?? getTargetProjectCount())
|
||||
const onlyCostMajorId = getOnlyCostScaleMajorEntry()?.id || ONLY_COST_SCALE_ROW_ID
|
||||
const dbValueMap = new Map<string, Partial<DetailRow> & Pick<DetailRow, 'id'>>()
|
||||
for (const row of rowsFromDb || []) {
|
||||
const projectIndex = resolveRowProjectIndex(row)
|
||||
const majorDictId = resolveRowMajorDictId(row) || onlyCostMajorId
|
||||
dbValueMap.set(makeProjectMajorKey(projectIndex, majorDictId), row)
|
||||
if (String(row.id || '') === ONLY_COST_SCALE_ROW_ID && !dbValueMap.has(makeProjectMajorKey(projectIndex, onlyCostMajorId))) {
|
||||
dbValueMap.set(makeProjectMajorKey(projectIndex, onlyCostMajorId), row)
|
||||
}
|
||||
}
|
||||
|
||||
const result: DetailRow[] = []
|
||||
for (let projectIndex = 1; projectIndex <= targetProjectCount; projectIndex++) {
|
||||
const key = makeProjectMajorKey(projectIndex, onlyCostMajorId)
|
||||
const firstProjectKey = makeProjectMajorKey(1, onlyCostMajorId)
|
||||
const fromDb =
|
||||
dbValueMap.get(key) ||
|
||||
(options?.cloneFromProjectOne && projectIndex > 1 ? dbValueMap.get(firstProjectKey) : undefined)
|
||||
const fallbackAmount =
|
||||
options?.cloneFromProjectOne && projectIndex > 1 && fromDb == null
|
||||
? calcOnlyCostScaleAmountFromRows(rowsFromDb)
|
||||
: null
|
||||
result.push(
|
||||
buildOnlyCostScaleRow(
|
||||
typeof fromDb?.amount === 'number' ? fromDb.amount : fallbackAmount,
|
||||
projectIndex,
|
||||
fromDb
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
type SourceRow = Pick<DetailRow, 'id'> &
|
||||
Partial<
|
||||
Pick<
|
||||
DetailRow,
|
||||
| 'projectIndex'
|
||||
| 'majorDictId'
|
||||
| 'amount'
|
||||
| 'benchmarkBudget'
|
||||
| 'benchmarkBudgetBasic'
|
||||
@@ -377,22 +490,32 @@ type SourceRow = Pick<DetailRow, 'id'> &
|
||||
>
|
||||
const mergeWithDictRows = (
|
||||
rowsFromDb: SourceRow[] | undefined,
|
||||
options?: { includeAmount?: boolean; includeFactorValues?: boolean }
|
||||
options?: {
|
||||
includeAmount?: boolean
|
||||
includeFactorValues?: boolean
|
||||
projectCount?: number
|
||||
cloneFromProjectOne?: boolean
|
||||
}
|
||||
): DetailRow[] => {
|
||||
const includeAmount = options?.includeAmount ?? true
|
||||
const includeFactorValues = options?.includeFactorValues ?? true
|
||||
const targetProjectCount = normalizeProjectCount(options?.projectCount ?? getTargetProjectCount())
|
||||
const dbValueMap = new Map<string, SourceRow>()
|
||||
for (const row of rowsFromDb || []) {
|
||||
const rowId = String(row.id)
|
||||
dbValueMap.set(rowId, row)
|
||||
const aliasId = majorIdAliasMap.get(rowId)
|
||||
if (aliasId && !dbValueMap.has(aliasId)) {
|
||||
dbValueMap.set(aliasId, row)
|
||||
}
|
||||
const projectIndex = resolveRowProjectIndex(row)
|
||||
const majorDictId = resolveRowMajorDictId(row)
|
||||
if (!majorDictId) continue
|
||||
dbValueMap.set(makeProjectMajorKey(projectIndex, majorDictId), row)
|
||||
}
|
||||
|
||||
return buildDefaultRows().map(row => {
|
||||
const fromDb = dbValueMap.get(row.id)
|
||||
return buildDefaultRows(targetProjectCount).map(row => {
|
||||
const rowProjectIndex = resolveRowProjectIndex(row)
|
||||
const rowMajorDictId = resolveRowMajorDictId(row)
|
||||
const fromDb =
|
||||
dbValueMap.get(makeProjectMajorKey(rowProjectIndex, rowMajorDictId)) ||
|
||||
(options?.cloneFromProjectOne && rowProjectIndex > 1
|
||||
? dbValueMap.get(makeProjectMajorKey(1, rowMajorDictId))
|
||||
: undefined)
|
||||
if (!fromDb) return row
|
||||
const hasConsultCategoryFactor = Object.prototype.hasOwnProperty.call(fromDb, 'consultCategoryFactor')
|
||||
const hasMajorFactor = Object.prototype.hasOwnProperty.call(fromDb, 'majorFactor')
|
||||
@@ -424,7 +547,7 @@ const mergeWithDictRows = (
|
||||
? fromDb.majorFactor
|
||||
: hasMajorFactor
|
||||
? null
|
||||
: getDefaultMajorFactorById(row.id),
|
||||
: getDefaultMajorFactorById(rowMajorDictId),
|
||||
workStageFactor: typeof fromDb.workStageFactor === 'number' ? fromDb.workStageFactor : row.workStageFactor,
|
||||
workRatio: typeof fromDb.workRatio === 'number' ? fromDb.workRatio : row.workRatio,
|
||||
budgetFee: typeof fromDb.budgetFee === 'number' ? fromDb.budgetFee : null,
|
||||
@@ -452,11 +575,6 @@ const formatMajorFactor = (params: any) => {
|
||||
}
|
||||
|
||||
const formatEditableMoney = (params: any) => {
|
||||
if (isOnlyCostScaleService.value) {
|
||||
if (!params.node?.rowPinned) return ''
|
||||
if (params.value == null || params.value === '') return '点击输入'
|
||||
return formatThousandsFlexible(params.value, 3)
|
||||
}
|
||||
if (!params.node?.group && !params.node?.rowPinned && !params.data?.hasCost) {
|
||||
return ''
|
||||
}
|
||||
@@ -477,7 +595,7 @@ type BudgetCheckField = 'benchmarkBudgetBasicChecked' | 'benchmarkBudgetOptional
|
||||
const createBudgetCellRendererWithCheck = (checkField: BudgetCheckField) => (params: any) => {
|
||||
const valueText = formatReadonlyMoney(params)
|
||||
const hasValue = params.value != null && params.value !== ''
|
||||
if (params.node?.group || (params.node?.rowPinned && !isOnlyCostScaleService.value) || !params.data || !hasValue) {
|
||||
if (params.node?.group || params.node?.rowPinned || !params.data || !hasValue) {
|
||||
return valueText
|
||||
}
|
||||
|
||||
@@ -495,11 +613,7 @@ const createBudgetCellRendererWithCheck = (checkField: BudgetCheckField) => (par
|
||||
checkbox.checked = params.data[checkField] !== false
|
||||
checkbox.addEventListener('click', event => event.stopPropagation())
|
||||
checkbox.addEventListener('change', () => {
|
||||
const isOnlyCostScalePinned = isOnlyCostScaleService.value && Boolean(params.node?.rowPinned)
|
||||
const targetRow =
|
||||
isOnlyCostScalePinned
|
||||
? detailRows.value[0]
|
||||
: (params.data as DetailRow | undefined)
|
||||
const targetRow = params.data as DetailRow | undefined
|
||||
if (!targetRow) return
|
||||
|
||||
targetRow[checkField] = checkbox.checked
|
||||
@@ -595,7 +709,6 @@ const getBudgetFeeSplit = (
|
||||
|
||||
const getMergeColSpanBeforeTotal = (params: any) => {
|
||||
if (!params.node?.group && !params.node?.rowPinned) return 1
|
||||
if (isOnlyCostScaleService.value && params.node?.rowPinned) return 1
|
||||
const displayedColumns = params.api?.getAllDisplayedColumns?.()
|
||||
if (!Array.isArray(displayedColumns) || !params.column) return 1
|
||||
const currentIndex = displayedColumns.findIndex((column: any) => column.getColId() === params.column.getColId())
|
||||
@@ -613,20 +726,15 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
|
||||
flex: 2,
|
||||
|
||||
editable: params => {
|
||||
if (isOnlyCostScaleService.value) return Boolean(params.node?.rowPinned)
|
||||
return !params.node?.group && !params.node?.rowPinned && Boolean(params.data?.hasCost)
|
||||
},
|
||||
cellClass: params =>
|
||||
isOnlyCostScaleService.value && params.node?.rowPinned
|
||||
? 'ag-right-aligned-cell editable-cell-line'
|
||||
: !params.node?.group && !params.node?.rowPinned && params.data?.hasCost
|
||||
!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 =>
|
||||
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 === '')
|
||||
!params.node?.group && !params.node?.rowPinned && Boolean(params.data?.hasCost) && (params.value == null || params.value === '')
|
||||
},
|
||||
valueParser: params => parseNumberOrNull(params.newValue, { precision: 3 }),
|
||||
valueFormatter: formatEditableMoney
|
||||
@@ -645,7 +753,7 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
|
||||
cellClass: 'ag-right-aligned-cell',
|
||||
valueGetter: params =>
|
||||
params.node?.rowPinned
|
||||
? (isOnlyCostScaleService.value ? getCheckedBenchmarkBudgetSplitByAmount(params.data)?.basic ?? null : null)
|
||||
? null
|
||||
: getCheckedBenchmarkBudgetSplitByAmount(params.data)?.basic ?? null,
|
||||
cellRenderer: createBudgetCellRendererWithCheck('benchmarkBudgetBasicChecked'),
|
||||
valueFormatter: formatReadonlyMoney
|
||||
@@ -660,7 +768,7 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
|
||||
cellClass: 'ag-right-aligned-cell',
|
||||
valueGetter: params =>
|
||||
params.node?.rowPinned
|
||||
? (isOnlyCostScaleService.value ? getCheckedBenchmarkBudgetSplitByAmount(params.data)?.optional ?? null : null)
|
||||
? null
|
||||
: getCheckedBenchmarkBudgetSplitByAmount(params.data)?.optional ?? null,
|
||||
cellRenderer: createBudgetCellRendererWithCheck('benchmarkBudgetOptionalChecked'),
|
||||
valueFormatter: formatReadonlyMoney
|
||||
@@ -675,7 +783,7 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
|
||||
cellClass: 'ag-right-aligned-cell',
|
||||
valueGetter: params =>
|
||||
params.node?.rowPinned
|
||||
? (isOnlyCostScaleService.value ? getCheckedBenchmarkBudgetSplitByAmount(params.data)?.total ?? null : null)
|
||||
? null
|
||||
: getCheckedBenchmarkBudgetSplitByAmount(params.data)?.total ?? null,
|
||||
valueFormatter: formatReadonlyMoney
|
||||
}
|
||||
@@ -691,21 +799,14 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
|
||||
colId: 'consultCategoryFactor',
|
||||
minWidth: 80,
|
||||
flex: 1,
|
||||
editable: params =>
|
||||
isOnlyCostScaleService.value
|
||||
? Boolean(params.node?.rowPinned)
|
||||
: !params.node?.group && !params.node?.rowPinned,
|
||||
editable: params => !params.node?.group && !params.node?.rowPinned,
|
||||
cellClass: params =>
|
||||
isOnlyCostScaleService.value && params.node?.rowPinned
|
||||
!params.node?.group && !params.node?.rowPinned
|
||||
? 'editable-cell-line'
|
||||
: !params.node?.group && !params.node?.rowPinned
|
||||
? 'editable-cell-line'
|
||||
: '',
|
||||
: '',
|
||||
cellClassRules: {
|
||||
'editable-cell-empty': params =>
|
||||
isOnlyCostScaleService.value && params.node?.rowPinned
|
||||
? params.value == null || params.value === ''
|
||||
: !params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
|
||||
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
|
||||
},
|
||||
valueParser: params => parseNumberOrNull(params.newValue, { precision: 3 }),
|
||||
valueFormatter: formatConsultCategoryFactor
|
||||
@@ -716,21 +817,14 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
|
||||
colId: 'majorFactor',
|
||||
minWidth: 80,
|
||||
flex: 1,
|
||||
editable: params =>
|
||||
isOnlyCostScaleService.value
|
||||
? Boolean(params.node?.rowPinned)
|
||||
: !params.node?.group && !params.node?.rowPinned,
|
||||
editable: params => !params.node?.group && !params.node?.rowPinned,
|
||||
cellClass: params =>
|
||||
isOnlyCostScaleService.value && params.node?.rowPinned
|
||||
!params.node?.group && !params.node?.rowPinned
|
||||
? 'editable-cell-line'
|
||||
: !params.node?.group && !params.node?.rowPinned
|
||||
? 'editable-cell-line'
|
||||
: '',
|
||||
: '',
|
||||
cellClassRules: {
|
||||
'editable-cell-empty': params =>
|
||||
isOnlyCostScaleService.value && params.node?.rowPinned
|
||||
? params.value == null || params.value === ''
|
||||
: !params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
|
||||
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
|
||||
},
|
||||
valueParser: params => parseNumberOrNull(params.newValue, { precision: 3 }),
|
||||
valueFormatter: formatMajorFactor
|
||||
@@ -741,21 +835,14 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
|
||||
colId: 'workStageFactor',
|
||||
minWidth: 80,
|
||||
flex: 1,
|
||||
editable: params =>
|
||||
isOnlyCostScaleService.value
|
||||
? Boolean(params.node?.rowPinned)
|
||||
: !params.node?.group && !params.node?.rowPinned,
|
||||
editable: params => !params.node?.group && !params.node?.rowPinned,
|
||||
cellClass: params =>
|
||||
isOnlyCostScaleService.value && params.node?.rowPinned
|
||||
!params.node?.group && !params.node?.rowPinned
|
||||
? 'editable-cell-line'
|
||||
: !params.node?.group && !params.node?.rowPinned
|
||||
? 'editable-cell-line'
|
||||
: '',
|
||||
: '',
|
||||
cellClassRules: {
|
||||
'editable-cell-empty': params =>
|
||||
isOnlyCostScaleService.value && params.node?.rowPinned
|
||||
? params.value == null || params.value === ''
|
||||
: !params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
|
||||
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
|
||||
},
|
||||
valueParser: params => parseNumberOrNull(params.newValue, { precision: 3 }),
|
||||
valueFormatter: formatEditableNumber
|
||||
@@ -766,21 +853,14 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
|
||||
colId: 'workRatio',
|
||||
minWidth: 80,
|
||||
flex: 1,
|
||||
editable: params =>
|
||||
isOnlyCostScaleService.value
|
||||
? Boolean(params.node?.rowPinned)
|
||||
: !params.node?.group && !params.node?.rowPinned,
|
||||
editable: params => !params.node?.group && !params.node?.rowPinned,
|
||||
cellClass: params =>
|
||||
isOnlyCostScaleService.value && params.node?.rowPinned
|
||||
!params.node?.group && !params.node?.rowPinned
|
||||
? 'editable-cell-line'
|
||||
: !params.node?.group && !params.node?.rowPinned
|
||||
? 'editable-cell-line'
|
||||
: '',
|
||||
: '',
|
||||
cellClassRules: {
|
||||
'editable-cell-empty': params =>
|
||||
isOnlyCostScaleService.value && params.node?.rowPinned
|
||||
? params.value == null || params.value === ''
|
||||
: !params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
|
||||
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
|
||||
},
|
||||
valueParser: params => parseNumberOrNull(params.newValue, { precision: 3 }),
|
||||
valueFormatter: formatEditableNumber
|
||||
@@ -839,54 +919,64 @@ const autoGroupColumnDef: ColDef = {
|
||||
if (params.node?.rowPinned) {
|
||||
return totalLabel.value
|
||||
}
|
||||
const rowData = params.data as DetailRow | undefined
|
||||
if (!params.node?.group && rowData?.majorCode && rowData?.majorName) {
|
||||
return `${rowData.majorCode} ${rowData.majorName}`
|
||||
}
|
||||
const nodeId = String(params.value || '')
|
||||
const projectIndex = parseProjectIndexFromPathKey(nodeId)
|
||||
if (projectIndex != null) return `项目${projectIndex}`
|
||||
return idLabelMap.get(nodeId) || nodeId
|
||||
},
|
||||
tooltipValueGetter: params => {
|
||||
if (params.node?.rowPinned) return totalLabel.value
|
||||
const rowData = params.data as DetailRow | undefined
|
||||
if (!params.node?.group && rowData?.majorCode && rowData?.majorName) {
|
||||
return `${rowData.majorCode} ${rowData.majorName}`
|
||||
}
|
||||
const nodeId = String(params.value || '')
|
||||
const projectIndex = parseProjectIndexFromPathKey(nodeId)
|
||||
if (projectIndex != null) return `项目${projectIndex}`
|
||||
return idLabelMap.get(nodeId) || nodeId
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const totalAmount = computed(() => sumByNumber(detailRows.value, row => row.amount))
|
||||
const visibleDetailRows = computed(() => (isOnlyCostScaleService.value ? [] : detailRows.value))
|
||||
const onlyCostScaleSourceRow = computed(() => detailRows.value[0] ?? buildOnlyCostScaleRow(null))
|
||||
|
||||
|
||||
|
||||
const totalBudgetFeeBasic = computed(() => sumByNumber(detailRows.value, row => getBudgetFeeSplit(row)?.basic))
|
||||
const totalBudgetFeeOptional = computed(() => sumByNumber(detailRows.value, row => getBudgetFeeSplit(row)?.optional))
|
||||
const totalBudgetFee = computed(() => sumByNumber(detailRows.value, row => getBudgetFee(row)))
|
||||
const pinnedTopRowData = computed(() => [
|
||||
{
|
||||
id: 'pinned-total-row',
|
||||
groupCode: '',
|
||||
groupName: '',
|
||||
majorCode: '',
|
||||
majorName: '',
|
||||
hasCost: false,
|
||||
hasArea: false,
|
||||
amount: isOnlyCostScaleService.value ? onlyCostScaleSourceRow.value.amount : null,
|
||||
benchmarkBudget: null,
|
||||
benchmarkBudgetBasic: null,
|
||||
benchmarkBudgetOptional: null,
|
||||
benchmarkBudgetBasicChecked: isOnlyCostScaleService.value ? onlyCostScaleSourceRow.value.benchmarkBudgetBasicChecked !== false : true,
|
||||
benchmarkBudgetOptionalChecked: isOnlyCostScaleService.value ? onlyCostScaleSourceRow.value.benchmarkBudgetOptionalChecked !== false : true,
|
||||
basicFormula: '',
|
||||
optionalFormula: '',
|
||||
consultCategoryFactor: isOnlyCostScaleService.value ? onlyCostScaleSourceRow.value.consultCategoryFactor : null,
|
||||
majorFactor: isOnlyCostScaleService.value ? onlyCostScaleSourceRow.value.majorFactor : null,
|
||||
workStageFactor: isOnlyCostScaleService.value ? onlyCostScaleSourceRow.value.workStageFactor : null,
|
||||
workRatio: isOnlyCostScaleService.value ? onlyCostScaleSourceRow.value.workRatio : null,
|
||||
budgetFee: totalBudgetFee.value,
|
||||
budgetFeeBasic: totalBudgetFeeBasic.value,
|
||||
budgetFeeOptional: totalBudgetFeeOptional.value,
|
||||
remark: '',
|
||||
path: ['TOTAL']
|
||||
}
|
||||
])
|
||||
const pinnedTopRowData = computed(() => {
|
||||
return [
|
||||
{
|
||||
id: 'pinned-total-row',
|
||||
groupCode: '',
|
||||
groupName: '',
|
||||
majorCode: '',
|
||||
majorName: '',
|
||||
hasCost: false,
|
||||
hasArea: false,
|
||||
amount: null,
|
||||
benchmarkBudget: null,
|
||||
benchmarkBudgetBasic: null,
|
||||
benchmarkBudgetOptional: null,
|
||||
benchmarkBudgetBasicChecked: true,
|
||||
benchmarkBudgetOptionalChecked: true,
|
||||
basicFormula: '',
|
||||
optionalFormula: '',
|
||||
consultCategoryFactor: null,
|
||||
majorFactor: null,
|
||||
workStageFactor: null,
|
||||
workRatio: null,
|
||||
budgetFee: totalBudgetFee.value,
|
||||
budgetFeeBasic: totalBudgetFeeBasic.value,
|
||||
budgetFeeOptional: totalBudgetFeeOptional.value,
|
||||
remark: '',
|
||||
path: ['TOTAL']
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
const syncComputedValuesToDetailRows = () => {
|
||||
for (const row of detailRows.value) {
|
||||
@@ -943,26 +1033,114 @@ const saveToIndexedDB = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const getProjectMajorKeyFromRow = (row: Partial<DetailRow> | undefined) => {
|
||||
if (!row) return ''
|
||||
const majorDictId = resolveRowMajorDictId(row)
|
||||
if (!majorDictId) return ''
|
||||
return makeProjectMajorKey(resolveRowProjectIndex(row), majorDictId)
|
||||
}
|
||||
|
||||
const buildRowsFromImportDefaultSource = async (
|
||||
targetProjectCount: number
|
||||
): Promise<DetailRow[]> => {
|
||||
// 与“使用默认数据”同源:先强制刷新系数,再按合同卡片默认带出。
|
||||
await loadFactorDefaults()
|
||||
const htData = await localforage.getItem<{ detailRows: SourceRow[] }>(HT_DB_KEY.value)
|
||||
const hasContractRows = Array.isArray(htData?.detailRows) && htData.detailRows.length > 0
|
||||
if (isOnlyCostScaleService.value) {
|
||||
return hasContractRows
|
||||
? buildOnlyCostScaleRows(htData!.detailRows as any, { projectCount: targetProjectCount, cloneFromProjectOne: true })
|
||||
: buildOnlyCostScaleRows(undefined, { projectCount: targetProjectCount })
|
||||
}
|
||||
return hasContractRows
|
||||
? mergeWithDictRows(htData!.detailRows, {
|
||||
includeFactorValues: true,
|
||||
projectCount: targetProjectCount,
|
||||
cloneFromProjectOne: true
|
||||
})
|
||||
: buildDefaultRows(targetProjectCount).map(row => ({
|
||||
...row,
|
||||
consultCategoryFactor: getDefaultConsultCategoryFactor(),
|
||||
majorFactor: getDefaultMajorFactorById(row.majorDictId || row.id)
|
||||
}))
|
||||
}
|
||||
|
||||
const applyProjectCountChange = async (nextValue: unknown) => {
|
||||
const normalized = normalizeProjectCount(nextValue)
|
||||
projectCount.value = normalized
|
||||
if (!isMutipleService.value) return
|
||||
const previousRows = detailRows.value.map(row => ({ ...row }))
|
||||
const previousProjectCount = inferProjectCountFromRows(previousRows)
|
||||
if (normalized === previousProjectCount) return
|
||||
|
||||
if (normalized < previousProjectCount) {
|
||||
detailRows.value = isOnlyCostScaleService.value
|
||||
? buildOnlyCostScaleRows(previousRows as any, { projectCount: normalized })
|
||||
: mergeWithDictRows(previousRows as any, { projectCount: normalized })
|
||||
syncComputedValuesToDetailRows()
|
||||
await saveToIndexedDB()
|
||||
return
|
||||
}
|
||||
|
||||
const defaultRows = await buildRowsFromImportDefaultSource(normalized)
|
||||
const existingMap = new Map<string, DetailRow>()
|
||||
for (const row of previousRows) {
|
||||
const key = getProjectMajorKeyFromRow(row)
|
||||
if (!key) continue
|
||||
existingMap.set(key, row)
|
||||
}
|
||||
|
||||
detailRows.value = defaultRows.map(defaultRow => {
|
||||
const key = getProjectMajorKeyFromRow(defaultRow)
|
||||
const existingRow = key ? existingMap.get(key) : undefined
|
||||
if (!existingRow) return defaultRow
|
||||
if (resolveRowProjectIndex(existingRow) > previousProjectCount) return defaultRow
|
||||
return {
|
||||
...defaultRow,
|
||||
...existingRow,
|
||||
id: defaultRow.id,
|
||||
projectIndex: defaultRow.projectIndex,
|
||||
majorDictId: defaultRow.majorDictId,
|
||||
groupCode: defaultRow.groupCode,
|
||||
groupName: defaultRow.groupName,
|
||||
majorCode: defaultRow.majorCode,
|
||||
majorName: defaultRow.majorName,
|
||||
hasCost: defaultRow.hasCost,
|
||||
hasArea: defaultRow.hasArea,
|
||||
path: defaultRow.path
|
||||
}
|
||||
})
|
||||
syncComputedValuesToDetailRows()
|
||||
await saveToIndexedDB()
|
||||
}
|
||||
|
||||
const loadFromIndexedDB = async () => {
|
||||
try {
|
||||
const baseInfo = await localforage.getItem<XmBaseInfoState>(BASE_INFO_KEY)
|
||||
activeIndustryCode.value =
|
||||
typeof baseInfo?.projectIndustry === 'string' ? baseInfo.projectIndustry.trim() : ''
|
||||
projectCount.value = 1
|
||||
|
||||
|
||||
await ensureFactorDefaultsLoaded()
|
||||
const applyContractDefaultRows = async () => {
|
||||
const htData = await localforage.getItem<{ detailRows: SourceRow[] }>(HT_DB_KEY.value)
|
||||
const hasContractRows = Array.isArray(htData?.detailRows) && htData.detailRows.length > 0
|
||||
const targetProjectCount = getTargetProjectCount()
|
||||
if (isOnlyCostScaleService.value) {
|
||||
detailRows.value = hasContractRows ? buildOnlyCostScaleRows(htData!.detailRows as any) : buildOnlyCostScaleRows()
|
||||
detailRows.value = hasContractRows
|
||||
? buildOnlyCostScaleRows(htData!.detailRows as any, { projectCount: targetProjectCount, cloneFromProjectOne: true })
|
||||
: buildOnlyCostScaleRows(undefined, { projectCount: targetProjectCount })
|
||||
} else {
|
||||
detailRows.value = hasContractRows
|
||||
? mergeWithDictRows(htData!.detailRows, { includeFactorValues: true })
|
||||
: buildDefaultRows().map(row => ({
|
||||
? mergeWithDictRows(htData!.detailRows, {
|
||||
includeFactorValues: true,
|
||||
projectCount: targetProjectCount,
|
||||
cloneFromProjectOne: true
|
||||
})
|
||||
: buildDefaultRows(targetProjectCount).map(row => ({
|
||||
...row,
|
||||
consultCategoryFactor: getDefaultConsultCategoryFactor(),
|
||||
majorFactor: getDefaultMajorFactorById(row.id)
|
||||
majorFactor: getDefaultMajorFactorById(row.majorDictId || row.id)
|
||||
}))
|
||||
}
|
||||
syncComputedValuesToDetailRows()
|
||||
@@ -974,9 +1152,18 @@ const loadFromIndexedDB = async () => {
|
||||
|
||||
const data = await localforage.getItem<XmInfoState>(DB_KEY.value)
|
||||
if (data) {
|
||||
if (isMutipleService.value) {
|
||||
projectCount.value = inferProjectCountFromRows(data.detailRows as any)
|
||||
}
|
||||
detailRows.value = isOnlyCostScaleService.value
|
||||
? buildOnlyCostScaleRows(data.detailRows as any)
|
||||
: mergeWithDictRows(data.detailRows)
|
||||
? buildOnlyCostScaleRows(data.detailRows as any, {
|
||||
projectCount: getTargetProjectCount(),
|
||||
cloneFromProjectOne: true
|
||||
})
|
||||
: mergeWithDictRows(data.detailRows as any, {
|
||||
projectCount: getTargetProjectCount(),
|
||||
cloneFromProjectOne: true
|
||||
})
|
||||
syncComputedValuesToDetailRows()
|
||||
return
|
||||
}
|
||||
@@ -984,7 +1171,9 @@ const loadFromIndexedDB = async () => {
|
||||
await applyContractDefaultRows()
|
||||
} catch (error) {
|
||||
console.error('loadFromIndexedDB failed:', error)
|
||||
detailRows.value = isOnlyCostScaleService.value ? buildOnlyCostScaleRows() : buildDefaultRows()
|
||||
detailRows.value = isOnlyCostScaleService.value
|
||||
? buildOnlyCostScaleRows(undefined, { projectCount: getTargetProjectCount() })
|
||||
: buildDefaultRows(getTargetProjectCount())
|
||||
syncComputedValuesToDetailRows()
|
||||
}
|
||||
}
|
||||
@@ -999,15 +1188,22 @@ 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
|
||||
const targetProjectCount = getTargetProjectCount()
|
||||
if (isOnlyCostScaleService.value) {
|
||||
detailRows.value = hasContractRows ? buildOnlyCostScaleRows(htData!.detailRows as any) : buildOnlyCostScaleRows()
|
||||
detailRows.value = hasContractRows
|
||||
? buildOnlyCostScaleRows(htData!.detailRows as any, { projectCount: targetProjectCount, cloneFromProjectOne: true })
|
||||
: buildOnlyCostScaleRows(undefined, { projectCount: targetProjectCount })
|
||||
} else {
|
||||
detailRows.value = hasContractRows
|
||||
? mergeWithDictRows(htData!.detailRows, { includeFactorValues: true })
|
||||
: buildDefaultRows().map(row => ({
|
||||
? mergeWithDictRows(htData!.detailRows, {
|
||||
includeFactorValues: true,
|
||||
projectCount: targetProjectCount,
|
||||
cloneFromProjectOne: true
|
||||
})
|
||||
: buildDefaultRows(targetProjectCount).map(row => ({
|
||||
...row,
|
||||
consultCategoryFactor: getDefaultConsultCategoryFactor(),
|
||||
majorFactor: getDefaultMajorFactorById(row.id)
|
||||
majorFactor: getDefaultMajorFactorById(row.majorDictId || row.id)
|
||||
}))
|
||||
}
|
||||
await saveToIndexedDB()
|
||||
@@ -1018,7 +1214,9 @@ const importContractData = async () => {
|
||||
|
||||
const clearAllData = async () => {
|
||||
try {
|
||||
detailRows.value = isOnlyCostScaleService.value ? buildOnlyCostScaleRows() : buildDefaultRows()
|
||||
detailRows.value = isOnlyCostScaleService.value
|
||||
? buildOnlyCostScaleRows(undefined, { projectCount: getTargetProjectCount() })
|
||||
: buildDefaultRows(getTargetProjectCount())
|
||||
await saveToIndexedDB()
|
||||
} catch (error) {
|
||||
console.error('clearAllData failed:', error)
|
||||
@@ -1039,29 +1237,7 @@ let persistTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
|
||||
let gridPersistTimer: ReturnType<typeof setTimeout> | null = null
|
||||
const applyOnlyCostScalePinnedValue = (field: string, rawValue: unknown) => {
|
||||
const parsedValue = parseNumberOrNull(rawValue, { precision: 3 })
|
||||
const current = detailRows.value[0]
|
||||
if (!current) {
|
||||
detailRows.value = [buildOnlyCostScaleRow(field === 'amount' ? parsedValue : null)]
|
||||
return
|
||||
}
|
||||
if (
|
||||
field !== 'amount' &&
|
||||
field !== 'consultCategoryFactor' &&
|
||||
field !== 'majorFactor' &&
|
||||
field !== 'workStageFactor' &&
|
||||
field !== 'workRatio'
|
||||
) {
|
||||
return
|
||||
}
|
||||
detailRows.value = [{ ...current, [field]: parsedValue }]
|
||||
}
|
||||
|
||||
const handleCellValueChanged = (event?: any) => {
|
||||
if (isOnlyCostScaleService.value && event?.node?.rowPinned && typeof event.colDef?.field === 'string') {
|
||||
applyOnlyCostScalePinnedValue(event.colDef.field, event.newValue)
|
||||
}
|
||||
const handleCellValueChanged = () => {
|
||||
syncComputedValuesToDetailRows()
|
||||
if (gridPersistTimer) clearTimeout(gridPersistTimer)
|
||||
gridPersistTimer = setTimeout(() => {
|
||||
@@ -1112,10 +1288,16 @@ const processCellFromClipboard = (params: any) => {
|
||||
<h3 class="text-sm font-semibold text-foreground">投资规模明细</h3>
|
||||
<div v-if="isMutipleService" class="flex items-center gap-2">
|
||||
<span class="text-xs text-muted-foreground">项目数量</span>
|
||||
<NumberFieldRoot v-model="projectCount" :min="1" :step="1" class="inline-flex items-center rounded-md border bg-background">
|
||||
<NumberFieldDecrement class="px-2 py-1 text-xs text-muted-foreground hover:bg-muted">-</NumberFieldDecrement>
|
||||
<NumberFieldRoot
|
||||
v-model="projectCount"
|
||||
:min="1"
|
||||
:step="1"
|
||||
class="inline-flex items-center rounded-md border bg-background"
|
||||
@update:model-value="value => void applyProjectCountChange(value)"
|
||||
>
|
||||
<NumberFieldDecrement class="cursor-pointer px-2 py-1 text-xs text-muted-foreground hover:bg-muted">-</NumberFieldDecrement>
|
||||
<NumberFieldInput class="h-7 w-14 border-x bg-transparent px-2 text-center text-xs outline-none" />
|
||||
<NumberFieldIncrement class="px-2 py-1 text-xs text-muted-foreground hover:bg-muted">+</NumberFieldIncrement>
|
||||
<NumberFieldIncrement class="cursor-pointer px-2 py-1 text-xs text-muted-foreground hover:bg-muted">+</NumberFieldIncrement>
|
||||
</NumberFieldRoot>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1168,7 +1350,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="visibleDetailRows" :pinnedTopRowData="pinnedTopRowData"
|
||||
<AgGridVue :style="{ height: '100%' }" :rowData="detailRows" :pinnedTopRowData="pinnedTopRowData"
|
||||
:columnDefs="columnDefs" :autoGroupColumnDef="autoGroupColumnDef" :gridOptions="gridOptions" :theme="myTheme"
|
||||
@cell-value-changed="handleCellValueChanged" :suppressColumnVirtualisation="true"
|
||||
:suppressRowVirtualisation="true" :cellSelection="{ handle: { mode: 'range' } }" :enableClipboard="true"
|
||||
|
||||
Reference in new issue
Block a user