From ab310b49e9f9887450a2947cbaba740829b5d929 Mon Sep 17 00:00:00 2001 From: wintsa <770775984@qq.com> Date: Mon, 9 Mar 2026 15:44:56 +0800 Subject: [PATCH] 1 --- src/components/common/HtFeeGrid.vue | 2 +- .../InvestmentScalePricingPane.vue | 564 ++++++++++++------ .../pricingView/LandScalePricingPane.vue | 294 +++++++-- src/layout/tab.vue | 168 ++++-- src/lib/pricingMethodTotals.ts | 55 +- src/sql.ts | 9 +- 6 files changed, 765 insertions(+), 327 deletions(-) diff --git a/src/components/common/HtFeeGrid.vue b/src/components/common/HtFeeGrid.vue index 9b2b6f7..fbb27b6 100644 --- a/src/components/common/HtFeeGrid.vue +++ b/src/components/common/HtFeeGrid.vue @@ -186,7 +186,7 @@ const columnDefs: ColDef[] = [ headerClass: 'ag-right-aligned-header', cellClass: 'ag-right-aligned-cell editable-cell-line', editable: true, - valueParser: params => parseNumberOrNull(params.newValue, { precision: 3 }), + valueParser: params => parseNumberOrNull(params.newValue, { precision: 2 }), valueFormatter: formatEditableUnitPrice, cellClassRules: { 'editable-cell-empty': params => params.value == null || params.value === '' diff --git a/src/components/views/pricingView/InvestmentScalePricingPane.vue b/src/components/views/pricingView/InvestmentScalePricingPane.vue index b1a358b..291af29 100644 --- a/src/components/views/pricingView/InvestmentScalePricingPane.vue +++ b/src/components/views/pricingView/InvestmentScalePricingPane.vue @@ -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(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 | 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 | 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>) => { + 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 & Pick> + rowsFromDb?: Array & Pick>, + 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 & Pick>() + 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 & Partial< Pick< DetailRow, + | 'projectIndex' + | 'majorDictId' | 'amount' | 'benchmarkBudget' | 'benchmarkBudgetBasic' @@ -377,22 +490,32 @@ type SourceRow = Pick & > 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() 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 | ColGroupDef> = [ 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 | ColGroupDef> = [ 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 | ColGroupDef> = [ 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 | ColGroupDef> = [ 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 | ColGroupDef> = [ 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 | ColGroupDef> = [ 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 | ColGroupDef> = [ 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 | ColGroupDef> = [ 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 | undefined) => { + if (!row) return '' + const majorDictId = resolveRowMajorDictId(row) + if (!majorDictId) return '' + return makeProjectMajorKey(resolveRowProjectIndex(row), majorDictId) +} + +const buildRowsFromImportDefaultSource = async ( + targetProjectCount: number +): Promise => { + // 与“使用默认数据”同源:先强制刷新系数,再按合同卡片默认带出。 + 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() + 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(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(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 | null = null let gridPersistTimer: ReturnType | 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) => {

投资规模明细

项目数量 - - - + + - - + + +
@@ -1168,7 +1350,7 @@ const processCellFromClipboard = (params: any) => {
- { return service?.mutiple === true }) const projectCount = ref(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 | 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 | 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>) => { + if (!isMutipleService.value) return 1 + let maxProjectIndex = 1 + for (const row of rows || []) { + maxProjectIndex = Math.max(maxProjectIndex, resolveRowProjectIndex(row)) + } + return maxProjectIndex +} const detailRows = ref([]) const getDefaultConsultCategoryFactor = () => @@ -231,39 +291,46 @@ 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, - landArea: 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, + landArea: 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 @@ -273,6 +340,8 @@ type SourceRow = Pick & Partial< Pick< DetailRow, + | 'projectIndex' + | 'majorDictId' | 'amount' | 'landArea' | 'benchmarkBudget' @@ -294,22 +363,32 @@ type SourceRow = Pick & > const mergeWithDictRows = ( rowsFromDb: SourceRow[] | undefined, - options?: { includeScaleValues?: boolean; includeFactorValues?: boolean } + options?: { + includeScaleValues?: boolean + includeFactorValues?: boolean + projectCount?: number + cloneFromProjectOne?: boolean + } ): DetailRow[] => { const includeScaleValues = options?.includeScaleValues ?? true const includeFactorValues = options?.includeFactorValues ?? true + const targetProjectCount = normalizeProjectCount(options?.projectCount ?? getTargetProjectCount()) const dbValueMap = new Map() 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') @@ -342,7 +421,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, @@ -688,12 +767,24 @@ 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 } } @@ -794,22 +885,102 @@ const saveToIndexedDB = async () => { } } +const getProjectMajorKeyFromRow = (row: Partial | undefined) => { + if (!row) return '' + const majorDictId = resolveRowMajorDictId(row) + if (!majorDictId) return '' + return makeProjectMajorKey(resolveRowProjectIndex(row), majorDictId) +} + +const buildRowsFromImportDefaultSource = async ( + targetProjectCount: number +): Promise => { + // 与“使用默认数据”同源:先强制刷新系数,再按合同卡片默认带出。 + await loadFactorDefaults() + const htData = await localforage.getItem<{ detailRows: SourceRow[] }>(HT_DB_KEY.value) + const hasContractRows = Array.isArray(htData?.detailRows) && htData.detailRows.length > 0 + 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 = mergeWithDictRows(previousRows as any, { projectCount: normalized }) + syncComputedValuesToDetailRows() + await saveToIndexedDB() + return + } + + const defaultRows = await buildRowsFromImportDefaultSource(normalized) + const existingMap = new Map() + 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(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() 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() } @@ -820,7 +991,13 @@ const loadFromIndexedDB = async () => { const data = await localforage.getItem(DB_KEY.value) if (data) { - detailRows.value = mergeWithDictRows(data.detailRows) + if (isMutipleService.value) { + projectCount.value = inferProjectCountFromRows(data.detailRows as any) + } + detailRows.value = mergeWithDictRows(data.detailRows as any, { + projectCount: getTargetProjectCount(), + cloneFromProjectOne: true + }) syncComputedValuesToDetailRows() return } @@ -828,7 +1005,7 @@ const loadFromIndexedDB = async () => { await applyContractDefaultRows() } catch (error) { console.error('loadFromIndexedDB failed:', error) - detailRows.value = buildDefaultRows() + detailRows.value = buildDefaultRows(getTargetProjectCount()) syncComputedValuesToDetailRows() } } @@ -843,12 +1020,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 + const targetProjectCount = getTargetProjectCount() 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() } catch (error) { @@ -858,7 +1040,7 @@ const importContractData = async () => { const clearAllData = async () => { try { - detailRows.value = buildDefaultRows() + detailRows.value = buildDefaultRows(getTargetProjectCount()) await saveToIndexedDB() } catch (error) { console.error('clearAllData failed:', error) @@ -930,10 +1112,16 @@ const processCellFromClipboard = (params: any) => {

用地规模明细

项目数量 - - - + + - - + + +
diff --git a/src/layout/tab.vue b/src/layout/tab.vue index 744461e..ed41b35 100644 --- a/src/layout/tab.vue +++ b/src/layout/tab.vue @@ -371,6 +371,7 @@ const tabScrollAreaRef = ref(null) const showTabScrollLeft = ref(false) const showTabScrollRight = ref(false) const isTabStripHover = ref(false) +const isTabDragging = ref(false) const tabTitleOverflowMap = ref>({}) let tabStripViewportEl: HTMLElement | null = null let tabTitleOverflowRafId: number | null = null @@ -544,6 +545,14 @@ const canMoveTab = (event: any) => { return true } +const handleTabDragStart = () => { + isTabDragging.value = true +} + +const handleTabDragEnd = () => { + isTabDragging.value = false +} + const setTabItemRef = (id: string, el: Element | ComponentPublicInstance | null) => { if (el instanceof HTMLElement) { tabItemElMap.set(id, el) @@ -1341,9 +1350,9 @@ watch(