1109 lines
40 KiB
Vue
1109 lines
40 KiB
Vue
<script setup lang="ts">
|
|
import { computed, nextTick, ref, watch } from 'vue'
|
|
import { useI18n } from 'vue-i18n'
|
|
import { AgGridVue } from 'ag-grid-vue3'
|
|
import type { ColDef, ColGroupDef, GridApi, GridOptions, GridReadyEvent } from 'ag-grid-community'
|
|
import { getIndustryDisplayName, getMajorDictEntries, getServiceDictItemById, isMajorIdInIndustryScope } from '@/sql'
|
|
import { myTheme, gridOptions, agGridWrapClass, agGridStyle } from '@/lib/diyAgGridOptions'
|
|
import { decimalAggSum, roundTo } from '@/lib/decimal'
|
|
import { formatThousandsFlexible } from '@/lib/numberFormat'
|
|
import { withReadonlyAutoHeight } from '@/lib/agGridReadonlyAutoHeight'
|
|
import { syncPricingTotalToZxFw } from '@/lib/zxFwPricingSync'
|
|
import { useZxFwPricingStore } from '@/pinia/zxFwPricing'
|
|
import { useKvStore } from '@/pinia/kv'
|
|
import { loadConsultCategoryFactorMap, loadMajorFactorMap } from '@/lib/xmFactorDefaults'
|
|
import { parseNumberOrNull } from '@/lib/number'
|
|
import {
|
|
buildScaleDetailDict,
|
|
buildScaleIdLabelMap,
|
|
buildScaleRowsFromDict,
|
|
type ScaleDictGroup
|
|
} from '@/lib/pricingScaleDict'
|
|
import {
|
|
createScaleAutoGroupColumn,
|
|
createScaleBenchmarkBudgetColumnGroup,
|
|
createScaleBudgetFeeColumnGroup,
|
|
createScaleRemarkColumn,
|
|
createScaleValueColumn
|
|
} from '@/lib/pricingScaleColumns'
|
|
import {
|
|
createScaleBudgetCellRendererToggleFactory,
|
|
formatScaleEditableConditionalNumber,
|
|
restoreScaleColumnDefaults,
|
|
type ScaleBudgetCheckField,
|
|
type ScaleBudgetHeaderCheckState,
|
|
ScaleBudgetToggleHeader,
|
|
} from '@/lib/pricingScaleGrid'
|
|
import {
|
|
getCheckedBenchmarkBudgetSplitByRow,
|
|
getScaleBudgetFeeByRow,
|
|
getScaleBudgetFeeSplitByRow,
|
|
isBenchmarkBudgetFullyUnchecked,
|
|
isSameNullableNumber,
|
|
recomputeScaleDetailRowsInPlace
|
|
} from '@/lib/pricingScaleDetail'
|
|
import { sumNullableBy } from '@/lib/pricingScaleCalc'
|
|
import { createPinnedTopRowData, createScalePinnedTotalRow } from '@/lib/pricingPinnedRows'
|
|
import { usePricingPaneLifecycle } from '@/lib/pricingScalePaneLifecycle'
|
|
import {
|
|
applyPricingScaleProjectCountChange,
|
|
clearPricingScalePaneRows,
|
|
importPricingScalePaneRows,
|
|
loadPricingScalePaneRows
|
|
} from '@/lib/pricingScalePaneData'
|
|
import {
|
|
buildScaleProjectGroupPathKey,
|
|
buildScopedScaleRowId,
|
|
getScaleProjectMajorKeyFromRow,
|
|
inferScaleProjectCountFromRows,
|
|
normalizeScaleProjectCount
|
|
} from '@/lib/pricingScaleProject'
|
|
import { mergeScaleRowsFromProjectMajorMap } from '@/lib/pricingScaleRowMap'
|
|
import { buildProjectScopedSessionKey } from '@/lib/pricingPersistControl'
|
|
import {
|
|
buildContractScaleIdMap,
|
|
buildContractScaleMap,
|
|
buildContractScaleProjectTotals,
|
|
getContractScaleRowByMajor,
|
|
getContractScaleProjectTotalsByRow,
|
|
makeProjectMajorKey,
|
|
normalizeChangedScaleRowIds,
|
|
parseProjectIndexFromPathKey,
|
|
parseScopedRowId,
|
|
resolveScaleRowMajorDictId as resolveRowMajorDictId,
|
|
resolveScaleRowProjectIndex as resolveRowProjectIndex
|
|
} from '@/lib/pricingScaleLink'
|
|
import { AgGridResetHeader } from '@/lib/agGridResetHeader'
|
|
import { Button } from '@/components/ui/button'
|
|
import {
|
|
AlertDialogAction,
|
|
AlertDialogCancel,
|
|
AlertDialogContent,
|
|
AlertDialogDescription,
|
|
AlertDialogOverlay,
|
|
AlertDialogPortal,
|
|
AlertDialogRoot,
|
|
NumberFieldDecrement,
|
|
NumberFieldIncrement,
|
|
NumberFieldInput,
|
|
NumberFieldRoot,
|
|
AlertDialogTitle,
|
|
AlertDialogTrigger,
|
|
} from 'reka-ui'
|
|
|
|
import { AG_GRID_LOCALE_CN } from '@ag-grid-community/locale';
|
|
// 精简的边框配置(细线条+浅灰色,弱化分割线视觉)
|
|
|
|
interface DetailRow {
|
|
id: string
|
|
projectIndex?: number
|
|
majorDictId?: string
|
|
groupCode: string
|
|
groupName: string
|
|
majorCode: string
|
|
majorName: string
|
|
hasCost: boolean
|
|
hasArea: boolean
|
|
amount: number | null
|
|
landArea: number | null
|
|
benchmarkBudget: number | null
|
|
benchmarkBudgetBasic: number | null
|
|
benchmarkBudgetOptional: number | null
|
|
benchmarkBudgetBasicChecked: boolean
|
|
benchmarkBudgetOptionalChecked: boolean
|
|
basicFormula: string | null
|
|
optionalFormula: string | null
|
|
consultCategoryFactor: number | null
|
|
majorFactor: number | null
|
|
workStageFactor: number | null
|
|
workRatio: number | null
|
|
budgetFee: number | null
|
|
budgetFeeBasic: number | null
|
|
budgetFeeOptional: number | null
|
|
remark: string
|
|
path: string[]
|
|
}
|
|
|
|
interface XmInfoState {
|
|
projectName: string
|
|
detailRows: DetailRow[]
|
|
}
|
|
interface XmBaseInfoState {
|
|
projectIndustry?: string
|
|
}
|
|
|
|
interface ContractScaleChangeState {
|
|
changedRowIds?: string[]
|
|
updatedAt?: number
|
|
}
|
|
|
|
interface FactorChangeState {
|
|
changedRowIds?: string[]
|
|
updatedAt?: number
|
|
}
|
|
|
|
interface ServiceLite {
|
|
mutiple?: boolean | null
|
|
}
|
|
|
|
const props = defineProps<{
|
|
contractId: string,
|
|
serviceId: string | number
|
|
projectInfoKey?: string
|
|
}>()
|
|
const zxFwPricingStore = useZxFwPricingStore()
|
|
const { t, locale } = useI18n()
|
|
const kvStore = useKvStore()
|
|
const DB_KEY = computed(() => `ydGMF-${props.contractId}-${props.serviceId}`)
|
|
const HT_DB_KEY = computed(() => `ht-info-v3-${props.contractId}`)
|
|
const HT_SCALE_CHANGE_KEY = computed(() => `ht-info-scale-change-v1-${props.contractId}`)
|
|
const HT_CONSULT_FACTOR_KEY = computed(() => `ht-consult-category-factor-v1-${props.contractId}`)
|
|
const HT_MAJOR_FACTOR_KEY = computed(() => `ht-major-factor-v1-${props.contractId}`)
|
|
const HT_CONSULT_FACTOR_CHANGE_KEY = computed(() => `${HT_CONSULT_FACTOR_KEY.value}-change`)
|
|
const HT_MAJOR_FACTOR_CHANGE_KEY = computed(() => `${HT_MAJOR_FACTOR_KEY.value}-change`)
|
|
const BASE_INFO_KEY = computed(() => props.projectInfoKey || 'xm-base-info-v1')
|
|
const activeIndustryCode = ref('')
|
|
const PRICING_CLEAR_SKIP_PREFIX = 'pricing-clear-skip:'
|
|
const PRICING_FORCE_DEFAULT_PREFIX = 'pricing-force-default:'
|
|
const consultCategoryFactorMap = ref<Map<string, number | null>>(new Map())
|
|
const majorFactorMap = ref<Map<string, number | null>>(new Map())
|
|
let factorDefaultsLoaded = false
|
|
const paneInstanceCreatedAt = Date.now()
|
|
const gridApi = ref<GridApi<DetailRow> | null>(null)
|
|
const lastAppliedConsultFactorChangeAt = ref(0)
|
|
const lastAppliedMajorFactorChangeAt = ref(0)
|
|
const totalLabel = computed(() => {
|
|
const industryName = getIndustryDisplayName(activeIndustryCode.value.trim(), locale.value)
|
|
return industryName ? t('pricingScale.totalInvestmentByIndustry', { industryName }) : t('pricingScale.totalInvestment')
|
|
})
|
|
|
|
const isMutipleService = computed(() => {
|
|
const service = getServiceDictItemById(props.serviceId) as ServiceLite | undefined
|
|
return service?.mutiple === true
|
|
})
|
|
const projectCount = ref<number>(1)
|
|
const normalizeProjectCount = normalizeScaleProjectCount
|
|
const getTargetProjectCount = () => (isMutipleService.value ? normalizeProjectCount(projectCount.value) : 1)
|
|
const buildProjectGroupPathKey = (projectIndex: number) => buildScaleProjectGroupPathKey(projectIndex)
|
|
const buildScopedRowId = (projectIndex: number, majorId: string) =>
|
|
buildScopedScaleRowId(isMutipleService.value, projectIndex, majorId)
|
|
const inferProjectCountFromRows = (rows?: Array<Partial<DetailRow>>) => {
|
|
return inferScaleProjectCountFromRows(rows, isMutipleService.value)
|
|
}
|
|
|
|
const getMethodState = () =>
|
|
zxFwPricingStore.getServicePricingMethodState<DetailRow>(props.contractId, props.serviceId, 'landScale')
|
|
|
|
const detailRows = computed<DetailRow[]>({
|
|
get: () => {
|
|
const rows = getMethodState()?.detailRows
|
|
return Array.isArray(rows) ? rows : []
|
|
},
|
|
set: rows => {
|
|
const currentState = getMethodState()
|
|
zxFwPricingStore.setServicePricingMethodState(props.contractId, props.serviceId, 'landScale', {
|
|
detailRows: rows,
|
|
projectCount: currentState?.projectCount ?? getTargetProjectCount()
|
|
})
|
|
}
|
|
})
|
|
const getDefaultConsultCategoryFactor = () =>
|
|
consultCategoryFactorMap.value.get(String(props.serviceId)) ?? null
|
|
|
|
const getDefaultMajorFactorById = (id: string): number | null => majorFactorMap.value.get(id) ?? null
|
|
|
|
const loadFactorDefaults = async () => {
|
|
const [consultMap, majorMap] = await Promise.all([
|
|
loadConsultCategoryFactorMap(HT_CONSULT_FACTOR_KEY.value),
|
|
loadMajorFactorMap(HT_MAJOR_FACTOR_KEY.value)
|
|
])
|
|
consultCategoryFactorMap.value = consultMap
|
|
majorFactorMap.value = majorMap
|
|
factorDefaultsLoaded = true
|
|
}
|
|
|
|
const ensureFactorDefaultsLoaded = async () => {
|
|
if (factorDefaultsLoaded) return
|
|
await loadFactorDefaults()
|
|
}
|
|
|
|
const shouldForceDefaultLoad = () => {
|
|
const storageKey = buildProjectScopedSessionKey(PRICING_FORCE_DEFAULT_PREFIX, DB_KEY.value)
|
|
const raw = sessionStorage.getItem(storageKey)
|
|
if (!raw) return false
|
|
const forceUntil = Number(raw)
|
|
sessionStorage.removeItem(storageKey)
|
|
return Number.isFinite(forceUntil) && Date.now() <= forceUntil
|
|
}
|
|
|
|
const shouldSkipPersist = () => {
|
|
const storageKey = buildProjectScopedSessionKey(PRICING_CLEAR_SKIP_PREFIX, DB_KEY.value)
|
|
const raw = sessionStorage.getItem(storageKey)
|
|
if (!raw) return false
|
|
const now = Date.now()
|
|
|
|
if (raw.includes(':')) {
|
|
const [issuedRaw, untilRaw] = raw.split(':')
|
|
const issuedAt = Number(issuedRaw)
|
|
const skipUntil = Number(untilRaw)
|
|
if (Number.isFinite(issuedAt) && Number.isFinite(skipUntil) && now <= skipUntil) {
|
|
return paneInstanceCreatedAt <= issuedAt
|
|
}
|
|
sessionStorage.removeItem(storageKey)
|
|
return false
|
|
}
|
|
|
|
const skipUntil = Number(raw)
|
|
if (Number.isFinite(skipUntil) && now <= skipUntil) return true
|
|
sessionStorage.removeItem(storageKey)
|
|
return false
|
|
}
|
|
|
|
type majorLite = { code: string; name: string; defCoe: number | null; hasCost?: boolean; hasArea?: boolean }
|
|
const serviceEntries: Array<[string, majorLite]> = []
|
|
const detailDict: ScaleDictGroup[] = []
|
|
const idLabelMap = new Map<string, string>()
|
|
const rebuildScaleDictCaches = () => {
|
|
const nextServiceEntries = getMajorDictEntries().map(({ id, item }) => [id, item] as [string, majorLite])
|
|
serviceEntries.splice(0, serviceEntries.length, ...nextServiceEntries)
|
|
const nextDetailDict = buildScaleDetailDict(
|
|
serviceEntries,
|
|
({ hasArea }) => hasArea
|
|
)
|
|
detailDict.splice(0, detailDict.length, ...nextDetailDict)
|
|
const nextIdLabelMap = buildScaleIdLabelMap(nextDetailDict)
|
|
idLabelMap.clear()
|
|
nextIdLabelMap.forEach((label, id) => {
|
|
idLabelMap.set(id, label)
|
|
})
|
|
}
|
|
rebuildScaleDictCaches()
|
|
|
|
const buildDefaultRows = (projectCountValue = getTargetProjectCount()): DetailRow[] => {
|
|
return buildScaleRowsFromDict({
|
|
detailDict,
|
|
projectCount: projectCountValue,
|
|
activeIndustryCode: activeIndustryCode.value,
|
|
isMajorInIndustryScope: isMajorIdInIndustryScope,
|
|
buildScopedRowId,
|
|
buildProjectGroupPathKey,
|
|
isMutipleService: isMutipleService.value,
|
|
createRow: ({ projectIndex, group, child, rowId, path }) => ({
|
|
id: rowId,
|
|
projectIndex,
|
|
majorDictId: child.id,
|
|
groupCode: group.code,
|
|
groupName: group.name,
|
|
majorCode: child.code,
|
|
majorName: child.name,
|
|
hasCost: child.hasCost,
|
|
hasArea: child.hasArea,
|
|
amount: null,
|
|
landArea: null,
|
|
benchmarkBudget: null,
|
|
benchmarkBudgetBasic: null,
|
|
benchmarkBudgetOptional: null,
|
|
benchmarkBudgetBasicChecked: true,
|
|
benchmarkBudgetOptionalChecked: true,
|
|
basicFormula: '',
|
|
optionalFormula: '',
|
|
consultCategoryFactor: null,
|
|
majorFactor: null,
|
|
workStageFactor: 1,
|
|
workRatio: 1,
|
|
budgetFee: null,
|
|
budgetFeeBasic: null,
|
|
budgetFeeOptional: null,
|
|
remark: '',
|
|
path
|
|
})
|
|
})
|
|
}
|
|
|
|
type SourceRow = Pick<DetailRow, 'id'> &
|
|
Partial<
|
|
Pick<
|
|
DetailRow,
|
|
| 'projectIndex'
|
|
| 'majorDictId'
|
|
| 'amount'
|
|
| 'landArea'
|
|
| 'benchmarkBudget'
|
|
| 'benchmarkBudgetBasic'
|
|
| 'benchmarkBudgetOptional'
|
|
| 'benchmarkBudgetBasicChecked'
|
|
| 'benchmarkBudgetOptionalChecked'
|
|
| 'basicFormula'
|
|
| 'optionalFormula'
|
|
| 'consultCategoryFactor'
|
|
| 'majorFactor'
|
|
| 'workStageFactor'
|
|
| 'workRatio'
|
|
| 'budgetFee'
|
|
| 'budgetFeeBasic'
|
|
| 'budgetFeeOptional'
|
|
| 'remark'
|
|
>
|
|
>
|
|
const mergeWithDictRows = (
|
|
rowsFromDb: SourceRow[] | undefined,
|
|
options?: {
|
|
includeScaleValues?: boolean
|
|
includeFactorValues?: boolean
|
|
projectCount?: number
|
|
cloneFromProjectOne?: boolean
|
|
}
|
|
): DetailRow[] => {
|
|
const includeScaleValues = options?.includeScaleValues ?? true
|
|
const includeFactorValues = options?.includeFactorValues ?? true
|
|
const targetProjectCount = normalizeProjectCount(options?.projectCount ?? getTargetProjectCount())
|
|
return mergeScaleRowsFromProjectMajorMap({
|
|
rowsFromDb,
|
|
projectCount: targetProjectCount,
|
|
buildDefaultRows,
|
|
resolveProjectIndex: row => resolveRowProjectIndex(row),
|
|
resolveMajorDictId: row => resolveRowMajorDictId(row),
|
|
cloneFromProjectOne: options?.cloneFromProjectOne,
|
|
mergeRow: (row, fromDb) => {
|
|
if (!fromDb) return row
|
|
const nextMajorDictId = resolveRowMajorDictId(row)
|
|
const hasConsultCategoryFactor = Object.prototype.hasOwnProperty.call(fromDb, 'consultCategoryFactor')
|
|
const hasMajorFactor = Object.prototype.hasOwnProperty.call(fromDb, 'majorFactor')
|
|
|
|
return {
|
|
...row,
|
|
amount: includeScaleValues && row.hasCost && typeof fromDb.amount === 'number' ? fromDb.amount : null,
|
|
landArea: includeScaleValues && row.hasArea && typeof fromDb.landArea === 'number' ? fromDb.landArea : null,
|
|
benchmarkBudget: typeof fromDb.benchmarkBudget === 'number' ? fromDb.benchmarkBudget : null,
|
|
benchmarkBudgetBasic: typeof fromDb.benchmarkBudgetBasic === 'number' ? fromDb.benchmarkBudgetBasic : null,
|
|
benchmarkBudgetOptional: typeof fromDb.benchmarkBudgetOptional === 'number' ? fromDb.benchmarkBudgetOptional : null,
|
|
benchmarkBudgetBasicChecked:
|
|
typeof fromDb.benchmarkBudgetBasicChecked === 'boolean' ? fromDb.benchmarkBudgetBasicChecked : true,
|
|
benchmarkBudgetOptionalChecked:
|
|
typeof fromDb.benchmarkBudgetOptionalChecked === 'boolean' ? fromDb.benchmarkBudgetOptionalChecked : true,
|
|
basicFormula: typeof fromDb.basicFormula === 'string' ? fromDb.basicFormula : '',
|
|
optionalFormula: typeof fromDb.optionalFormula === 'string' ? fromDb.optionalFormula : '',
|
|
consultCategoryFactor:
|
|
!includeFactorValues
|
|
? null
|
|
: typeof fromDb.consultCategoryFactor === 'number'
|
|
? fromDb.consultCategoryFactor
|
|
: hasConsultCategoryFactor
|
|
? null
|
|
: getDefaultConsultCategoryFactor(),
|
|
majorFactor:
|
|
!includeFactorValues
|
|
? null
|
|
: typeof fromDb.majorFactor === 'number'
|
|
? fromDb.majorFactor
|
|
: hasMajorFactor
|
|
? null
|
|
: getDefaultMajorFactorById(nextMajorDictId),
|
|
workStageFactor: typeof fromDb.workStageFactor === 'number' ? fromDb.workStageFactor : row.workStageFactor,
|
|
workRatio: typeof fromDb.workRatio === 'number' ? fromDb.workRatio : row.workRatio,
|
|
budgetFee: typeof fromDb.budgetFee === 'number' ? fromDb.budgetFee : null,
|
|
budgetFeeBasic: typeof fromDb.budgetFeeBasic === 'number' ? fromDb.budgetFeeBasic : null,
|
|
budgetFeeOptional: typeof fromDb.budgetFeeOptional === 'number' ? fromDb.budgetFeeOptional : null,
|
|
remark: typeof fromDb.remark === 'string' ? fromDb.remark : ''
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
const createBudgetCellRendererWithCheck = createScaleBudgetCellRendererToggleFactory(
|
|
() => detailRows.value,
|
|
() => handleCellValueChanged()
|
|
)
|
|
|
|
const getBenchmarkBudgetHeaderCheckState = (field: ScaleBudgetCheckField): ScaleBudgetHeaderCheckState => {
|
|
const targetRows = detailRows.value.filter(row => row.hasArea)
|
|
if (!targetRows.length) return 'all'
|
|
const checkedCount = targetRows.reduce((count, row) => (row[field] !== false ? count + 1 : count), 0)
|
|
if (checkedCount === 0) return 'none'
|
|
if (checkedCount === targetRows.length) return 'all'
|
|
return 'partial'
|
|
}
|
|
|
|
const toggleBenchmarkBudgetColumnChecked = (field: ScaleBudgetCheckField, checked: boolean) => {
|
|
const targetRows = detailRows.value.filter(row => row.hasArea)
|
|
if (!targetRows.length) return
|
|
for (const row of targetRows) {
|
|
row[field] = checked
|
|
}
|
|
gridApi.value?.refreshHeader()
|
|
gridApi.value?.refreshCells({ force: true })
|
|
handleCellValueChanged()
|
|
}
|
|
|
|
const getBenchmarkBudgetHeaderParams = (field: ScaleBudgetCheckField) => ({
|
|
field,
|
|
getHeaderCheckState: getBenchmarkBudgetHeaderCheckState,
|
|
onToggleAll: toggleBenchmarkBudgetColumnChecked
|
|
})
|
|
|
|
const getBenchmarkBudgetSplitByLandArea = (row?: Pick<DetailRow, 'landArea'>) =>
|
|
getCheckedBenchmarkBudgetSplitByRow(
|
|
{
|
|
landArea: row?.landArea,
|
|
benchmarkBudgetBasicChecked: true,
|
|
benchmarkBudgetOptionalChecked: true
|
|
},
|
|
'area'
|
|
)
|
|
|
|
const getCheckedBenchmarkBudgetSplitByLandArea = (
|
|
row?: Pick<DetailRow, 'landArea' | 'benchmarkBudgetBasicChecked' | 'benchmarkBudgetOptionalChecked'>
|
|
) => getCheckedBenchmarkBudgetSplitByRow(row, 'area')
|
|
|
|
const getBudgetFee = (
|
|
row?: Pick<
|
|
DetailRow,
|
|
| 'landArea'
|
|
| 'benchmarkBudgetBasicChecked'
|
|
| 'benchmarkBudgetOptionalChecked'
|
|
| 'majorFactor'
|
|
| 'consultCategoryFactor'
|
|
| 'workStageFactor'
|
|
| 'workRatio'
|
|
>
|
|
) => getScaleBudgetFeeByRow(row, 'area')
|
|
|
|
const getBudgetFeeSplit = (
|
|
row?: Pick<
|
|
DetailRow,
|
|
| 'landArea'
|
|
| 'benchmarkBudgetBasicChecked'
|
|
| 'benchmarkBudgetOptionalChecked'
|
|
| 'majorFactor'
|
|
| 'consultCategoryFactor'
|
|
| 'workStageFactor'
|
|
| 'workRatio'
|
|
>
|
|
) => getScaleBudgetFeeSplitByRow(row, 'area')
|
|
|
|
const formatEditableFlexibleNumber = (params: any) =>
|
|
formatScaleEditableConditionalNumber(params, {
|
|
enabled: Boolean(params.data?.hasArea),
|
|
precision: 3,
|
|
emptyText: t('pricingScale.clickToInput')
|
|
})
|
|
|
|
const restoreLandAreaColumnDefaults = async () => {
|
|
const htData = await kvStore.getItem<{ detailRows: SourceRow[]; totalAmount?: number | null }>(HT_DB_KEY.value)
|
|
const sourceRows = Array.isArray(htData?.detailRows) ? htData.detailRows : []
|
|
const sourceRowMap = buildContractScaleMap(sourceRows)
|
|
const sourceRowIdMap = buildContractScaleIdMap(sourceRows)
|
|
const projectTotals = buildContractScaleProjectTotals(sourceRows, htData?.totalAmount)
|
|
const useSummaryLandArea = detailRows.value.length === 1
|
|
await restoreScaleColumnDefaults({
|
|
gridApi: gridApi.value,
|
|
rows: detailRows.value,
|
|
getCurrentValue: row => row.landArea,
|
|
getNextValue: row => {
|
|
if (!row.hasArea) return null
|
|
if (useSummaryLandArea) return getContractScaleProjectTotalsByRow(row, projectTotals).landArea
|
|
const sourceRow = getContractScaleRowByMajor(row, sourceRowMap, sourceRowIdMap)
|
|
return typeof sourceRow?.landArea === 'number' ? sourceRow.landArea : null
|
|
},
|
|
isSameValue: isSameNullableNumber,
|
|
applyValue: (row, nextValue) => {
|
|
row.landArea = nextValue
|
|
},
|
|
afterApply: async () => {
|
|
syncComputedValuesToDetailRows()
|
|
await saveToIndexedDB({ skipComputedSync: true })
|
|
}
|
|
})
|
|
}
|
|
|
|
const restoreConsultCategoryFactorColumnDefaults = async () => {
|
|
await loadFactorDefaults()
|
|
const nextConsultFactor = getDefaultConsultCategoryFactor()
|
|
await restoreScaleColumnDefaults({
|
|
gridApi: gridApi.value,
|
|
rows: detailRows.value,
|
|
getCurrentValue: row => row.consultCategoryFactor,
|
|
getNextValue: () => nextConsultFactor,
|
|
isSameValue: isSameNullableNumber,
|
|
applyValue: (row, nextValue) => {
|
|
row.consultCategoryFactor = nextValue
|
|
},
|
|
afterApply: async () => {
|
|
syncComputedValuesToDetailRows()
|
|
await saveToIndexedDB({ skipComputedSync: true })
|
|
}
|
|
})
|
|
}
|
|
|
|
const restoreMajorFactorColumnDefaults = async () => {
|
|
await loadFactorDefaults()
|
|
await restoreScaleColumnDefaults({
|
|
gridApi: gridApi.value,
|
|
rows: detailRows.value,
|
|
getCurrentValue: row => row.majorFactor,
|
|
getNextValue: row => getDefaultMajorFactorById(resolveRowMajorDictId(row)),
|
|
isSameValue: isSameNullableNumber,
|
|
applyValue: (row, nextValue) => {
|
|
row.majorFactor = nextValue
|
|
},
|
|
afterApply: async () => {
|
|
syncComputedValuesToDetailRows()
|
|
await saveToIndexedDB({ skipComputedSync: true })
|
|
}
|
|
})
|
|
}
|
|
|
|
const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
|
|
createScaleValueColumn<DetailRow>({
|
|
headerName: t('pricingScale.columns.landArea'),
|
|
field: 'landArea',
|
|
headerTooltip: t('pricingScale.tooltip.resetLandArea'),
|
|
headerComponent: AgGridResetHeader,
|
|
onReset: restoreLandAreaColumnDefaults,
|
|
resetTitle: t('pricingScale.tooltip.resetLandArea'),
|
|
minWidth: 90,
|
|
flex: 2,
|
|
isEditable: row => Boolean(row?.hasArea),
|
|
emptyTextPredicate: (row, value) => Boolean(row?.hasArea) && (value == null || value === ''),
|
|
valueParser: params => {
|
|
const value = parseNumberOrNull(params.newValue)
|
|
return value == null ? null : roundTo(value, 3)
|
|
},
|
|
valueFormatter: formatEditableFlexibleNumber
|
|
}),
|
|
createScaleBenchmarkBudgetColumnGroup<DetailRow>({
|
|
getCheckedSplit: getCheckedBenchmarkBudgetSplitByLandArea,
|
|
createBudgetCellRendererWithCheck,
|
|
getHeaderComponent: () => ScaleBudgetToggleHeader,
|
|
getHeaderComponentParams: getBenchmarkBudgetHeaderParams
|
|
}),
|
|
createScaleBudgetFeeColumnGroup<DetailRow>({
|
|
headerComponent: AgGridResetHeader,
|
|
restoreConsultCategoryFactorColumnDefaults,
|
|
restoreMajorFactorColumnDefaults,
|
|
parseNumberOrNull,
|
|
getBudgetFee,
|
|
aggFunc: decimalAggSum
|
|
}),
|
|
createScaleRemarkColumn<DetailRow>()
|
|
]
|
|
const gridColumnDefs = computed(() => withReadonlyAutoHeight(columnDefs))
|
|
|
|
const autoGroupColumnDef: ColDef = createScaleAutoGroupColumn<DetailRow>({
|
|
totalLabel: totalLabel.value,
|
|
idLabelMap,
|
|
parseProjectIndexFromPathKey
|
|
})
|
|
|
|
|
|
|
|
const totalBudgetFeeBasic = computed(() => sumNullableBy(detailRows.value, row => getBudgetFeeSplit(row)?.basic))
|
|
const totalBudgetFeeOptional = computed(() => sumNullableBy(detailRows.value, row => getBudgetFeeSplit(row)?.optional))
|
|
const totalBudgetFee = computed(() => sumNullableBy(detailRows.value, row => getBudgetFee(row)))
|
|
const pinnedTopRowData = computed(() =>
|
|
createPinnedTopRowData(
|
|
createScalePinnedTotalRow<DetailRow>({
|
|
landArea: null,
|
|
budgetFee: totalBudgetFee.value,
|
|
budgetFeeBasic: totalBudgetFeeBasic.value,
|
|
budgetFeeOptional: totalBudgetFeeOptional.value
|
|
})
|
|
)
|
|
)
|
|
|
|
const syncComputedValuesToDetailRows = () => {
|
|
recomputeScaleDetailRowsInPlace(detailRows.value, 'area')
|
|
}
|
|
|
|
const buildPersistDetailRows = () => detailRows.value.map(row => ({ ...row }))
|
|
|
|
const saveToIndexedDB = async (options?: { skipComputedSync?: boolean }) => {
|
|
if (shouldSkipPersist()) return
|
|
try {
|
|
if (!options?.skipComputedSync) {
|
|
syncComputedValuesToDetailRows()
|
|
}
|
|
const payload = {
|
|
detailRows: JSON.parse(JSON.stringify(buildPersistDetailRows())),
|
|
projectCount: getTargetProjectCount()
|
|
}
|
|
zxFwPricingStore.setServicePricingMethodState(
|
|
props.contractId,
|
|
props.serviceId,
|
|
'landScale',
|
|
payload,
|
|
{ force: true }
|
|
)
|
|
const synced = await syncPricingTotalToZxFw({
|
|
contractId: props.contractId,
|
|
serviceId: props.serviceId,
|
|
field: 'landScale',
|
|
value: totalBudgetFee.value
|
|
})
|
|
if (!synced) return
|
|
} catch (error) {
|
|
console.error('saveToIndexedDB failed:', error)
|
|
}
|
|
}
|
|
|
|
const syncLinkedFieldsFromContractAndFactors = async () => {
|
|
if (detailRows.value.length === 0) return
|
|
const consultChangeState =
|
|
(zxFwPricingStore.keyedStates[HT_CONSULT_FACTOR_CHANGE_KEY.value]
|
|
?? kvStore.entries[HT_CONSULT_FACTOR_CHANGE_KEY.value]
|
|
?? null) as FactorChangeState | null
|
|
const majorChangeState =
|
|
(zxFwPricingStore.keyedStates[HT_MAJOR_FACTOR_CHANGE_KEY.value]
|
|
?? kvStore.entries[HT_MAJOR_FACTOR_CHANGE_KEY.value]
|
|
?? null) as FactorChangeState | null
|
|
|
|
const consultUpdatedAt = Number(consultChangeState?.updatedAt)
|
|
const majorUpdatedAt = Number(majorChangeState?.updatedAt)
|
|
const hasNewConsultChange = Number.isFinite(consultUpdatedAt) && consultUpdatedAt > lastAppliedConsultFactorChangeAt.value
|
|
const hasNewMajorChange = Number.isFinite(majorUpdatedAt) && majorUpdatedAt > lastAppliedMajorFactorChangeAt.value
|
|
|
|
const consultChangedRowIds = new Set(
|
|
(consultChangeState?.changedRowIds || []).map(id => String(id || '').trim()).filter(Boolean)
|
|
)
|
|
const majorChangedRowIdSet = normalizeChangedScaleRowIds(majorChangeState?.changedRowIds)
|
|
const shouldSyncConsultFactor = hasNewConsultChange && consultChangedRowIds.has(String(props.serviceId).trim())
|
|
const shouldSyncMajorFactor = hasNewMajorChange && majorChangedRowIdSet.size > 0
|
|
|
|
if (hasNewConsultChange) lastAppliedConsultFactorChangeAt.value = consultUpdatedAt
|
|
if (hasNewMajorChange) lastAppliedMajorFactorChangeAt.value = majorUpdatedAt
|
|
if (!shouldSyncConsultFactor && !shouldSyncMajorFactor) return
|
|
|
|
await loadFactorDefaults()
|
|
const consultFactor = getDefaultConsultCategoryFactor()
|
|
let changed = false
|
|
for (const row of detailRows.value) {
|
|
if (shouldSyncConsultFactor && !isSameNullableNumber(row.consultCategoryFactor, consultFactor)) {
|
|
row.consultCategoryFactor = consultFactor
|
|
changed = true
|
|
}
|
|
if (!shouldSyncMajorFactor) continue
|
|
const majorDictId = resolveRowMajorDictId(row)
|
|
if (!majorChangedRowIdSet.has(majorDictId)) continue
|
|
const nextMajorFactor = getDefaultMajorFactorById(majorDictId)
|
|
if (isSameNullableNumber(row.majorFactor, nextMajorFactor)) continue
|
|
row.majorFactor = nextMajorFactor
|
|
changed = true
|
|
}
|
|
if (!changed) return
|
|
syncComputedValuesToDetailRows()
|
|
await saveToIndexedDB({ skipComputedSync: true })
|
|
}
|
|
|
|
const syncLinkedScaleValuesFromContract = async (changedRowIds?: string[]) => {
|
|
if (detailRows.value.length === 0) return
|
|
const htData = await kvStore.getItem<{ detailRows: SourceRow[]; totalAmount?: number | null }>(HT_DB_KEY.value)
|
|
const sourceRows = Array.isArray(htData?.detailRows) ? htData.detailRows : []
|
|
const sourceRowMap = buildContractScaleMap(sourceRows)
|
|
const sourceRowIdMap = buildContractScaleIdMap(sourceRows)
|
|
const projectTotals = buildContractScaleProjectTotals(sourceRows, htData?.totalAmount)
|
|
const useSummaryLandArea = detailRows.value.length === 1
|
|
const changedRowIdSet = changedRowIds?.length ? normalizeChangedScaleRowIds(changedRowIds) : null
|
|
|
|
let changed = false
|
|
for (const row of detailRows.value) {
|
|
if (changedRowIdSet && !useSummaryLandArea) {
|
|
const rowMajorId = resolveRowMajorDictId(row)
|
|
if (!changedRowIdSet.has(rowMajorId)) continue
|
|
}
|
|
const sourceRow = getContractScaleRowByMajor(row, sourceRowMap, sourceRowIdMap)
|
|
const nextLandArea = row.hasArea
|
|
? (useSummaryLandArea
|
|
? getContractScaleProjectTotalsByRow(row, projectTotals).landArea
|
|
: (typeof sourceRow?.landArea === 'number' ? sourceRow.landArea : null))
|
|
: null
|
|
if (isSameNullableNumber(row.landArea, nextLandArea)) continue
|
|
row.landArea = nextLandArea
|
|
changed = true
|
|
}
|
|
if (!changed) return
|
|
syncComputedValuesToDetailRows()
|
|
await saveToIndexedDB({ skipComputedSync: true })
|
|
}
|
|
|
|
const getRowId = (params: { data?: DetailRow }) => String(params.data?.id || '')
|
|
const detailGridOptions: GridOptions<DetailRow> = {
|
|
...gridOptions,
|
|
getRowId
|
|
}
|
|
|
|
const linkedSourceSignature = computed(() => JSON.stringify({
|
|
consultFactorChange:
|
|
zxFwPricingStore.keyedStates[HT_CONSULT_FACTOR_CHANGE_KEY.value]
|
|
?? kvStore.entries[HT_CONSULT_FACTOR_CHANGE_KEY.value]
|
|
?? null,
|
|
majorFactorChange:
|
|
zxFwPricingStore.keyedStates[HT_MAJOR_FACTOR_CHANGE_KEY.value]
|
|
?? kvStore.entries[HT_MAJOR_FACTOR_CHANGE_KEY.value]
|
|
?? null
|
|
}))
|
|
|
|
const linkedContractScaleSignature = computed(() => JSON.stringify(
|
|
kvStore.entries[HT_SCALE_CHANGE_KEY.value] ?? null
|
|
))
|
|
|
|
const buildRowsFromImportDefaultSource = async (
|
|
targetProjectCount: number
|
|
): Promise<DetailRow[]> => {
|
|
// 与“使用默认数据”同源:先强制刷新系数,再按合同卡片默认带出。
|
|
await loadFactorDefaults()
|
|
const htData = await kvStore.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 readBaseIndustryCode = async () => {
|
|
const baseInfo = await kvStore.getItem<XmBaseInfoState>(BASE_INFO_KEY.value)
|
|
return typeof baseInfo?.projectIndustry === 'string' ? baseInfo.projectIndustry.trim() : ''
|
|
}
|
|
|
|
const buildRowsFromStoredState = (rows: DetailRow[]) =>
|
|
mergeWithDictRows(rows as any, {
|
|
projectCount: getTargetProjectCount(),
|
|
cloneFromProjectOne: true
|
|
})
|
|
|
|
const buildEmptyRows = (targetProjectCount: number) => buildDefaultRows(targetProjectCount)
|
|
|
|
const applyDetailRows = (rows: DetailRow[]) => {
|
|
detailRows.value = rows.map(row => ({
|
|
...row,
|
|
benchmarkBudgetBasicChecked:
|
|
typeof row.benchmarkBudgetBasicChecked === 'boolean' ? row.benchmarkBudgetBasicChecked : true,
|
|
benchmarkBudgetOptionalChecked:
|
|
typeof row.benchmarkBudgetOptionalChecked === 'boolean' ? row.benchmarkBudgetOptionalChecked : true
|
|
}))
|
|
}
|
|
|
|
const relabelDetailRowsFromDict = async () => {
|
|
rebuildScaleDictCaches()
|
|
if (detailRows.value.length === 0) {
|
|
gridApi.value?.refreshCells({ force: true })
|
|
return
|
|
}
|
|
const majorMetaMap = new Map<string, Pick<DetailRow, 'groupCode' | 'groupName' | 'majorCode' | 'majorName'>>()
|
|
for (const group of detailDict) {
|
|
majorMetaMap.set(group.id, {
|
|
groupCode: group.code,
|
|
groupName: group.name,
|
|
majorCode: group.code,
|
|
majorName: group.name
|
|
})
|
|
for (const child of group.children) {
|
|
majorMetaMap.set(child.id, {
|
|
groupCode: group.code,
|
|
groupName: group.name,
|
|
majorCode: child.code,
|
|
majorName: child.name
|
|
})
|
|
}
|
|
}
|
|
let changed = false
|
|
detailRows.value = detailRows.value.map(row => {
|
|
const meta = majorMetaMap.get(resolveRowMajorDictId(row))
|
|
if (!meta) return row
|
|
if (
|
|
row.groupCode === meta.groupCode &&
|
|
row.groupName === meta.groupName &&
|
|
row.majorCode === meta.majorCode &&
|
|
row.majorName === meta.majorName
|
|
) {
|
|
return row
|
|
}
|
|
changed = true
|
|
return {
|
|
...row,
|
|
groupCode: meta.groupCode,
|
|
groupName: meta.groupName,
|
|
majorCode: meta.majorCode,
|
|
majorName: meta.majorName
|
|
}
|
|
})
|
|
gridApi.value?.refreshCells({ force: true })
|
|
if (!changed) return
|
|
syncComputedValuesToDetailRows()
|
|
await saveToIndexedDB({ skipComputedSync: true })
|
|
}
|
|
|
|
const mergeProjectExpandedRow = (defaultRow: DetailRow, existingRow: DetailRow): DetailRow => ({
|
|
...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
|
|
})
|
|
|
|
const persistProjectCountChange = async () => {
|
|
syncComputedValuesToDetailRows()
|
|
await saveToIndexedDB({ skipComputedSync: true })
|
|
}
|
|
|
|
const applyProjectCountChange = async (nextValue: unknown) => {
|
|
await applyPricingScaleProjectCountChange({
|
|
nextValue,
|
|
setProjectCount: count => {
|
|
projectCount.value = count
|
|
},
|
|
isMutipleService: isMutipleService.value,
|
|
currentRows: detailRows.value,
|
|
cloneRows: rows => rows.map(row => ({ ...row })),
|
|
normalizeProjectCount,
|
|
inferProjectCountFromRows: rows => inferProjectCountFromRows(rows),
|
|
buildRowsForReducedCount: (rows, targetProjectCount) =>
|
|
mergeWithDictRows(rows as any, { projectCount: targetProjectCount }),
|
|
buildRowsFromImportDefaultSource,
|
|
getRowKey: row => getScaleProjectMajorKeyFromRow(row),
|
|
getRowProjectIndex: row => resolveRowProjectIndex(row),
|
|
mergeExistingRow: mergeProjectExpandedRow,
|
|
applyRows: applyDetailRows,
|
|
afterApplyRows: persistProjectCountChange
|
|
})
|
|
}
|
|
|
|
const loadFromIndexedDB = async () => {
|
|
await loadPricingScalePaneRows({
|
|
industry: {
|
|
readIndustryCode: readBaseIndustryCode,
|
|
setIndustryCode: code => {
|
|
activeIndustryCode.value = code
|
|
}
|
|
},
|
|
setProjectCount: count => {
|
|
projectCount.value = count
|
|
},
|
|
ensureFactorDefaultsLoaded,
|
|
shouldForceDefaultLoad,
|
|
buildContractDefaultRows: buildRowsFromImportDefaultSource,
|
|
loadStoredState: () =>
|
|
zxFwPricingStore.loadServicePricingMethodState<DetailRow>(props.contractId, props.serviceId, 'landScale'),
|
|
isMutipleService: isMutipleService.value,
|
|
normalizeProjectCount,
|
|
inferProjectCountFromRows: rows => inferProjectCountFromRows(rows as any),
|
|
buildRowsFromStoredState,
|
|
buildEmptyRows,
|
|
getTargetProjectCount,
|
|
applyRows: applyDetailRows,
|
|
afterApplyRows: syncComputedValuesToDetailRows,
|
|
onError: error => {
|
|
console.error('loadFromIndexedDB failed:', error)
|
|
}
|
|
})
|
|
}
|
|
|
|
const importContractData = async () => {
|
|
await importPricingScalePaneRows({
|
|
industry: {
|
|
readIndustryCode: readBaseIndustryCode,
|
|
setIndustryCode: code => {
|
|
activeIndustryCode.value = code
|
|
}
|
|
},
|
|
getTargetProjectCount,
|
|
buildContractDefaultRows: buildRowsFromImportDefaultSource,
|
|
applyRows: applyDetailRows,
|
|
saveRows: () => saveToIndexedDB(),
|
|
onError: error => {
|
|
console.error('importContractData failed:', error)
|
|
}
|
|
})
|
|
}
|
|
|
|
const clearAllData = async () => {
|
|
await clearPricingScalePaneRows({
|
|
getTargetProjectCount,
|
|
buildEmptyRows,
|
|
applyRows: applyDetailRows,
|
|
saveRows: () => saveToIndexedDB(),
|
|
onError: error => {
|
|
console.error('clearAllData failed:', error)
|
|
}
|
|
})
|
|
}
|
|
|
|
let isBulkClipboardMutation = false
|
|
|
|
const commitGridChanges = async (source: string) => {
|
|
await nextTick()
|
|
syncComputedValuesToDetailRows()
|
|
gridApi.value?.refreshHeader()
|
|
gridApi.value?.refreshCells({ force: true })
|
|
await saveToIndexedDB({ skipComputedSync: true })
|
|
await nextTick()
|
|
gridApi.value?.refreshHeader()
|
|
gridApi.value?.refreshCells({ force: true })
|
|
}
|
|
|
|
const handleCellValueChanged = (event?: any) => {
|
|
if (isBulkClipboardMutation) return
|
|
void commitGridChanges('cell-value-changed')
|
|
}
|
|
|
|
const handleBulkMutationStart = () => {
|
|
isBulkClipboardMutation = true
|
|
}
|
|
|
|
const handleBulkMutationEnd = (event?: any) => {
|
|
isBulkClipboardMutation = false
|
|
void commitGridChanges(event?.type || 'bulk-end')
|
|
}
|
|
|
|
const handleGridReady = (event: GridReadyEvent<DetailRow>) => {
|
|
gridApi.value = event.api
|
|
}
|
|
|
|
usePricingPaneLifecycle({
|
|
gridApi,
|
|
loadFromIndexedDB,
|
|
syncLinkedFields: syncLinkedFieldsFromContractAndFactors,
|
|
linkedSourceSignature,
|
|
linkedSecondarySignature: linkedContractScaleSignature,
|
|
syncSecondaryLinkedFields: () => {
|
|
const state = (kvStore.entries[HT_SCALE_CHANGE_KEY.value] as ContractScaleChangeState | undefined) ?? undefined
|
|
return syncLinkedScaleValuesFromContract(state?.changedRowIds)
|
|
},
|
|
saveToIndexedDB: () => saveToIndexedDB()
|
|
})
|
|
watch(
|
|
() => locale.value,
|
|
() => {
|
|
void relabelDetailRowsFromDict()
|
|
}
|
|
)
|
|
const processCellForClipboard = (params: any) => {
|
|
if (Array.isArray(params.value)) {
|
|
return JSON.stringify(params.value); // 数组转字符串复制
|
|
}
|
|
return params.value;
|
|
};
|
|
|
|
const processCellFromClipboard = (params: any) => {
|
|
const field = String(params.column?.getColDef?.().field || '')
|
|
if (field === 'landArea' || field === 'consultCategoryFactor' || field === 'majorFactor' || field === 'workStageFactor') {
|
|
return parseNumberOrNull(params.value, { precision: 3 })
|
|
}
|
|
if (field === 'workRatio') {
|
|
return parseNumberOrNull(params.value, { precision: 2 })
|
|
}
|
|
try {
|
|
const parsed = JSON.parse(params.value);
|
|
if (Array.isArray(parsed)) return parsed;
|
|
} catch (e) {
|
|
// 解析失败时返回原始值,无需额外处理
|
|
}
|
|
return params.value;
|
|
};
|
|
|
|
|
|
</script>
|
|
|
|
<template>
|
|
<div class="h-full min-h-0 flex flex-col">
|
|
|
|
|
|
<div class="rounded-lg border bg-card xmMx flex min-h-0 flex-1 flex-col">
|
|
<div class="flex items-center justify-between border-b px-4 py-3">
|
|
<div class="flex items-center gap-3">
|
|
<h3 class="text-sm font-semibold text-foreground">{{ t('pricingPane.land.title') }}</h3>
|
|
<div v-if="isMutipleService" class="flex items-center gap-2">
|
|
<span class="text-xs text-muted-foreground">{{ t('pricingPane.projectCount') }}</span>
|
|
<NumberFieldRoot
|
|
v-model="projectCount"
|
|
:min="1"
|
|
:step="1"
|
|
class="inline-flex items-center rounded-md border bg-background"
|
|
@update:model-value="value => void applyProjectCountChange(value)"
|
|
>
|
|
<NumberFieldDecrement class="cursor-pointer px-2 py-1 text-xs text-muted-foreground hover:bg-muted">-</NumberFieldDecrement>
|
|
<NumberFieldInput class="h-7 w-14 border-x bg-transparent px-2 text-center text-xs outline-none" />
|
|
<NumberFieldIncrement class="cursor-pointer px-2 py-1 text-xs text-muted-foreground hover:bg-muted">+</NumberFieldIncrement>
|
|
</NumberFieldRoot>
|
|
</div>
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
<AlertDialogRoot>
|
|
<AlertDialogTrigger as-child>
|
|
<Button type="button" variant="outline" size="sm">{{ t('common.clear') }}</Button>
|
|
</AlertDialogTrigger>
|
|
<AlertDialogPortal>
|
|
<AlertDialogOverlay class="fixed inset-0 z-50 bg-black/45" />
|
|
<AlertDialogContent
|
|
class="fixed left-1/2 top-1/2 z-50 w-[92vw] max-w-md -translate-x-1/2 -translate-y-1/2 rounded-lg border bg-background p-5 shadow-xl">
|
|
<AlertDialogTitle class="text-base font-semibold">{{ t('pricingPane.clearTitle') }}</AlertDialogTitle>
|
|
<AlertDialogDescription class="mt-2 text-sm text-muted-foreground">
|
|
{{ t('pricingPane.land.clearDesc') }}
|
|
</AlertDialogDescription>
|
|
<div class="mt-4 flex items-center justify-end gap-2">
|
|
<AlertDialogCancel as-child>
|
|
<Button variant="outline">{{ t('common.cancel') }}</Button>
|
|
</AlertDialogCancel>
|
|
<AlertDialogAction as-child>
|
|
<Button variant="destructive" @click="clearAllData">{{ t('pricingPane.confirmClear') }}</Button>
|
|
</AlertDialogAction>
|
|
</div>
|
|
</AlertDialogContent>
|
|
</AlertDialogPortal>
|
|
</AlertDialogRoot>
|
|
<AlertDialogRoot>
|
|
<AlertDialogTrigger as-child>
|
|
<Button type="button" variant="outline" size="sm">{{ t('pricingPane.useDefault') }}</Button>
|
|
</AlertDialogTrigger>
|
|
<AlertDialogPortal>
|
|
<AlertDialogOverlay class="fixed inset-0 z-50 bg-black/45" />
|
|
<AlertDialogContent
|
|
class="fixed left-1/2 top-1/2 z-50 w-[92vw] max-w-md -translate-x-1/2 -translate-y-1/2 rounded-lg border bg-background p-5 shadow-xl">
|
|
<AlertDialogTitle class="text-base font-semibold">{{ t('pricingPane.overrideTitle') }}</AlertDialogTitle>
|
|
<AlertDialogDescription class="mt-2 text-sm text-muted-foreground">
|
|
{{ t('pricingPane.land.overrideDesc') }}
|
|
</AlertDialogDescription>
|
|
<div class="mt-4 flex items-center justify-end gap-2">
|
|
<AlertDialogCancel as-child>
|
|
<Button variant="outline">{{ t('common.cancel') }}</Button>
|
|
</AlertDialogCancel>
|
|
<AlertDialogAction as-child>
|
|
<Button @click="importContractData">{{ t('pricingPane.confirmOverride') }}</Button>
|
|
</AlertDialogAction>
|
|
</div>
|
|
</AlertDialogContent>
|
|
</AlertDialogPortal>
|
|
</AlertDialogRoot>
|
|
</div>
|
|
</div>
|
|
|
|
<div :class="agGridWrapClass">
|
|
<AgGridVue :style="agGridStyle" :rowData="detailRows" :pinnedTopRowData="pinnedTopRowData"
|
|
:columnDefs="gridColumnDefs" :autoGroupColumnDef="autoGroupColumnDef" :gridOptions="detailGridOptions" :theme="myTheme"
|
|
:animateRows="true"
|
|
@grid-ready="handleGridReady"
|
|
@cell-value-changed="handleCellValueChanged" :suppressColumnVirtualisation="true"
|
|
@paste-start="handleBulkMutationStart" @paste-end="handleBulkMutationEnd"
|
|
@fill-start="handleBulkMutationStart" @fill-end="handleBulkMutationEnd"
|
|
:suppressRowVirtualisation="true" :cellSelection="{ handle: { mode: 'range' } }" :enableClipboard="true"
|
|
:localeText="AG_GRID_LOCALE_CN" :tooltipShowDelay="500" :headerHeight="50"
|
|
:processCellForClipboard="processCellForClipboard" :processCellFromClipboard="processCellFromClipboard"
|
|
:undoRedoCellEditing="true" :undoRedoCellEditingLimit="20" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|