Compare commits
2 Commits
d1dda7f9fa
...
f79e8e0da6
| Author | SHA1 | Date | |
|---|---|---|---|
| f79e8e0da6 | |||
| ab310b49e9 |
@ -186,7 +186,7 @@ const columnDefs: ColDef<FeeRow>[] = [
|
|||||||
headerClass: 'ag-right-aligned-header',
|
headerClass: 'ag-right-aligned-header',
|
||||||
cellClass: 'ag-right-aligned-cell editable-cell-line',
|
cellClass: 'ag-right-aligned-cell editable-cell-line',
|
||||||
editable: true,
|
editable: true,
|
||||||
valueParser: params => parseNumberOrNull(params.newValue, { precision: 3 }),
|
valueParser: params => parseNumberOrNull(params.newValue, { precision: 2 }),
|
||||||
valueFormatter: formatEditableUnitPrice,
|
valueFormatter: formatEditableUnitPrice,
|
||||||
cellClassRules: {
|
cellClassRules: {
|
||||||
'editable-cell-empty': params => params.value == null || params.value === ''
|
'editable-cell-empty': params => params.value == null || params.value === ''
|
||||||
|
|||||||
@ -49,6 +49,8 @@ interface DictGroup {
|
|||||||
|
|
||||||
interface DetailRow {
|
interface DetailRow {
|
||||||
id: string
|
id: string
|
||||||
|
projectIndex?: number
|
||||||
|
majorDictId?: string
|
||||||
groupCode: string
|
groupCode: string
|
||||||
groupName: string
|
groupName: string
|
||||||
majorCode: string
|
majorCode: string
|
||||||
@ -125,6 +127,64 @@ const isMutipleService = computed(() => {
|
|||||||
return service?.mutiple === true
|
return service?.mutiple === true
|
||||||
})
|
})
|
||||||
const projectCount = ref<number>(1)
|
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 totalLabel = computed(() => {
|
||||||
const industryName = industryNameMap.get(activeIndustryCode.value.trim()) || ''
|
const industryName = industryNameMap.get(activeIndustryCode.value.trim()) || ''
|
||||||
return industryName ? `${industryName}总投资` : '总投资'
|
return industryName ? `${industryName}总投资` : '总投资'
|
||||||
@ -246,14 +306,18 @@ for (const group of detailDict) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const buildDefaultRows = (): DetailRow[] => {
|
const buildDefaultRows = (projectCountValue = getTargetProjectCount()): DetailRow[] => {
|
||||||
if (!activeIndustryCode.value) return []
|
if (!activeIndustryCode.value) return []
|
||||||
const rows: DetailRow[] = []
|
const rows: DetailRow[] = []
|
||||||
|
for (let projectIndex = 1; projectIndex <= projectCountValue; projectIndex++) {
|
||||||
for (const group of detailDict) {
|
for (const group of detailDict) {
|
||||||
if (activeIndustryCode.value && !isMajorIdInIndustryScope(group.id, activeIndustryCode.value)) continue
|
if (activeIndustryCode.value && !isMajorIdInIndustryScope(group.id, activeIndustryCode.value)) continue
|
||||||
for (const child of group.children) {
|
for (const child of group.children) {
|
||||||
|
const rowId = buildScopedRowId(projectIndex, child.id)
|
||||||
rows.push({
|
rows.push({
|
||||||
id: child.id,
|
id: rowId,
|
||||||
|
projectIndex,
|
||||||
|
majorDictId: child.id,
|
||||||
groupCode: group.code,
|
groupCode: group.code,
|
||||||
groupName: group.name,
|
groupName: group.name,
|
||||||
majorCode: child.code,
|
majorCode: child.code,
|
||||||
@ -276,25 +340,36 @@ const buildDefaultRows = (): DetailRow[] => {
|
|||||||
budgetFeeBasic: null,
|
budgetFeeBasic: null,
|
||||||
budgetFeeOptional: null,
|
budgetFeeOptional: null,
|
||||||
remark: '',
|
remark: '',
|
||||||
path: [group.id, child.id]
|
path: isMutipleService.value
|
||||||
|
? [buildProjectGroupPathKey(projectIndex), group.id, rowId]
|
||||||
|
: [group.id, rowId]
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return rows
|
return rows
|
||||||
}
|
}
|
||||||
|
|
||||||
const calcOnlyCostScaleAmountFromRows = (rows?: Array<{ amount?: unknown }>) =>
|
const calcOnlyCostScaleAmountFromRows = (rows?: Array<{ amount?: unknown }>) =>
|
||||||
sumByNumber(rows || [], row => (typeof row?.amount === 'number' ? row.amount : null))
|
sumByNumber(rows || [], row => (typeof row?.amount === 'number' ? row.amount : null))
|
||||||
|
|
||||||
const getOnlyCostScaleMajorFactorDefault = () => {
|
const getOnlyCostScaleMajorEntry = () => {
|
||||||
const industryId = String(activeIndustryCode.value || '').trim()
|
const industryId = String(activeIndustryCode.value || '').trim()
|
||||||
if (!industryId) return 1
|
if (!industryId) return null
|
||||||
const industryMajor = serviceEntries.find(([, item]) => {
|
const industryMajor = serviceEntries.find(([, item]) => {
|
||||||
const majorIndustryId = String(item?.industryId ?? '').trim()
|
const majorIndustryId = String(item?.industryId ?? '').trim()
|
||||||
return majorIndustryId === industryId && !String(item?.code || '').includes('-')
|
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
|
if (!industryMajor) return 1
|
||||||
const [majorId, majorItem] = industryMajor
|
const majorId = industryMajor.id
|
||||||
|
const majorItem = industryMajor.item
|
||||||
const fromMap = majorFactorMap.value.get(String(majorId))
|
const fromMap = majorFactorMap.value.get(String(majorId))
|
||||||
if (typeof fromMap === 'number' && Number.isFinite(fromMap)) return fromMap
|
if (typeof fromMap === 'number' && Number.isFinite(fromMap)) return fromMap
|
||||||
if (typeof majorItem?.defCoe === 'number' && Number.isFinite(majorItem.defCoe)) return majorItem.defCoe
|
if (typeof majorItem?.defCoe === 'number' && Number.isFinite(majorItem.defCoe)) return majorItem.defCoe
|
||||||
@ -303,6 +378,7 @@ const getOnlyCostScaleMajorFactorDefault = () => {
|
|||||||
|
|
||||||
const buildOnlyCostScaleRow = (
|
const buildOnlyCostScaleRow = (
|
||||||
amount: number | null,
|
amount: number | null,
|
||||||
|
projectIndex: number,
|
||||||
fromDb?: Partial<
|
fromDb?: Partial<
|
||||||
Pick<
|
Pick<
|
||||||
DetailRow,
|
DetailRow,
|
||||||
@ -316,11 +392,13 @@ const buildOnlyCostScaleRow = (
|
|||||||
>
|
>
|
||||||
>
|
>
|
||||||
): DetailRow => ({
|
): DetailRow => ({
|
||||||
id: ONLY_COST_SCALE_ROW_ID,
|
id: buildScopedRowId(projectIndex, getOnlyCostScaleMajorEntry()?.id || ONLY_COST_SCALE_ROW_ID),
|
||||||
groupCode: 'TOTAL',
|
projectIndex,
|
||||||
groupName: '总投资',
|
majorDictId: getOnlyCostScaleMajorEntry()?.id || ONLY_COST_SCALE_ROW_ID,
|
||||||
majorCode: 'TOTAL',
|
groupCode: getOnlyCostScaleMajorEntry()?.item?.code || 'TOTAL',
|
||||||
majorName: '总投资',
|
groupName: getOnlyCostScaleMajorEntry()?.item?.name || totalLabel.value,
|
||||||
|
majorCode: getOnlyCostScaleMajorEntry()?.item?.code || 'TOTAL',
|
||||||
|
majorName: getOnlyCostScaleMajorEntry()?.item?.name || totalLabel.value,
|
||||||
hasCost: true,
|
hasCost: true,
|
||||||
hasArea: false,
|
hasArea: false,
|
||||||
amount,
|
amount,
|
||||||
@ -342,21 +420,56 @@ const buildOnlyCostScaleRow = (
|
|||||||
budgetFeeBasic: null,
|
budgetFeeBasic: null,
|
||||||
budgetFeeOptional: null,
|
budgetFeeOptional: null,
|
||||||
remark: typeof fromDb?.remark === 'string' ? fromDb.remark : '',
|
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 = (
|
const buildOnlyCostScaleRows = (
|
||||||
rowsFromDb?: Array<Partial<DetailRow> & Pick<DetailRow, 'id'>>
|
rowsFromDb?: Array<Partial<DetailRow> & Pick<DetailRow, 'id'>>,
|
||||||
|
options?: { projectCount?: number; cloneFromProjectOne?: boolean }
|
||||||
): DetailRow[] => {
|
): DetailRow[] => {
|
||||||
const totalAmount = calcOnlyCostScaleAmountFromRows(rowsFromDb)
|
const targetProjectCount = normalizeProjectCount(options?.projectCount ?? getTargetProjectCount())
|
||||||
const onlyRow = rowsFromDb?.find(row => String(row.id) === ONLY_COST_SCALE_ROW_ID)
|
const onlyCostMajorId = getOnlyCostScaleMajorEntry()?.id || ONLY_COST_SCALE_ROW_ID
|
||||||
return [buildOnlyCostScaleRow(totalAmount, onlyRow)]
|
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'> &
|
type SourceRow = Pick<DetailRow, 'id'> &
|
||||||
Partial<
|
Partial<
|
||||||
Pick<
|
Pick<
|
||||||
DetailRow,
|
DetailRow,
|
||||||
|
| 'projectIndex'
|
||||||
|
| 'majorDictId'
|
||||||
| 'amount'
|
| 'amount'
|
||||||
| 'benchmarkBudget'
|
| 'benchmarkBudget'
|
||||||
| 'benchmarkBudgetBasic'
|
| 'benchmarkBudgetBasic'
|
||||||
@ -377,22 +490,32 @@ type SourceRow = Pick<DetailRow, 'id'> &
|
|||||||
>
|
>
|
||||||
const mergeWithDictRows = (
|
const mergeWithDictRows = (
|
||||||
rowsFromDb: SourceRow[] | undefined,
|
rowsFromDb: SourceRow[] | undefined,
|
||||||
options?: { includeAmount?: boolean; includeFactorValues?: boolean }
|
options?: {
|
||||||
|
includeAmount?: boolean
|
||||||
|
includeFactorValues?: boolean
|
||||||
|
projectCount?: number
|
||||||
|
cloneFromProjectOne?: boolean
|
||||||
|
}
|
||||||
): DetailRow[] => {
|
): DetailRow[] => {
|
||||||
const includeAmount = options?.includeAmount ?? true
|
const includeAmount = options?.includeAmount ?? true
|
||||||
const includeFactorValues = options?.includeFactorValues ?? true
|
const includeFactorValues = options?.includeFactorValues ?? true
|
||||||
|
const targetProjectCount = normalizeProjectCount(options?.projectCount ?? getTargetProjectCount())
|
||||||
const dbValueMap = new Map<string, SourceRow>()
|
const dbValueMap = new Map<string, SourceRow>()
|
||||||
for (const row of rowsFromDb || []) {
|
for (const row of rowsFromDb || []) {
|
||||||
const rowId = String(row.id)
|
const projectIndex = resolveRowProjectIndex(row)
|
||||||
dbValueMap.set(rowId, row)
|
const majorDictId = resolveRowMajorDictId(row)
|
||||||
const aliasId = majorIdAliasMap.get(rowId)
|
if (!majorDictId) continue
|
||||||
if (aliasId && !dbValueMap.has(aliasId)) {
|
dbValueMap.set(makeProjectMajorKey(projectIndex, majorDictId), row)
|
||||||
dbValueMap.set(aliasId, row)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return buildDefaultRows().map(row => {
|
return buildDefaultRows(targetProjectCount).map(row => {
|
||||||
const fromDb = dbValueMap.get(row.id)
|
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
|
if (!fromDb) return row
|
||||||
const hasConsultCategoryFactor = Object.prototype.hasOwnProperty.call(fromDb, 'consultCategoryFactor')
|
const hasConsultCategoryFactor = Object.prototype.hasOwnProperty.call(fromDb, 'consultCategoryFactor')
|
||||||
const hasMajorFactor = Object.prototype.hasOwnProperty.call(fromDb, 'majorFactor')
|
const hasMajorFactor = Object.prototype.hasOwnProperty.call(fromDb, 'majorFactor')
|
||||||
@ -424,7 +547,7 @@ const mergeWithDictRows = (
|
|||||||
? fromDb.majorFactor
|
? fromDb.majorFactor
|
||||||
: hasMajorFactor
|
: hasMajorFactor
|
||||||
? null
|
? null
|
||||||
: getDefaultMajorFactorById(row.id),
|
: getDefaultMajorFactorById(rowMajorDictId),
|
||||||
workStageFactor: typeof fromDb.workStageFactor === 'number' ? fromDb.workStageFactor : row.workStageFactor,
|
workStageFactor: typeof fromDb.workStageFactor === 'number' ? fromDb.workStageFactor : row.workStageFactor,
|
||||||
workRatio: typeof fromDb.workRatio === 'number' ? fromDb.workRatio : row.workRatio,
|
workRatio: typeof fromDb.workRatio === 'number' ? fromDb.workRatio : row.workRatio,
|
||||||
budgetFee: typeof fromDb.budgetFee === 'number' ? fromDb.budgetFee : null,
|
budgetFee: typeof fromDb.budgetFee === 'number' ? fromDb.budgetFee : null,
|
||||||
@ -452,11 +575,6 @@ const formatMajorFactor = (params: any) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const formatEditableMoney = (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) {
|
if (!params.node?.group && !params.node?.rowPinned && !params.data?.hasCost) {
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
@ -477,7 +595,7 @@ type BudgetCheckField = 'benchmarkBudgetBasicChecked' | 'benchmarkBudgetOptional
|
|||||||
const createBudgetCellRendererWithCheck = (checkField: BudgetCheckField) => (params: any) => {
|
const createBudgetCellRendererWithCheck = (checkField: BudgetCheckField) => (params: any) => {
|
||||||
const valueText = formatReadonlyMoney(params)
|
const valueText = formatReadonlyMoney(params)
|
||||||
const hasValue = params.value != null && params.value !== ''
|
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
|
return valueText
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -495,11 +613,7 @@ const createBudgetCellRendererWithCheck = (checkField: BudgetCheckField) => (par
|
|||||||
checkbox.checked = params.data[checkField] !== false
|
checkbox.checked = params.data[checkField] !== false
|
||||||
checkbox.addEventListener('click', event => event.stopPropagation())
|
checkbox.addEventListener('click', event => event.stopPropagation())
|
||||||
checkbox.addEventListener('change', () => {
|
checkbox.addEventListener('change', () => {
|
||||||
const isOnlyCostScalePinned = isOnlyCostScaleService.value && Boolean(params.node?.rowPinned)
|
const targetRow = params.data as DetailRow | undefined
|
||||||
const targetRow =
|
|
||||||
isOnlyCostScalePinned
|
|
||||||
? detailRows.value[0]
|
|
||||||
: (params.data as DetailRow | undefined)
|
|
||||||
if (!targetRow) return
|
if (!targetRow) return
|
||||||
|
|
||||||
targetRow[checkField] = checkbox.checked
|
targetRow[checkField] = checkbox.checked
|
||||||
@ -595,7 +709,6 @@ const getBudgetFeeSplit = (
|
|||||||
|
|
||||||
const getMergeColSpanBeforeTotal = (params: any) => {
|
const getMergeColSpanBeforeTotal = (params: any) => {
|
||||||
if (!params.node?.group && !params.node?.rowPinned) return 1
|
if (!params.node?.group && !params.node?.rowPinned) return 1
|
||||||
if (isOnlyCostScaleService.value && params.node?.rowPinned) return 1
|
|
||||||
const displayedColumns = params.api?.getAllDisplayedColumns?.()
|
const displayedColumns = params.api?.getAllDisplayedColumns?.()
|
||||||
if (!Array.isArray(displayedColumns) || !params.column) return 1
|
if (!Array.isArray(displayedColumns) || !params.column) return 1
|
||||||
const currentIndex = displayedColumns.findIndex((column: any) => column.getColId() === params.column.getColId())
|
const currentIndex = displayedColumns.findIndex((column: any) => column.getColId() === params.column.getColId())
|
||||||
@ -613,20 +726,15 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
|
|||||||
flex: 2,
|
flex: 2,
|
||||||
|
|
||||||
editable: params => {
|
editable: params => {
|
||||||
if (isOnlyCostScaleService.value) return Boolean(params.node?.rowPinned)
|
|
||||||
return !params.node?.group && !params.node?.rowPinned && Boolean(params.data?.hasCost)
|
return !params.node?.group && !params.node?.rowPinned && Boolean(params.data?.hasCost)
|
||||||
},
|
},
|
||||||
cellClass: params =>
|
cellClass: params =>
|
||||||
isOnlyCostScaleService.value && params.node?.rowPinned
|
!params.node?.group && !params.node?.rowPinned && params.data?.hasCost
|
||||||
? '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 editable-cell-line'
|
||||||
: 'ag-right-aligned-cell',
|
: 'ag-right-aligned-cell',
|
||||||
cellClassRules: {
|
cellClassRules: {
|
||||||
'editable-cell-empty': params =>
|
'editable-cell-empty': params =>
|
||||||
isOnlyCostScaleService.value && params.node?.rowPinned
|
!params.node?.group && !params.node?.rowPinned && Boolean(params.data?.hasCost) && (params.value == null || params.value === '')
|
||||||
? 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 }),
|
valueParser: params => parseNumberOrNull(params.newValue, { precision: 3 }),
|
||||||
valueFormatter: formatEditableMoney
|
valueFormatter: formatEditableMoney
|
||||||
@ -645,7 +753,7 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
|
|||||||
cellClass: 'ag-right-aligned-cell',
|
cellClass: 'ag-right-aligned-cell',
|
||||||
valueGetter: params =>
|
valueGetter: params =>
|
||||||
params.node?.rowPinned
|
params.node?.rowPinned
|
||||||
? (isOnlyCostScaleService.value ? getCheckedBenchmarkBudgetSplitByAmount(params.data)?.basic ?? null : null)
|
? null
|
||||||
: getCheckedBenchmarkBudgetSplitByAmount(params.data)?.basic ?? null,
|
: getCheckedBenchmarkBudgetSplitByAmount(params.data)?.basic ?? null,
|
||||||
cellRenderer: createBudgetCellRendererWithCheck('benchmarkBudgetBasicChecked'),
|
cellRenderer: createBudgetCellRendererWithCheck('benchmarkBudgetBasicChecked'),
|
||||||
valueFormatter: formatReadonlyMoney
|
valueFormatter: formatReadonlyMoney
|
||||||
@ -660,7 +768,7 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
|
|||||||
cellClass: 'ag-right-aligned-cell',
|
cellClass: 'ag-right-aligned-cell',
|
||||||
valueGetter: params =>
|
valueGetter: params =>
|
||||||
params.node?.rowPinned
|
params.node?.rowPinned
|
||||||
? (isOnlyCostScaleService.value ? getCheckedBenchmarkBudgetSplitByAmount(params.data)?.optional ?? null : null)
|
? null
|
||||||
: getCheckedBenchmarkBudgetSplitByAmount(params.data)?.optional ?? null,
|
: getCheckedBenchmarkBudgetSplitByAmount(params.data)?.optional ?? null,
|
||||||
cellRenderer: createBudgetCellRendererWithCheck('benchmarkBudgetOptionalChecked'),
|
cellRenderer: createBudgetCellRendererWithCheck('benchmarkBudgetOptionalChecked'),
|
||||||
valueFormatter: formatReadonlyMoney
|
valueFormatter: formatReadonlyMoney
|
||||||
@ -675,7 +783,7 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
|
|||||||
cellClass: 'ag-right-aligned-cell',
|
cellClass: 'ag-right-aligned-cell',
|
||||||
valueGetter: params =>
|
valueGetter: params =>
|
||||||
params.node?.rowPinned
|
params.node?.rowPinned
|
||||||
? (isOnlyCostScaleService.value ? getCheckedBenchmarkBudgetSplitByAmount(params.data)?.total ?? null : null)
|
? null
|
||||||
: getCheckedBenchmarkBudgetSplitByAmount(params.data)?.total ?? null,
|
: getCheckedBenchmarkBudgetSplitByAmount(params.data)?.total ?? null,
|
||||||
valueFormatter: formatReadonlyMoney
|
valueFormatter: formatReadonlyMoney
|
||||||
}
|
}
|
||||||
@ -691,21 +799,14 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
|
|||||||
colId: 'consultCategoryFactor',
|
colId: 'consultCategoryFactor',
|
||||||
minWidth: 80,
|
minWidth: 80,
|
||||||
flex: 1,
|
flex: 1,
|
||||||
editable: params =>
|
editable: params => !params.node?.group && !params.node?.rowPinned,
|
||||||
isOnlyCostScaleService.value
|
|
||||||
? Boolean(params.node?.rowPinned)
|
|
||||||
: !params.node?.group && !params.node?.rowPinned,
|
|
||||||
cellClass: params =>
|
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'
|
? 'editable-cell-line'
|
||||||
: '',
|
: '',
|
||||||
cellClassRules: {
|
cellClassRules: {
|
||||||
'editable-cell-empty': params =>
|
'editable-cell-empty': params =>
|
||||||
isOnlyCostScaleService.value && params.node?.rowPinned
|
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
|
||||||
? params.value == null || params.value === ''
|
|
||||||
: !params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
|
|
||||||
},
|
},
|
||||||
valueParser: params => parseNumberOrNull(params.newValue, { precision: 3 }),
|
valueParser: params => parseNumberOrNull(params.newValue, { precision: 3 }),
|
||||||
valueFormatter: formatConsultCategoryFactor
|
valueFormatter: formatConsultCategoryFactor
|
||||||
@ -716,21 +817,14 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
|
|||||||
colId: 'majorFactor',
|
colId: 'majorFactor',
|
||||||
minWidth: 80,
|
minWidth: 80,
|
||||||
flex: 1,
|
flex: 1,
|
||||||
editable: params =>
|
editable: params => !params.node?.group && !params.node?.rowPinned,
|
||||||
isOnlyCostScaleService.value
|
|
||||||
? Boolean(params.node?.rowPinned)
|
|
||||||
: !params.node?.group && !params.node?.rowPinned,
|
|
||||||
cellClass: params =>
|
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'
|
? 'editable-cell-line'
|
||||||
: '',
|
: '',
|
||||||
cellClassRules: {
|
cellClassRules: {
|
||||||
'editable-cell-empty': params =>
|
'editable-cell-empty': params =>
|
||||||
isOnlyCostScaleService.value && params.node?.rowPinned
|
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
|
||||||
? params.value == null || params.value === ''
|
|
||||||
: !params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
|
|
||||||
},
|
},
|
||||||
valueParser: params => parseNumberOrNull(params.newValue, { precision: 3 }),
|
valueParser: params => parseNumberOrNull(params.newValue, { precision: 3 }),
|
||||||
valueFormatter: formatMajorFactor
|
valueFormatter: formatMajorFactor
|
||||||
@ -741,21 +835,14 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
|
|||||||
colId: 'workStageFactor',
|
colId: 'workStageFactor',
|
||||||
minWidth: 80,
|
minWidth: 80,
|
||||||
flex: 1,
|
flex: 1,
|
||||||
editable: params =>
|
editable: params => !params.node?.group && !params.node?.rowPinned,
|
||||||
isOnlyCostScaleService.value
|
|
||||||
? Boolean(params.node?.rowPinned)
|
|
||||||
: !params.node?.group && !params.node?.rowPinned,
|
|
||||||
cellClass: params =>
|
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'
|
? 'editable-cell-line'
|
||||||
: '',
|
: '',
|
||||||
cellClassRules: {
|
cellClassRules: {
|
||||||
'editable-cell-empty': params =>
|
'editable-cell-empty': params =>
|
||||||
isOnlyCostScaleService.value && params.node?.rowPinned
|
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
|
||||||
? params.value == null || params.value === ''
|
|
||||||
: !params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
|
|
||||||
},
|
},
|
||||||
valueParser: params => parseNumberOrNull(params.newValue, { precision: 3 }),
|
valueParser: params => parseNumberOrNull(params.newValue, { precision: 3 }),
|
||||||
valueFormatter: formatEditableNumber
|
valueFormatter: formatEditableNumber
|
||||||
@ -766,21 +853,14 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
|
|||||||
colId: 'workRatio',
|
colId: 'workRatio',
|
||||||
minWidth: 80,
|
minWidth: 80,
|
||||||
flex: 1,
|
flex: 1,
|
||||||
editable: params =>
|
editable: params => !params.node?.group && !params.node?.rowPinned,
|
||||||
isOnlyCostScaleService.value
|
|
||||||
? Boolean(params.node?.rowPinned)
|
|
||||||
: !params.node?.group && !params.node?.rowPinned,
|
|
||||||
cellClass: params =>
|
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'
|
? 'editable-cell-line'
|
||||||
: '',
|
: '',
|
||||||
cellClassRules: {
|
cellClassRules: {
|
||||||
'editable-cell-empty': params =>
|
'editable-cell-empty': params =>
|
||||||
isOnlyCostScaleService.value && params.node?.rowPinned
|
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
|
||||||
? params.value == null || params.value === ''
|
|
||||||
: !params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
|
|
||||||
},
|
},
|
||||||
valueParser: params => parseNumberOrNull(params.newValue, { precision: 3 }),
|
valueParser: params => parseNumberOrNull(params.newValue, { precision: 3 }),
|
||||||
valueFormatter: formatEditableNumber
|
valueFormatter: formatEditableNumber
|
||||||
@ -839,27 +919,36 @@ const autoGroupColumnDef: ColDef = {
|
|||||||
if (params.node?.rowPinned) {
|
if (params.node?.rowPinned) {
|
||||||
return totalLabel.value
|
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 nodeId = String(params.value || '')
|
||||||
|
const projectIndex = parseProjectIndexFromPathKey(nodeId)
|
||||||
|
if (projectIndex != null) return `项目${projectIndex}`
|
||||||
return idLabelMap.get(nodeId) || nodeId
|
return idLabelMap.get(nodeId) || nodeId
|
||||||
},
|
},
|
||||||
tooltipValueGetter: params => {
|
tooltipValueGetter: params => {
|
||||||
if (params.node?.rowPinned) return totalLabel.value
|
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 nodeId = String(params.value || '')
|
||||||
|
const projectIndex = parseProjectIndexFromPathKey(nodeId)
|
||||||
|
if (projectIndex != null) return `项目${projectIndex}`
|
||||||
return idLabelMap.get(nodeId) || nodeId
|
return idLabelMap.get(nodeId) || nodeId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const totalAmount = computed(() => sumByNumber(detailRows.value, row => row.amount))
|
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 totalBudgetFeeBasic = computed(() => sumByNumber(detailRows.value, row => getBudgetFeeSplit(row)?.basic))
|
||||||
const totalBudgetFeeOptional = computed(() => sumByNumber(detailRows.value, row => getBudgetFeeSplit(row)?.optional))
|
const totalBudgetFeeOptional = computed(() => sumByNumber(detailRows.value, row => getBudgetFeeSplit(row)?.optional))
|
||||||
const totalBudgetFee = computed(() => sumByNumber(detailRows.value, row => getBudgetFee(row)))
|
const totalBudgetFee = computed(() => sumByNumber(detailRows.value, row => getBudgetFee(row)))
|
||||||
const pinnedTopRowData = computed(() => [
|
const pinnedTopRowData = computed(() => {
|
||||||
|
return [
|
||||||
{
|
{
|
||||||
id: 'pinned-total-row',
|
id: 'pinned-total-row',
|
||||||
groupCode: '',
|
groupCode: '',
|
||||||
@ -868,25 +957,26 @@ const pinnedTopRowData = computed(() => [
|
|||||||
majorName: '',
|
majorName: '',
|
||||||
hasCost: false,
|
hasCost: false,
|
||||||
hasArea: false,
|
hasArea: false,
|
||||||
amount: isOnlyCostScaleService.value ? onlyCostScaleSourceRow.value.amount : null,
|
amount: null,
|
||||||
benchmarkBudget: null,
|
benchmarkBudget: null,
|
||||||
benchmarkBudgetBasic: null,
|
benchmarkBudgetBasic: null,
|
||||||
benchmarkBudgetOptional: null,
|
benchmarkBudgetOptional: null,
|
||||||
benchmarkBudgetBasicChecked: isOnlyCostScaleService.value ? onlyCostScaleSourceRow.value.benchmarkBudgetBasicChecked !== false : true,
|
benchmarkBudgetBasicChecked: true,
|
||||||
benchmarkBudgetOptionalChecked: isOnlyCostScaleService.value ? onlyCostScaleSourceRow.value.benchmarkBudgetOptionalChecked !== false : true,
|
benchmarkBudgetOptionalChecked: true,
|
||||||
basicFormula: '',
|
basicFormula: '',
|
||||||
optionalFormula: '',
|
optionalFormula: '',
|
||||||
consultCategoryFactor: isOnlyCostScaleService.value ? onlyCostScaleSourceRow.value.consultCategoryFactor : null,
|
consultCategoryFactor: null,
|
||||||
majorFactor: isOnlyCostScaleService.value ? onlyCostScaleSourceRow.value.majorFactor : null,
|
majorFactor: null,
|
||||||
workStageFactor: isOnlyCostScaleService.value ? onlyCostScaleSourceRow.value.workStageFactor : null,
|
workStageFactor: null,
|
||||||
workRatio: isOnlyCostScaleService.value ? onlyCostScaleSourceRow.value.workRatio : null,
|
workRatio: null,
|
||||||
budgetFee: totalBudgetFee.value,
|
budgetFee: totalBudgetFee.value,
|
||||||
budgetFeeBasic: totalBudgetFeeBasic.value,
|
budgetFeeBasic: totalBudgetFeeBasic.value,
|
||||||
budgetFeeOptional: totalBudgetFeeOptional.value,
|
budgetFeeOptional: totalBudgetFeeOptional.value,
|
||||||
remark: '',
|
remark: '',
|
||||||
path: ['TOTAL']
|
path: ['TOTAL']
|
||||||
}
|
}
|
||||||
])
|
]
|
||||||
|
})
|
||||||
|
|
||||||
const syncComputedValuesToDetailRows = () => {
|
const syncComputedValuesToDetailRows = () => {
|
||||||
for (const row of detailRows.value) {
|
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 () => {
|
const loadFromIndexedDB = async () => {
|
||||||
try {
|
try {
|
||||||
const baseInfo = await localforage.getItem<XmBaseInfoState>(BASE_INFO_KEY)
|
const baseInfo = await localforage.getItem<XmBaseInfoState>(BASE_INFO_KEY)
|
||||||
activeIndustryCode.value =
|
activeIndustryCode.value =
|
||||||
typeof baseInfo?.projectIndustry === 'string' ? baseInfo.projectIndustry.trim() : ''
|
typeof baseInfo?.projectIndustry === 'string' ? baseInfo.projectIndustry.trim() : ''
|
||||||
|
projectCount.value = 1
|
||||||
|
|
||||||
await ensureFactorDefaultsLoaded()
|
await ensureFactorDefaultsLoaded()
|
||||||
const applyContractDefaultRows = async () => {
|
const applyContractDefaultRows = async () => {
|
||||||
const htData = await localforage.getItem<{ detailRows: SourceRow[] }>(HT_DB_KEY.value)
|
const htData = await localforage.getItem<{ detailRows: SourceRow[] }>(HT_DB_KEY.value)
|
||||||
const hasContractRows = Array.isArray(htData?.detailRows) && htData.detailRows.length > 0
|
const hasContractRows = Array.isArray(htData?.detailRows) && htData.detailRows.length > 0
|
||||||
|
const targetProjectCount = getTargetProjectCount()
|
||||||
if (isOnlyCostScaleService.value) {
|
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 {
|
} else {
|
||||||
detailRows.value = hasContractRows
|
detailRows.value = hasContractRows
|
||||||
? mergeWithDictRows(htData!.detailRows, { includeFactorValues: true })
|
? mergeWithDictRows(htData!.detailRows, {
|
||||||
: buildDefaultRows().map(row => ({
|
includeFactorValues: true,
|
||||||
|
projectCount: targetProjectCount,
|
||||||
|
cloneFromProjectOne: true
|
||||||
|
})
|
||||||
|
: buildDefaultRows(targetProjectCount).map(row => ({
|
||||||
...row,
|
...row,
|
||||||
consultCategoryFactor: getDefaultConsultCategoryFactor(),
|
consultCategoryFactor: getDefaultConsultCategoryFactor(),
|
||||||
majorFactor: getDefaultMajorFactorById(row.id)
|
majorFactor: getDefaultMajorFactorById(row.majorDictId || row.id)
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
syncComputedValuesToDetailRows()
|
syncComputedValuesToDetailRows()
|
||||||
@ -974,9 +1152,18 @@ const loadFromIndexedDB = async () => {
|
|||||||
|
|
||||||
const data = await localforage.getItem<XmInfoState>(DB_KEY.value)
|
const data = await localforage.getItem<XmInfoState>(DB_KEY.value)
|
||||||
if (data) {
|
if (data) {
|
||||||
|
if (isMutipleService.value) {
|
||||||
|
projectCount.value = inferProjectCountFromRows(data.detailRows as any)
|
||||||
|
}
|
||||||
detailRows.value = isOnlyCostScaleService.value
|
detailRows.value = isOnlyCostScaleService.value
|
||||||
? buildOnlyCostScaleRows(data.detailRows as any)
|
? buildOnlyCostScaleRows(data.detailRows as any, {
|
||||||
: mergeWithDictRows(data.detailRows)
|
projectCount: getTargetProjectCount(),
|
||||||
|
cloneFromProjectOne: true
|
||||||
|
})
|
||||||
|
: mergeWithDictRows(data.detailRows as any, {
|
||||||
|
projectCount: getTargetProjectCount(),
|
||||||
|
cloneFromProjectOne: true
|
||||||
|
})
|
||||||
syncComputedValuesToDetailRows()
|
syncComputedValuesToDetailRows()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -984,7 +1171,9 @@ const loadFromIndexedDB = async () => {
|
|||||||
await applyContractDefaultRows()
|
await applyContractDefaultRows()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('loadFromIndexedDB failed:', error)
|
console.error('loadFromIndexedDB failed:', error)
|
||||||
detailRows.value = isOnlyCostScaleService.value ? buildOnlyCostScaleRows() : buildDefaultRows()
|
detailRows.value = isOnlyCostScaleService.value
|
||||||
|
? buildOnlyCostScaleRows(undefined, { projectCount: getTargetProjectCount() })
|
||||||
|
: buildDefaultRows(getTargetProjectCount())
|
||||||
syncComputedValuesToDetailRows()
|
syncComputedValuesToDetailRows()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -999,15 +1188,22 @@ const importContractData = async () => {
|
|||||||
await loadFactorDefaults()
|
await loadFactorDefaults()
|
||||||
const htData = await localforage.getItem<{ detailRows: SourceRow[] }>(HT_DB_KEY.value)
|
const htData = await localforage.getItem<{ detailRows: SourceRow[] }>(HT_DB_KEY.value)
|
||||||
const hasContractRows = Array.isArray(htData?.detailRows) && htData.detailRows.length > 0
|
const hasContractRows = Array.isArray(htData?.detailRows) && htData.detailRows.length > 0
|
||||||
|
const targetProjectCount = getTargetProjectCount()
|
||||||
if (isOnlyCostScaleService.value) {
|
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 {
|
} else {
|
||||||
detailRows.value = hasContractRows
|
detailRows.value = hasContractRows
|
||||||
? mergeWithDictRows(htData!.detailRows, { includeFactorValues: true })
|
? mergeWithDictRows(htData!.detailRows, {
|
||||||
: buildDefaultRows().map(row => ({
|
includeFactorValues: true,
|
||||||
|
projectCount: targetProjectCount,
|
||||||
|
cloneFromProjectOne: true
|
||||||
|
})
|
||||||
|
: buildDefaultRows(targetProjectCount).map(row => ({
|
||||||
...row,
|
...row,
|
||||||
consultCategoryFactor: getDefaultConsultCategoryFactor(),
|
consultCategoryFactor: getDefaultConsultCategoryFactor(),
|
||||||
majorFactor: getDefaultMajorFactorById(row.id)
|
majorFactor: getDefaultMajorFactorById(row.majorDictId || row.id)
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
await saveToIndexedDB()
|
await saveToIndexedDB()
|
||||||
@ -1018,7 +1214,9 @@ const importContractData = async () => {
|
|||||||
|
|
||||||
const clearAllData = async () => {
|
const clearAllData = async () => {
|
||||||
try {
|
try {
|
||||||
detailRows.value = isOnlyCostScaleService.value ? buildOnlyCostScaleRows() : buildDefaultRows()
|
detailRows.value = isOnlyCostScaleService.value
|
||||||
|
? buildOnlyCostScaleRows(undefined, { projectCount: getTargetProjectCount() })
|
||||||
|
: buildDefaultRows(getTargetProjectCount())
|
||||||
await saveToIndexedDB()
|
await saveToIndexedDB()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('clearAllData failed:', error)
|
console.error('clearAllData failed:', error)
|
||||||
@ -1039,29 +1237,7 @@ let persistTimer: ReturnType<typeof setTimeout> | null = null
|
|||||||
|
|
||||||
|
|
||||||
let gridPersistTimer: ReturnType<typeof setTimeout> | null = null
|
let gridPersistTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
const applyOnlyCostScalePinnedValue = (field: string, rawValue: unknown) => {
|
const handleCellValueChanged = () => {
|
||||||
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)
|
|
||||||
}
|
|
||||||
syncComputedValuesToDetailRows()
|
syncComputedValuesToDetailRows()
|
||||||
if (gridPersistTimer) clearTimeout(gridPersistTimer)
|
if (gridPersistTimer) clearTimeout(gridPersistTimer)
|
||||||
gridPersistTimer = setTimeout(() => {
|
gridPersistTimer = setTimeout(() => {
|
||||||
@ -1112,10 +1288,16 @@ const processCellFromClipboard = (params: any) => {
|
|||||||
<h3 class="text-sm font-semibold text-foreground">投资规模明细</h3>
|
<h3 class="text-sm font-semibold text-foreground">投资规模明细</h3>
|
||||||
<div v-if="isMutipleService" class="flex items-center gap-2">
|
<div v-if="isMutipleService" class="flex items-center gap-2">
|
||||||
<span class="text-xs text-muted-foreground">项目数量</span>
|
<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">
|
<NumberFieldRoot
|
||||||
<NumberFieldDecrement class="px-2 py-1 text-xs text-muted-foreground hover:bg-muted">-</NumberFieldDecrement>
|
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" />
|
<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>
|
</NumberFieldRoot>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -1168,7 +1350,7 @@ const processCellFromClipboard = (params: any) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="ag-theme-quartz h-full min-h-0 w-full flex-1">
|
<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"
|
:columnDefs="columnDefs" :autoGroupColumnDef="autoGroupColumnDef" :gridOptions="gridOptions" :theme="myTheme"
|
||||||
@cell-value-changed="handleCellValueChanged" :suppressColumnVirtualisation="true"
|
@cell-value-changed="handleCellValueChanged" :suppressColumnVirtualisation="true"
|
||||||
:suppressRowVirtualisation="true" :cellSelection="{ handle: { mode: 'range' } }" :enableClipboard="true"
|
:suppressRowVirtualisation="true" :cellSelection="{ handle: { mode: 'range' } }" :enableClipboard="true"
|
||||||
|
|||||||
@ -49,6 +49,8 @@ interface DictGroup {
|
|||||||
|
|
||||||
interface DetailRow {
|
interface DetailRow {
|
||||||
id: string
|
id: string
|
||||||
|
projectIndex?: number
|
||||||
|
majorDictId?: string
|
||||||
groupCode: string
|
groupCode: string
|
||||||
groupName: string
|
groupName: string
|
||||||
majorCode: string
|
majorCode: string
|
||||||
@ -119,6 +121,64 @@ const isMutipleService = computed(() => {
|
|||||||
return service?.mutiple === true
|
return service?.mutiple === true
|
||||||
})
|
})
|
||||||
const projectCount = ref<number>(1)
|
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 detailRows = ref<DetailRow[]>([])
|
const detailRows = ref<DetailRow[]>([])
|
||||||
const getDefaultConsultCategoryFactor = () =>
|
const getDefaultConsultCategoryFactor = () =>
|
||||||
@ -231,14 +291,18 @@ for (const group of detailDict) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const buildDefaultRows = (): DetailRow[] => {
|
const buildDefaultRows = (projectCountValue = getTargetProjectCount()): DetailRow[] => {
|
||||||
if (!activeIndustryCode.value) return []
|
if (!activeIndustryCode.value) return []
|
||||||
const rows: DetailRow[] = []
|
const rows: DetailRow[] = []
|
||||||
|
for (let projectIndex = 1; projectIndex <= projectCountValue; projectIndex++) {
|
||||||
for (const group of detailDict) {
|
for (const group of detailDict) {
|
||||||
if (activeIndustryCode.value && !isMajorIdInIndustryScope(group.id, activeIndustryCode.value)) continue
|
if (activeIndustryCode.value && !isMajorIdInIndustryScope(group.id, activeIndustryCode.value)) continue
|
||||||
for (const child of group.children) {
|
for (const child of group.children) {
|
||||||
|
const rowId = buildScopedRowId(projectIndex, child.id)
|
||||||
rows.push({
|
rows.push({
|
||||||
id: child.id,
|
id: rowId,
|
||||||
|
projectIndex,
|
||||||
|
majorDictId: child.id,
|
||||||
groupCode: group.code,
|
groupCode: group.code,
|
||||||
groupName: group.name,
|
groupName: group.name,
|
||||||
majorCode: child.code,
|
majorCode: child.code,
|
||||||
@ -262,10 +326,13 @@ const buildDefaultRows = (): DetailRow[] => {
|
|||||||
budgetFeeBasic: null,
|
budgetFeeBasic: null,
|
||||||
budgetFeeOptional: null,
|
budgetFeeOptional: null,
|
||||||
remark: '',
|
remark: '',
|
||||||
path: [group.id, child.id]
|
path: isMutipleService.value
|
||||||
|
? [buildProjectGroupPathKey(projectIndex), group.id, rowId]
|
||||||
|
: [group.id, rowId]
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return rows
|
return rows
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -273,6 +340,8 @@ type SourceRow = Pick<DetailRow, 'id'> &
|
|||||||
Partial<
|
Partial<
|
||||||
Pick<
|
Pick<
|
||||||
DetailRow,
|
DetailRow,
|
||||||
|
| 'projectIndex'
|
||||||
|
| 'majorDictId'
|
||||||
| 'amount'
|
| 'amount'
|
||||||
| 'landArea'
|
| 'landArea'
|
||||||
| 'benchmarkBudget'
|
| 'benchmarkBudget'
|
||||||
@ -294,22 +363,32 @@ type SourceRow = Pick<DetailRow, 'id'> &
|
|||||||
>
|
>
|
||||||
const mergeWithDictRows = (
|
const mergeWithDictRows = (
|
||||||
rowsFromDb: SourceRow[] | undefined,
|
rowsFromDb: SourceRow[] | undefined,
|
||||||
options?: { includeScaleValues?: boolean; includeFactorValues?: boolean }
|
options?: {
|
||||||
|
includeScaleValues?: boolean
|
||||||
|
includeFactorValues?: boolean
|
||||||
|
projectCount?: number
|
||||||
|
cloneFromProjectOne?: boolean
|
||||||
|
}
|
||||||
): DetailRow[] => {
|
): DetailRow[] => {
|
||||||
const includeScaleValues = options?.includeScaleValues ?? true
|
const includeScaleValues = options?.includeScaleValues ?? true
|
||||||
const includeFactorValues = options?.includeFactorValues ?? true
|
const includeFactorValues = options?.includeFactorValues ?? true
|
||||||
|
const targetProjectCount = normalizeProjectCount(options?.projectCount ?? getTargetProjectCount())
|
||||||
const dbValueMap = new Map<string, SourceRow>()
|
const dbValueMap = new Map<string, SourceRow>()
|
||||||
for (const row of rowsFromDb || []) {
|
for (const row of rowsFromDb || []) {
|
||||||
const rowId = String(row.id)
|
const projectIndex = resolveRowProjectIndex(row)
|
||||||
dbValueMap.set(rowId, row)
|
const majorDictId = resolveRowMajorDictId(row)
|
||||||
const aliasId = majorIdAliasMap.get(rowId)
|
if (!majorDictId) continue
|
||||||
if (aliasId && !dbValueMap.has(aliasId)) {
|
dbValueMap.set(makeProjectMajorKey(projectIndex, majorDictId), row)
|
||||||
dbValueMap.set(aliasId, row)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return buildDefaultRows().map(row => {
|
return buildDefaultRows(targetProjectCount).map(row => {
|
||||||
const fromDb = dbValueMap.get(row.id)
|
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
|
if (!fromDb) return row
|
||||||
const hasConsultCategoryFactor = Object.prototype.hasOwnProperty.call(fromDb, 'consultCategoryFactor')
|
const hasConsultCategoryFactor = Object.prototype.hasOwnProperty.call(fromDb, 'consultCategoryFactor')
|
||||||
const hasMajorFactor = Object.prototype.hasOwnProperty.call(fromDb, 'majorFactor')
|
const hasMajorFactor = Object.prototype.hasOwnProperty.call(fromDb, 'majorFactor')
|
||||||
@ -342,7 +421,7 @@ const mergeWithDictRows = (
|
|||||||
? fromDb.majorFactor
|
? fromDb.majorFactor
|
||||||
: hasMajorFactor
|
: hasMajorFactor
|
||||||
? null
|
? null
|
||||||
: getDefaultMajorFactorById(row.id),
|
: getDefaultMajorFactorById(rowMajorDictId),
|
||||||
workStageFactor: typeof fromDb.workStageFactor === 'number' ? fromDb.workStageFactor : row.workStageFactor,
|
workStageFactor: typeof fromDb.workStageFactor === 'number' ? fromDb.workStageFactor : row.workStageFactor,
|
||||||
workRatio: typeof fromDb.workRatio === 'number' ? fromDb.workRatio : row.workRatio,
|
workRatio: typeof fromDb.workRatio === 'number' ? fromDb.workRatio : row.workRatio,
|
||||||
budgetFee: typeof fromDb.budgetFee === 'number' ? fromDb.budgetFee : null,
|
budgetFee: typeof fromDb.budgetFee === 'number' ? fromDb.budgetFee : null,
|
||||||
@ -688,12 +767,24 @@ const autoGroupColumnDef: ColDef = {
|
|||||||
if (params.node?.rowPinned) {
|
if (params.node?.rowPinned) {
|
||||||
return totalLabel.value
|
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 nodeId = String(params.value || '')
|
||||||
|
const projectIndex = parseProjectIndexFromPathKey(nodeId)
|
||||||
|
if (projectIndex != null) return `项目${projectIndex}`
|
||||||
return idLabelMap.get(nodeId) || nodeId
|
return idLabelMap.get(nodeId) || nodeId
|
||||||
},
|
},
|
||||||
tooltipValueGetter: params => {
|
tooltipValueGetter: params => {
|
||||||
if (params.node?.rowPinned) return totalLabel.value
|
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 nodeId = String(params.value || '')
|
||||||
|
const projectIndex = parseProjectIndexFromPathKey(nodeId)
|
||||||
|
if (projectIndex != null) return `项目${projectIndex}`
|
||||||
return idLabelMap.get(nodeId) || nodeId
|
return idLabelMap.get(nodeId) || nodeId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -794,22 +885,102 @@ 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
|
||||||
|
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<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 () => {
|
const loadFromIndexedDB = async () => {
|
||||||
try {
|
try {
|
||||||
const baseInfo = await localforage.getItem<XmBaseInfoState>(BASE_INFO_KEY)
|
const baseInfo = await localforage.getItem<XmBaseInfoState>(BASE_INFO_KEY)
|
||||||
activeIndustryCode.value =
|
activeIndustryCode.value =
|
||||||
typeof baseInfo?.projectIndustry === 'string' ? baseInfo.projectIndustry.trim() : ''
|
typeof baseInfo?.projectIndustry === 'string' ? baseInfo.projectIndustry.trim() : ''
|
||||||
|
projectCount.value = 1
|
||||||
|
|
||||||
await ensureFactorDefaultsLoaded()
|
await ensureFactorDefaultsLoaded()
|
||||||
const applyContractDefaultRows = async () => {
|
const applyContractDefaultRows = async () => {
|
||||||
const htData = await localforage.getItem<{ detailRows: SourceRow[] }>(HT_DB_KEY.value)
|
const htData = await localforage.getItem<{ detailRows: SourceRow[] }>(HT_DB_KEY.value)
|
||||||
const hasContractRows = Array.isArray(htData?.detailRows) && htData.detailRows.length > 0
|
const hasContractRows = Array.isArray(htData?.detailRows) && htData.detailRows.length > 0
|
||||||
|
const targetProjectCount = getTargetProjectCount()
|
||||||
detailRows.value = hasContractRows
|
detailRows.value = hasContractRows
|
||||||
? mergeWithDictRows(htData!.detailRows, { includeFactorValues: true })
|
? mergeWithDictRows(htData!.detailRows, {
|
||||||
: buildDefaultRows().map(row => ({
|
includeFactorValues: true,
|
||||||
|
projectCount: targetProjectCount,
|
||||||
|
cloneFromProjectOne: true
|
||||||
|
})
|
||||||
|
: buildDefaultRows(targetProjectCount).map(row => ({
|
||||||
...row,
|
...row,
|
||||||
consultCategoryFactor: getDefaultConsultCategoryFactor(),
|
consultCategoryFactor: getDefaultConsultCategoryFactor(),
|
||||||
majorFactor: getDefaultMajorFactorById(row.id)
|
majorFactor: getDefaultMajorFactorById(row.majorDictId || row.id)
|
||||||
}))
|
}))
|
||||||
syncComputedValuesToDetailRows()
|
syncComputedValuesToDetailRows()
|
||||||
}
|
}
|
||||||
@ -820,7 +991,13 @@ const loadFromIndexedDB = async () => {
|
|||||||
|
|
||||||
const data = await localforage.getItem<XmInfoState>(DB_KEY.value)
|
const data = await localforage.getItem<XmInfoState>(DB_KEY.value)
|
||||||
if (data) {
|
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()
|
syncComputedValuesToDetailRows()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -828,7 +1005,7 @@ const loadFromIndexedDB = async () => {
|
|||||||
await applyContractDefaultRows()
|
await applyContractDefaultRows()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('loadFromIndexedDB failed:', error)
|
console.error('loadFromIndexedDB failed:', error)
|
||||||
detailRows.value = buildDefaultRows()
|
detailRows.value = buildDefaultRows(getTargetProjectCount())
|
||||||
syncComputedValuesToDetailRows()
|
syncComputedValuesToDetailRows()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -843,12 +1020,17 @@ const importContractData = async () => {
|
|||||||
await loadFactorDefaults()
|
await loadFactorDefaults()
|
||||||
const htData = await localforage.getItem<{ detailRows: SourceRow[] }>(HT_DB_KEY.value)
|
const htData = await localforage.getItem<{ detailRows: SourceRow[] }>(HT_DB_KEY.value)
|
||||||
const hasContractRows = Array.isArray(htData?.detailRows) && htData.detailRows.length > 0
|
const hasContractRows = Array.isArray(htData?.detailRows) && htData.detailRows.length > 0
|
||||||
|
const targetProjectCount = getTargetProjectCount()
|
||||||
detailRows.value = hasContractRows
|
detailRows.value = hasContractRows
|
||||||
? mergeWithDictRows(htData!.detailRows, { includeFactorValues: true })
|
? mergeWithDictRows(htData!.detailRows, {
|
||||||
: buildDefaultRows().map(row => ({
|
includeFactorValues: true,
|
||||||
|
projectCount: targetProjectCount,
|
||||||
|
cloneFromProjectOne: true
|
||||||
|
})
|
||||||
|
: buildDefaultRows(targetProjectCount).map(row => ({
|
||||||
...row,
|
...row,
|
||||||
consultCategoryFactor: getDefaultConsultCategoryFactor(),
|
consultCategoryFactor: getDefaultConsultCategoryFactor(),
|
||||||
majorFactor: getDefaultMajorFactorById(row.id)
|
majorFactor: getDefaultMajorFactorById(row.majorDictId || row.id)
|
||||||
}))
|
}))
|
||||||
await saveToIndexedDB()
|
await saveToIndexedDB()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -858,7 +1040,7 @@ const importContractData = async () => {
|
|||||||
|
|
||||||
const clearAllData = async () => {
|
const clearAllData = async () => {
|
||||||
try {
|
try {
|
||||||
detailRows.value = buildDefaultRows()
|
detailRows.value = buildDefaultRows(getTargetProjectCount())
|
||||||
await saveToIndexedDB()
|
await saveToIndexedDB()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('clearAllData failed:', error)
|
console.error('clearAllData failed:', error)
|
||||||
@ -930,10 +1112,16 @@ const processCellFromClipboard = (params: any) => {
|
|||||||
<h3 class="text-sm font-semibold text-foreground">用地规模明细</h3>
|
<h3 class="text-sm font-semibold text-foreground">用地规模明细</h3>
|
||||||
<div v-if="isMutipleService" class="flex items-center gap-2">
|
<div v-if="isMutipleService" class="flex items-center gap-2">
|
||||||
<span class="text-xs text-muted-foreground">项目数量</span>
|
<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">
|
<NumberFieldRoot
|
||||||
<NumberFieldDecrement class="px-2 py-1 text-xs text-muted-foreground hover:bg-muted">-</NumberFieldDecrement>
|
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" />
|
<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>
|
</NumberFieldRoot>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -371,6 +371,7 @@ const tabScrollAreaRef = ref<HTMLElement | null>(null)
|
|||||||
const showTabScrollLeft = ref(false)
|
const showTabScrollLeft = ref(false)
|
||||||
const showTabScrollRight = ref(false)
|
const showTabScrollRight = ref(false)
|
||||||
const isTabStripHover = ref(false)
|
const isTabStripHover = ref(false)
|
||||||
|
const isTabDragging = ref(false)
|
||||||
const tabTitleOverflowMap = ref<Record<string, boolean>>({})
|
const tabTitleOverflowMap = ref<Record<string, boolean>>({})
|
||||||
let tabStripViewportEl: HTMLElement | null = null
|
let tabStripViewportEl: HTMLElement | null = null
|
||||||
let tabTitleOverflowRafId: number | null = null
|
let tabTitleOverflowRafId: number | null = null
|
||||||
@ -544,6 +545,14 @@ const canMoveTab = (event: any) => {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleTabDragStart = () => {
|
||||||
|
isTabDragging.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleTabDragEnd = () => {
|
||||||
|
isTabDragging.value = false
|
||||||
|
}
|
||||||
|
|
||||||
const setTabItemRef = (id: string, el: Element | ComponentPublicInstance | null) => {
|
const setTabItemRef = (id: string, el: Element | ComponentPublicInstance | null) => {
|
||||||
if (el instanceof HTMLElement) {
|
if (el instanceof HTMLElement) {
|
||||||
tabItemElMap.set(id, el)
|
tabItemElMap.set(id, el)
|
||||||
@ -1341,9 +1350,9 @@ watch(
|
|||||||
<template>
|
<template>
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<div class="flex flex-col w-full h-screen bg-background overflow-hidden">
|
<div class="flex flex-col w-full h-screen bg-background overflow-hidden">
|
||||||
<div class="flex items-start gap-2 border-b bg-muted/30 px-2 pt-2 flex-none">
|
<div class="flex items-start gap-2 border-b bg-muted/30 px-2 pt-1 min-h-14 flex-none">
|
||||||
<div
|
<div
|
||||||
class="mb-2 flex min-w-0 flex-1 items-center gap-1"
|
class="flex min-w-0 flex-1 items-start gap-1 h-full self-start"
|
||||||
@mouseenter="isTabStripHover = true"
|
@mouseenter="isTabStripHover = true"
|
||||||
@mouseleave="isTabStripHover = false"
|
@mouseleave="isTabStripHover = false"
|
||||||
>
|
>
|
||||||
@ -1362,20 +1371,26 @@ watch(
|
|||||||
v-model="tabsModel"
|
v-model="tabsModel"
|
||||||
item-key="id"
|
item-key="id"
|
||||||
tag="div"
|
tag="div"
|
||||||
class="flex w-max gap-1"
|
:class="['tab-strip-sortable flex w-max gap-0', isTabDragging ? 'is-dragging' : '']"
|
||||||
:animation="180"
|
:animation="260"
|
||||||
|
easing="cubic-bezier(0.22, 1, 0.36, 1)"
|
||||||
|
ghost-class="tab-drag-ghost"
|
||||||
|
chosen-class="tab-drag-chosen"
|
||||||
|
drag-class="tab-drag-active"
|
||||||
:move="canMoveTab"
|
:move="canMoveTab"
|
||||||
|
@start="handleTabDragStart"
|
||||||
|
@end="handleTabDragEnd"
|
||||||
>
|
>
|
||||||
<template #item="{ element: tab }">
|
<template #item="{ element: tab }">
|
||||||
<div
|
<div
|
||||||
:ref="el => setTabItemRef(tab.id, el)"
|
:ref="el => setTabItemRef(tab.id, el)"
|
||||||
@click="tabStore.activeTabId = tab.id"
|
@mousedown.left="tabStore.activeTabId = tab.id"
|
||||||
@contextmenu.prevent="openTabContextMenu($event, tab.id)"
|
@contextmenu.prevent="openTabContextMenu($event, tab.id)"
|
||||||
:class="[
|
:class="[
|
||||||
'group relative flex items-center h-9 px-4 min-w-[120px] max-w-[220px] cursor-pointer rounded-t-md border-x border-t transition-all text-sm',
|
'tab-item group relative -mb-px -ml-px first:ml-0 flex items-center h-10 px-4 min-w-[120px] max-w-[220px] cursor-pointer rounded-t-md border border-transparent transition-[background-color,border-color,color,box-shadow,transform] duration-200 text-sm',
|
||||||
tabStore.activeTabId === tab.id
|
tabStore.activeTabId === tab.id && !isTabDragging
|
||||||
? 'bg-background border-border font-medium'
|
? 'z-10 bg-background text-foreground !border-border !border-b-0 font-medium'
|
||||||
: 'border-transparent hover:bg-muted text-muted-foreground',
|
: 'bg-muted/25 text-muted-foreground hover:bg-muted/40 hover:text-foreground hover:border-border/70',
|
||||||
tab.id !== 'XmView' ? 'cursor-move' : ''
|
tab.id !== 'XmView' ? 'cursor-move' : ''
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
@ -1415,12 +1430,12 @@ watch(
|
|||||||
>
|
>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex shrink-0 self-start items-start gap-1 mb-1">
|
||||||
<div ref="dataMenuRef" class="relative mb-2 shrink-0">
|
<div ref="dataMenuRef" class="relative shrink-0">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
class="h-9 min-h-9 px-3 py-0 text-sm leading-none cursor-pointer"
|
class="h-9 min-h-9 shrink-0 px-3 py-0 text-sm leading-none cursor-pointer"
|
||||||
@click="dataMenuOpen = !dataMenuOpen"
|
@click="dataMenuOpen = !dataMenuOpen"
|
||||||
>
|
>
|
||||||
<ChevronDown class="h-4 w-4 mr-1" />
|
<ChevronDown class="h-4 w-4 mr-1" />
|
||||||
@ -1461,7 +1476,7 @@ watch(
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
class="mb-2 h-9 min-h-9 shrink-0 px-3 py-0 text-sm leading-none cursor-pointer"
|
class="h-9 min-h-9 shrink-0 px-3 py-0 text-sm leading-none cursor-pointer"
|
||||||
@click="openUserGuide(0)"
|
@click="openUserGuide(0)"
|
||||||
>
|
>
|
||||||
<CircleHelp class="h-4 w-4 mr-1" />
|
<CircleHelp class="h-4 w-4 mr-1" />
|
||||||
@ -1470,7 +1485,7 @@ watch(
|
|||||||
|
|
||||||
<AlertDialogRoot>
|
<AlertDialogRoot>
|
||||||
<AlertDialogTrigger as-child>
|
<AlertDialogTrigger as-child>
|
||||||
<Button variant="destructive" size="sm" class="mb-2 h-9 min-h-9 shrink-0 px-3 py-0 text-sm leading-none">
|
<Button variant="destructive" size="sm" class="h-9 min-h-9 shrink-0 px-3 py-0 text-sm leading-none cursor-pointer">
|
||||||
<RotateCcw class="h-4 w-4 mr-1" />
|
<RotateCcw class="h-4 w-4 mr-1" />
|
||||||
重置
|
重置
|
||||||
</Button>
|
</Button>
|
||||||
@ -1515,6 +1530,8 @@ watch(
|
|||||||
</AlertDialogRoot>
|
</AlertDialogRoot>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex-1 overflow-auto relative">
|
<div class="flex-1 overflow-auto relative">
|
||||||
<div
|
<div
|
||||||
v-for="tab in tabStore.tabs"
|
v-for="tab in tabStore.tabs"
|
||||||
@ -1612,6 +1629,27 @@ watch(
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.tab-strip-sortable > .tab-item {
|
||||||
|
transition: transform 0.26s cubic-bezier(0.22, 1, 0.36, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-strip-sortable.is-dragging > .tab-item {
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-drag-ghost {
|
||||||
|
opacity: 0.32;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-drag-chosen {
|
||||||
|
transform: scale(1.015);
|
||||||
|
box-shadow: 0 10px 24px rgb(0 0 0 / 18%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-drag-active {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
.tab-strip-scroll-area :deep([data-slot="scroll-area-viewport"]) {
|
.tab-strip-scroll-area :deep([data-slot="scroll-area-viewport"]) {
|
||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -310,20 +310,46 @@ const getOnlyCostScaleBudgetFee = (
|
|||||||
majorFactorMap?: Map<string, number | null>,
|
majorFactorMap?: Map<string, number | null>,
|
||||||
industryId?: string | null
|
industryId?: string | null
|
||||||
) => {
|
) => {
|
||||||
const totalAmount = sumByNumber(rowsFromDb || [], row =>
|
|
||||||
typeof row?.amount === 'number' && Number.isFinite(row.amount) ? row.amount : null
|
|
||||||
)
|
|
||||||
const onlyRow = (rowsFromDb || []).find(row => String(row?.id || '') === ONLY_COST_SCALE_ROW_ID)
|
|
||||||
const consultCategoryFactor =
|
|
||||||
toFiniteNumberOrNull(onlyRow?.consultCategoryFactor) ??
|
|
||||||
consultCategoryFactorMap?.get(String(serviceId)) ??
|
|
||||||
getDefaultConsultCategoryFactor(serviceId)
|
|
||||||
const industryMajorEntry = getIndustryMajorEntryByIndustryId(industryId)
|
const industryMajorEntry = getIndustryMajorEntryByIndustryId(industryId)
|
||||||
const majorFactor =
|
const sourceRows = rowsFromDb || []
|
||||||
toFiniteNumberOrNull(onlyRow?.majorFactor) ??
|
const defaultConsultCategoryFactor =
|
||||||
|
consultCategoryFactorMap?.get(String(serviceId)) ?? getDefaultConsultCategoryFactor(serviceId)
|
||||||
|
const defaultMajorFactor =
|
||||||
(industryMajorEntry ? majorFactorMap?.get(industryMajorEntry.id) ?? null : null) ??
|
(industryMajorEntry ? majorFactorMap?.get(industryMajorEntry.id) ?? null : null) ??
|
||||||
toFiniteNumberOrNull(industryMajorEntry?.item?.defCoe) ??
|
toFiniteNumberOrNull(industryMajorEntry?.item?.defCoe) ??
|
||||||
1
|
1
|
||||||
|
|
||||||
|
// 新版 onlyCostScale 支持“按项目行”存储(如 1::majorId、2::majorId),每行需独立计费后求和。
|
||||||
|
const usePerRowCalculation = sourceRows.some(row => {
|
||||||
|
if (typeof row?.projectIndex === 'number' && Number.isFinite(row.projectIndex)) return true
|
||||||
|
const id = String(row?.id || '')
|
||||||
|
return /^\d+::/.test(id)
|
||||||
|
})
|
||||||
|
if (usePerRowCalculation) {
|
||||||
|
return sumByNumber(sourceRows, row => {
|
||||||
|
const amount = toFiniteNumberOrNull(row?.amount)
|
||||||
|
if (amount == null) return null
|
||||||
|
return getScaleBudgetFee({
|
||||||
|
benchmarkBudget: getBenchmarkBudgetByAmount(amount),
|
||||||
|
majorFactor: toFiniteNumberOrNull(row?.majorFactor) ?? defaultMajorFactor,
|
||||||
|
consultCategoryFactor: toFiniteNumberOrNull(row?.consultCategoryFactor) ?? defaultConsultCategoryFactor,
|
||||||
|
workStageFactor: toFiniteNumberOrNull(row?.workStageFactor) ?? 1,
|
||||||
|
workRatio: toFiniteNumberOrNull(row?.workRatio) ?? 100
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalAmount = sumByNumber(sourceRows, row =>
|
||||||
|
typeof row?.amount === 'number' && Number.isFinite(row.amount) ? row.amount : null
|
||||||
|
)
|
||||||
|
const onlyRow =
|
||||||
|
sourceRows.find(row => String(row?.id || '') === ONLY_COST_SCALE_ROW_ID) ||
|
||||||
|
sourceRows.find(row => hasOwn(row, 'consultCategoryFactor') || hasOwn(row, 'majorFactor')) ||
|
||||||
|
sourceRows[0]
|
||||||
|
const consultCategoryFactor =
|
||||||
|
toFiniteNumberOrNull(onlyRow?.consultCategoryFactor) ?? defaultConsultCategoryFactor
|
||||||
|
const majorFactor =
|
||||||
|
toFiniteNumberOrNull(onlyRow?.majorFactor) ?? defaultMajorFactor
|
||||||
const workStageFactor = toFiniteNumberOrNull(onlyRow?.workStageFactor) ?? 1
|
const workStageFactor = toFiniteNumberOrNull(onlyRow?.workStageFactor) ?? 1
|
||||||
const workRatio = toFiniteNumberOrNull(onlyRow?.workRatio) ?? 100
|
const workRatio = toFiniteNumberOrNull(onlyRow?.workRatio) ?? 100
|
||||||
return getScaleBudgetFee({
|
return getScaleBudgetFee({
|
||||||
@ -345,12 +371,15 @@ const buildOnlyCostScaleDetailRows = (
|
|||||||
const totalAmount = sumByNumber(rowsFromDb || [], row =>
|
const totalAmount = sumByNumber(rowsFromDb || [], row =>
|
||||||
typeof row?.amount === 'number' && Number.isFinite(row.amount) ? row.amount : null
|
typeof row?.amount === 'number' && Number.isFinite(row.amount) ? row.amount : null
|
||||||
)
|
)
|
||||||
const onlyRow = (rowsFromDb || []).find(row => String(row?.id || '') === ONLY_COST_SCALE_ROW_ID)
|
const industryMajorEntry = getIndustryMajorEntryByIndustryId(industryId)
|
||||||
|
const onlyCostRowId = industryMajorEntry?.id || ONLY_COST_SCALE_ROW_ID
|
||||||
|
const onlyRow =
|
||||||
|
(rowsFromDb || []).find(row => String(row?.id || '') === ONLY_COST_SCALE_ROW_ID) ||
|
||||||
|
(rowsFromDb || []).find(row => String(row?.id || '') === onlyCostRowId)
|
||||||
const consultCategoryFactor =
|
const consultCategoryFactor =
|
||||||
toFiniteNumberOrNull(onlyRow?.consultCategoryFactor) ??
|
toFiniteNumberOrNull(onlyRow?.consultCategoryFactor) ??
|
||||||
consultCategoryFactorMap?.get(String(serviceId)) ??
|
consultCategoryFactorMap?.get(String(serviceId)) ??
|
||||||
getDefaultConsultCategoryFactor(serviceId)
|
getDefaultConsultCategoryFactor(serviceId)
|
||||||
const industryMajorEntry = getIndustryMajorEntryByIndustryId(industryId)
|
|
||||||
const majorFactor =
|
const majorFactor =
|
||||||
toFiniteNumberOrNull(onlyRow?.majorFactor) ??
|
toFiniteNumberOrNull(onlyRow?.majorFactor) ??
|
||||||
(industryMajorEntry ? majorFactorMap?.get(industryMajorEntry.id) ?? null : null) ??
|
(industryMajorEntry ? majorFactorMap?.get(industryMajorEntry.id) ?? null : null) ??
|
||||||
@ -361,7 +390,7 @@ const buildOnlyCostScaleDetailRows = (
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
id: ONLY_COST_SCALE_ROW_ID,
|
id: onlyCostRowId,
|
||||||
amount: totalAmount,
|
amount: totalAmount,
|
||||||
consultCategoryFactor,
|
consultCategoryFactor,
|
||||||
majorFactor,
|
majorFactor,
|
||||||
|
|||||||
39
src/sql.ts
39
src/sql.ts
@ -583,7 +583,7 @@ async function generateTemplate(data) {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
m5: {
|
m5: {//数量单价
|
||||||
fee: 10000,
|
fee: 10000,
|
||||||
det: [
|
det: [
|
||||||
{
|
{
|
||||||
@ -610,11 +610,12 @@ async function generateTemplate(data) {
|
|||||||
code: { richText: [{ font: { charset: 134, color: { theme: 1 }, italic: true, name: '宋体', size: 10 }, text: 'C' }, { font: { charset: 134, color: { theme: 1 }, italic: true, name: 'Calibri', size: 10, vertAlign: 'subscript' }, text: 'X' }] },
|
code: { richText: [{ font: { charset: 134, color: { theme: 1 }, italic: true, name: '宋体', size: 10 }, text: 'C' }, { font: { charset: 134, color: { theme: 1 }, italic: true, name: 'Calibri', size: 10, vertAlign: 'subscript' }, text: 'X' }] },
|
||||||
name: '咨询服务协调工作',
|
name: '咨询服务协调工作',
|
||||||
fee: 10000,
|
fee: 10000,
|
||||||
m0: {
|
m0: { //费率计取
|
||||||
coe: 0.03,
|
|
||||||
|
coe: 0.03,//费率
|
||||||
fee: 10000,
|
fee: 10000,
|
||||||
},
|
},
|
||||||
m4: {
|
m4: { //工时法
|
||||||
person_num: 10,
|
person_num: 10,
|
||||||
work_day: 3,
|
work_day: 3,
|
||||||
fee: 10000,
|
fee: 10000,
|
||||||
@ -1325,7 +1326,7 @@ async function generateTemplate(data) {
|
|||||||
if (addobj.m4) {
|
if (addobj.m4) {
|
||||||
cusInsertRowFunc(4 + num_4, [sheet_4.getRow(4)], sheet_4, (targetRow) => {
|
cusInsertRowFunc(4 + num_4, [sheet_4.getRow(4)], sheet_4, (targetRow) => {
|
||||||
targetRow.getCell(1).value = num_4++;
|
targetRow.getCell(1).value = num_4++;
|
||||||
targetRow.getCell(2).value = addobj.ref;
|
targetRow.getCell(2).value = addobj.code;
|
||||||
targetRow.getCell(3).value = addobj.name;
|
targetRow.getCell(3).value = addobj.name;
|
||||||
targetRow.getCell(4).value = numberFormatter(addobj.m4.person_num, 0);
|
targetRow.getCell(4).value = numberFormatter(addobj.m4.person_num, 0);
|
||||||
targetRow.getCell(5).value = numberFormatter(addobj.m4.work_day, 2);
|
targetRow.getCell(5).value = numberFormatter(addobj.m4.work_day, 2);
|
||||||
@ -1333,7 +1334,7 @@ async function generateTemplate(data) {
|
|||||||
});
|
});
|
||||||
cusInsertRowFunc(4 + num_4_1, [sheet_4_1.getRow(4)], sheet_4_1, (targetRow) => {
|
cusInsertRowFunc(4 + num_4_1, [sheet_4_1.getRow(4)], sheet_4_1, (targetRow) => {
|
||||||
targetRow.getCell(1).value = num_4_1++;
|
targetRow.getCell(1).value = num_4_1++;
|
||||||
targetRow.getCell(2).value = addobj.ref;
|
targetRow.getCell(2).value = addobj.code;
|
||||||
targetRow.getCell(3).value = addobj.name;
|
targetRow.getCell(3).value = addobj.name;
|
||||||
targetRow.getCell(4).value = '/';
|
targetRow.getCell(4).value = '/';
|
||||||
targetRow.getCell(5).value = '/';
|
targetRow.getCell(5).value = '/';
|
||||||
@ -1361,20 +1362,20 @@ async function generateTemplate(data) {
|
|||||||
if (addobj.m5) {
|
if (addobj.m5) {
|
||||||
cusInsertRowFunc(4 + num_5, [sheet_5.getRow(4)], sheet_5, (targetRow) => {
|
cusInsertRowFunc(4 + num_5, [sheet_5.getRow(4)], sheet_5, (targetRow) => {
|
||||||
num_5++;
|
num_5++;
|
||||||
targetRow.getCell(1).value = addobj.ref;
|
targetRow.getCell(1).value = addobj.code;
|
||||||
targetRow.getCell(2).value = addobj.name;
|
targetRow.getCell(2).value = addobj.name;
|
||||||
targetRow.getCell(3).value = '/';
|
targetRow.getCell(3).value = '/';
|
||||||
targetRow.getCell(4).value = '/';
|
targetRow.getCell(4).value = '/';
|
||||||
targetRow.getCell(5).value = '/';
|
targetRow.getCell(5).value = '/';
|
||||||
targetRow.getCell(6).value = numberFormatter(addobj.m5.fee, 2);
|
targetRow.getCell(6).value = numberFormatter(addobj.m5.fee, 2);
|
||||||
});
|
});
|
||||||
const tmpJSS = JSON.stringify(addobj.ref);
|
const tmpJSS = JSON.stringify(addobj.code);
|
||||||
addobj.m5.det.forEach((eobj, eindex) => {
|
addobj.m5.det.forEach((eobj, eindex) => {
|
||||||
let ref = JSON.parse(tmpJSS);
|
let code = JSON.parse(tmpJSS);
|
||||||
ref.richText.push({ font: { charset: 134, color: { theme: 1 }, italic: true, name: '宋体', size: 10 }, text: '-' + (eindex + 1) });
|
code.richText.push({ font: { charset: 134, color: { theme: 1 }, italic: true, name: '宋体', size: 10 }, text: '-' + (eindex + 1) });
|
||||||
cusInsertRowFunc(4 + num_5, [sheet_5.getRow(4)], sheet_5, (targetRow) => {
|
cusInsertRowFunc(4 + num_5, [sheet_5.getRow(4)], sheet_5, (targetRow) => {
|
||||||
num_5++;
|
num_5++;
|
||||||
targetRow.getCell(1).value = ref;
|
targetRow.getCell(1).value = code;
|
||||||
targetRow.getCell(2).value = eobj.name;
|
targetRow.getCell(2).value = eobj.name;
|
||||||
targetRow.getCell(3).value = eobj.unit;
|
targetRow.getCell(3).value = eobj.unit;
|
||||||
targetRow.getCell(4).value = numberFormatter(eobj.amount, 3);
|
targetRow.getCell(4).value = numberFormatter(eobj.amount, 3);
|
||||||
@ -1391,7 +1392,7 @@ async function generateTemplate(data) {
|
|||||||
endRows++;
|
endRows++;
|
||||||
cusInsertRowFunc(ci.services.length + 3 + endRows, [sheet_1.getRow(3)], sheet_1, (targetRow) => {
|
cusInsertRowFunc(ci.services.length + 3 + endRows, [sheet_1.getRow(3)], sheet_1, (targetRow) => {
|
||||||
targetRow.getCell(1).value = ci.services.length + endRows;
|
targetRow.getCell(1).value = ci.services.length + endRows;
|
||||||
targetRow.getCell(2).value = ci.reserve.ref;
|
targetRow.getCell(2).value = ci.reserve.code;
|
||||||
targetRow.getCell(3).value = ci.reserve.name;
|
targetRow.getCell(3).value = ci.reserve.name;
|
||||||
let tmpArr = [];
|
let tmpArr = [];
|
||||||
if (ci.reserve.m0) tmpArr.push(`按上述小计及附加工作费之和的${ci.reserve.m0.coe}计得${ci.reserve.m0.fee}元`);
|
if (ci.reserve.m0) tmpArr.push(`按上述小计及附加工作费之和的${ci.reserve.m0.coe}计得${ci.reserve.m0.fee}元`);
|
||||||
@ -1403,7 +1404,7 @@ async function generateTemplate(data) {
|
|||||||
if (ci.reserve.m4) {
|
if (ci.reserve.m4) {
|
||||||
cusInsertRowFunc(4 + num_4, [sheet_4.getRow(4)], sheet_4, (targetRow) => {
|
cusInsertRowFunc(4 + num_4, [sheet_4.getRow(4)], sheet_4, (targetRow) => {
|
||||||
targetRow.getCell(1).value = num_4++;
|
targetRow.getCell(1).value = num_4++;
|
||||||
targetRow.getCell(2).value = ci.reserve.ref;
|
targetRow.getCell(2).value = ci.reserve.code;
|
||||||
targetRow.getCell(3).value = ci.reserve.name;
|
targetRow.getCell(3).value = ci.reserve.name;
|
||||||
targetRow.getCell(4).value = numberFormatter(ci.reserve.m4.person_num, 0);
|
targetRow.getCell(4).value = numberFormatter(ci.reserve.m4.person_num, 0);
|
||||||
targetRow.getCell(5).value = numberFormatter(ci.reserve.m4.work_day, 2);
|
targetRow.getCell(5).value = numberFormatter(ci.reserve.m4.work_day, 2);
|
||||||
@ -1411,7 +1412,7 @@ async function generateTemplate(data) {
|
|||||||
});
|
});
|
||||||
cusInsertRowFunc(4 + num_4_1, [sheet_4_1.getRow(4)], sheet_4_1, (targetRow) => {
|
cusInsertRowFunc(4 + num_4_1, [sheet_4_1.getRow(4)], sheet_4_1, (targetRow) => {
|
||||||
targetRow.getCell(1).value = num_4_1++;
|
targetRow.getCell(1).value = num_4_1++;
|
||||||
targetRow.getCell(2).value = ci.reserve.ref;
|
targetRow.getCell(2).value = ci.reserve.code;
|
||||||
targetRow.getCell(3).value = ci.reserve.name;
|
targetRow.getCell(3).value = ci.reserve.name;
|
||||||
targetRow.getCell(4).value = '/';
|
targetRow.getCell(4).value = '/';
|
||||||
targetRow.getCell(5).value = '/';
|
targetRow.getCell(5).value = '/';
|
||||||
@ -1439,20 +1440,20 @@ async function generateTemplate(data) {
|
|||||||
if (ci.reserve.m5) {
|
if (ci.reserve.m5) {
|
||||||
cusInsertRowFunc(4 + num_5, [sheet_5.getRow(4)], sheet_5, (targetRow) => {
|
cusInsertRowFunc(4 + num_5, [sheet_5.getRow(4)], sheet_5, (targetRow) => {
|
||||||
num_5++;
|
num_5++;
|
||||||
targetRow.getCell(1).value = ci.reserve.ref;
|
targetRow.getCell(1).value = ci.reserve.code;
|
||||||
targetRow.getCell(2).value = ci.reserve.name;
|
targetRow.getCell(2).value = ci.reserve.name;
|
||||||
targetRow.getCell(3).value = '/';
|
targetRow.getCell(3).value = '/';
|
||||||
targetRow.getCell(4).value = '/';
|
targetRow.getCell(4).value = '/';
|
||||||
targetRow.getCell(5).value = '/';
|
targetRow.getCell(5).value = '/';
|
||||||
targetRow.getCell(6).value = numberFormatter(ci.reserve.m5.fee, 2);
|
targetRow.getCell(6).value = numberFormatter(ci.reserve.m5.fee, 2);
|
||||||
});
|
});
|
||||||
const tmpJSS = JSON.stringify(ci.reserve.ref);
|
const tmpJSS = JSON.stringify(ci.reserve.code);
|
||||||
ci.reserve.m5.det.forEach((eobj, eindex) => {
|
ci.reserve.m5.det.forEach((eobj, eindex) => {
|
||||||
let ref = JSON.parse(tmpJSS);
|
let code = JSON.parse(tmpJSS);
|
||||||
ref.richText.push({ font: { charset: 134, color: { theme: 1 }, italic: true, name: '宋体', size: 10 }, text: '-' + (eindex + 1) });
|
code.richText.push({ font: { charset: 134, color: { theme: 1 }, italic: true, name: '宋体', size: 10 }, text: '-' + (eindex + 1) });
|
||||||
cusInsertRowFunc(4 + num_5, [sheet_5.getRow(4)], sheet_5, (targetRow) => {
|
cusInsertRowFunc(4 + num_5, [sheet_5.getRow(4)], sheet_5, (targetRow) => {
|
||||||
num_5++;
|
num_5++;
|
||||||
targetRow.getCell(1).value = ref;
|
targetRow.getCell(1).value = code;
|
||||||
targetRow.getCell(2).value = eobj.name;
|
targetRow.getCell(2).value = eobj.name;
|
||||||
targetRow.getCell(3).value = eobj.unit;
|
targetRow.getCell(3).value = eobj.unit;
|
||||||
targetRow.getCell(4).value = numberFormatter(eobj.amount, 3);
|
targetRow.getCell(4).value = numberFormatter(eobj.amount, 3);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user