diff --git a/src/components/views/pricingView/InvestmentScalePricingPane.vue b/src/components/views/pricingView/InvestmentScalePricingPane.vue
index 0e5a6d4..c7493b1 100644
--- a/src/components/views/pricingView/InvestmentScalePricingPane.vue
+++ b/src/components/views/pricingView/InvestmentScalePricingPane.vue
@@ -3,13 +3,15 @@ import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { AgGridVue } from 'ag-grid-vue3'
import type { ColDef } from 'ag-grid-community'
import localforage from 'localforage'
-import { getBasicFeeFromScale, majorList } from '@/sql'
+import { majorList } from '@/sql'
import { myTheme, gridOptions } from '@/lib/diyAgGridOptions'
-import { addNumbers, decimalAggSum, roundTo, sumByNumber, toDecimal } from '@/lib/decimal'
+import { decimalAggSum, roundTo, sumByNumber } from '@/lib/decimal'
import { formatThousands } from '@/lib/numberFormat'
import { syncPricingTotalToZxFw, ZXFW_RELOAD_SERVICE_KEY } from '@/lib/zxFwPricingSync'
import { usePricingPaneReloadStore } from '@/pinia/pricingPaneReload'
import { loadConsultCategoryFactorMap, loadMajorFactorMap } from '@/lib/xmFactorDefaults'
+import { parseNumberOrNull } from '@/lib/number'
+import { getBenchmarkBudgetByScale, getScaleBudgetFee } from '@/lib/pricingScaleFee'
import { Button } from '@/components/ui/button'
import {
AlertDialogAction,
@@ -250,12 +252,6 @@ const mergeWithDictRows = (
})
}
-const parseNumberOrNull = (value: unknown) => {
- if (value === '' || value == null) return null
- const v = Number(value)
- return Number.isFinite(v) ? v : null
-}
-
const formatEditableNumber = (params: any) => {
if (!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')) {
return '点击输入'
@@ -285,15 +281,15 @@ const formatReadonlyMoney = (params: any) => {
return formatThousands(roundTo(params.value, 2))
}
-const getBenchmarkBudgetByAmount = (row?: Pick
) => {
- const result = getBasicFeeFromScale(row?.amount, 'cost')
- return result ? roundTo(addNumbers(result.basic, result.optional), 2) : null
-}
+const getBenchmarkBudgetByAmount = (row?: Pick) =>
+ getBenchmarkBudgetByScale(row?.amount, 'cost')
const getBudgetFee = (row?: Pick) => {
- const benchmarkBudget = getBenchmarkBudgetByAmount(row)
- if (benchmarkBudget == null || row?.majorFactor == null || row?.consultCategoryFactor == null) return null
- return roundTo(toDecimal(benchmarkBudget).mul(row.majorFactor).mul(row.consultCategoryFactor), 2)
+ return getScaleBudgetFee({
+ benchmarkBudget: getBenchmarkBudgetByAmount(row),
+ majorFactor: row?.majorFactor,
+ consultCategoryFactor: row?.consultCategoryFactor
+ })
}
const columnDefs: ColDef[] = [
diff --git a/src/components/views/pricingView/LandScalePricingPane.vue b/src/components/views/pricingView/LandScalePricingPane.vue
index 99df92f..b75366c 100644
--- a/src/components/views/pricingView/LandScalePricingPane.vue
+++ b/src/components/views/pricingView/LandScalePricingPane.vue
@@ -3,13 +3,15 @@ import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { AgGridVue } from 'ag-grid-vue3'
import type { ColDef } from 'ag-grid-community'
import localforage from 'localforage'
-import { getBasicFeeFromScale, majorList } from '@/sql'
+import { majorList } from '@/sql'
import { myTheme, gridOptions } from '@/lib/diyAgGridOptions'
-import { addNumbers, decimalAggSum, roundTo, sumByNumber, toDecimal } from '@/lib/decimal'
+import { decimalAggSum, roundTo, sumByNumber } from '@/lib/decimal'
import { formatThousands } from '@/lib/numberFormat'
import { syncPricingTotalToZxFw, ZXFW_RELOAD_SERVICE_KEY } from '@/lib/zxFwPricingSync'
import { usePricingPaneReloadStore } from '@/pinia/pricingPaneReload'
import { loadConsultCategoryFactorMap, loadMajorFactorMap } from '@/lib/xmFactorDefaults'
+import { parseNumberOrNull } from '@/lib/number'
+import { getBenchmarkBudgetByScale, getScaleBudgetFee } from '@/lib/pricingScaleFee'
import { Button } from '@/components/ui/button'
import {
AlertDialogAction,
@@ -253,12 +255,6 @@ const mergeWithDictRows = (
})
}
-const parseNumberOrNull = (value: unknown) => {
- if (value === '' || value == null) return null
- const v = Number(value)
- return Number.isFinite(v) ? v : null
-}
-
const formatEditableNumber = (params: any) => {
if (!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')) {
return '点击输入'
@@ -280,15 +276,15 @@ const formatReadonlyMoney = (params: any) => {
return formatThousands(roundTo(params.value, 2))
}
-const getBenchmarkBudgetByLandArea = (row?: Pick) => {
- const result = getBasicFeeFromScale(row?.landArea, 'area')
- return result ? roundTo(addNumbers(result.basic, result.optional), 2) : null
-}
+const getBenchmarkBudgetByLandArea = (row?: Pick) =>
+ getBenchmarkBudgetByScale(row?.landArea, 'area')
const getBudgetFee = (row?: Pick) => {
- const benchmarkBudget = getBenchmarkBudgetByLandArea(row)
- if (benchmarkBudget == null || row?.majorFactor == null || row?.consultCategoryFactor == null) return null
- return roundTo(toDecimal(benchmarkBudget).mul(row.majorFactor).mul(row.consultCategoryFactor), 2)
+ return getScaleBudgetFee({
+ benchmarkBudget: getBenchmarkBudgetByLandArea(row),
+ majorFactor: row?.majorFactor,
+ consultCategoryFactor: row?.consultCategoryFactor
+ })
}
const formatEditableFlexibleNumber = (params: any) => {
diff --git a/src/components/views/pricingView/WorkloadPricingPane.vue b/src/components/views/pricingView/WorkloadPricingPane.vue
index 7684c94..ec7e6e8 100644
--- a/src/components/views/pricingView/WorkloadPricingPane.vue
+++ b/src/components/views/pricingView/WorkloadPricingPane.vue
@@ -7,6 +7,7 @@ import { taskList } from '@/sql'
import { myTheme, gridOptions } from '@/lib/diyAgGridOptions'
import { decimalAggSum, roundTo, sumByNumber, toDecimal } from '@/lib/decimal'
import { formatThousands } from '@/lib/numberFormat'
+import { parseNumberOrNull } from '@/lib/number'
import { syncPricingTotalToZxFw, ZXFW_RELOAD_SERVICE_KEY } from '@/lib/zxFwPricingSync'
import { usePricingPaneReloadStore } from '@/pinia/pricingPaneReload'
import { loadConsultCategoryFactorMap } from '@/lib/xmFactorDefaults'
@@ -180,12 +181,8 @@ const mergeWithDictRows = (rowsFromDb: DetailRow[] | undefined): DetailRow[] =>
})
}
-const parseNumberOrNull = (value: unknown) => {
- if (value === '' || value == null) return null
- const normalized = typeof value === 'string' ? value.replace(/[^0-9.\-]/g, '') : value
- const v = Number(normalized)
- return Number.isFinite(v) ? v : null
-}
+const parseSanitizedNumberOrNull = (value: unknown) =>
+ parseNumberOrNull(value, { sanitize: true })
const calcServiceFee = (row: DetailRow | undefined) => {
if (!row || isNoTaskRow(row)) return null
@@ -281,7 +278,7 @@ const columnDefs: ColDef[] = [
!isNoTaskRow(params.data) &&
(params.value == null || params.value === '')
},
- valueParser: params => parseNumberOrNull(params.newValue),
+ valueParser: params => parseSanitizedNumberOrNull(params.newValue),
valueFormatter: params => {
if (isNoTaskRow(params.data)) return '无'
if (!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')) {
@@ -307,7 +304,7 @@ const columnDefs: ColDef[] = [
(params.value == null || params.value === '')
},
aggFunc: decimalAggSum,
- valueParser: params => parseNumberOrNull(params.newValue),
+ valueParser: params => parseSanitizedNumberOrNull(params.newValue),
valueFormatter: formatEditableNumber
},
{
@@ -325,7 +322,7 @@ const columnDefs: ColDef[] = [
!isNoTaskRow(params.data) &&
(params.value == null || params.value === '')
},
- valueParser: params => parseNumberOrNull(params.newValue),
+ valueParser: params => parseSanitizedNumberOrNull(params.newValue),
valueFormatter: formatEditableNumber
},
{
@@ -511,7 +508,7 @@ const mydiyTheme = myTheme.withParams({
diff --git a/src/components/views/xmInfo.vue b/src/components/views/xmInfo.vue
index 7fd6e8e..3dc58a4 100644
--- a/src/components/views/xmInfo.vue
+++ b/src/components/views/xmInfo.vue
@@ -433,7 +433,7 @@ const scrollToGridSection = () => {
>
项目明细
-
导入导出
+
diff --git a/src/lib/decimal.ts b/src/lib/decimal.ts
index ceb6fe9..c4306f2 100644
--- a/src/lib/decimal.ts
+++ b/src/lib/decimal.ts
@@ -1,4 +1,5 @@
import Decimal from 'decimal.js'
+import { isFiniteNumber } from '@/lib/number'
type MaybeNumber = number | null | undefined
type DecimalInput = Decimal.Value
@@ -8,30 +9,26 @@ export const toDecimal = (value: DecimalInput) => new Decimal(value)
export const roundTo = (value: DecimalInput, decimalPlaces = 2) =>
new Decimal(value).toDecimalPlaces(decimalPlaces, Decimal.ROUND_HALF_UP).toNumber()
-export const addNumbers = (...values: MaybeNumber[]) => {
+const sumFiniteValues = (values: Iterable
) => {
let total = new Decimal(0)
for (const value of values) {
- if (typeof value !== 'number' || !Number.isFinite(value)) continue
+ if (!isFiniteNumber(value)) continue
total = total.plus(value)
}
return total.toNumber()
}
+export const addNumbers = (...values: MaybeNumber[]) => sumFiniteValues(values)
+
export const sumByNumber = (list: T[], pick: (item: T) => MaybeNumber) => {
let total = new Decimal(0)
for (const item of list) {
const value = pick(item)
- if (typeof value !== 'number' || !Number.isFinite(value)) continue
+ if (!isFiniteNumber(value)) continue
total = total.plus(value)
}
return total.toNumber()
}
-export const decimalAggSum = (params: { values?: unknown[] }) => {
- let total = new Decimal(0)
- for (const value of params.values || []) {
- if (typeof value !== 'number' || !Number.isFinite(value)) continue
- total = total.plus(value)
- }
- return total.toNumber()
-}
+export const decimalAggSum = (params: { values?: unknown[] }) =>
+ sumFiniteValues(params.values || [])
diff --git a/src/lib/diyAgGridOptions.ts b/src/lib/diyAgGridOptions.ts
index 827ef72..95fb6b0 100644
--- a/src/lib/diyAgGridOptions.ts
+++ b/src/lib/diyAgGridOptions.ts
@@ -1,30 +1,25 @@
-import { GridOptions, themeQuartz } from "ag-grid-community"
+import type { GridOptions } from 'ag-grid-community'
+import { themeQuartz } from 'ag-grid-community'
+
const borderConfig = {
- style: "solid", // 虚线改实线更简洁,也可保留 dotted 但建议用 solid
- width: 0.3, // 更细的边框,减少视觉干扰
- color: "#d3d3d3" // 浅灰色边框,清新不刺眼
-};
+ style: 'solid',
+ width: 0.3,
+ color: '#d3d3d3'
+}
-// 简洁清新风格的主题配置
export const myTheme = themeQuartz.withParams({
- // 核心:移除外边框,减少视觉包裹感
wrapperBorder: false,
-
- // 表头样式(柔和浅蓝,无加粗,更轻盈)
- headerBackgroundColor: "#f0f2f3", // 极浅的背景色,替代深一点的 #e7f3fc
- headerTextColor: "#374151", // 深灰色文字,比纯黑更柔和
- headerFontSize: 15, // 字体稍大一点,更易读
- headerFontWeight: "normal", // 取消加粗,降低视觉重量
-
- // 行/列/表头边框(统一浅灰细边框)
+ headerBackgroundColor: '#f0f2f3',
+ headerTextColor: '#374151',
+ headerFontSize: 15,
+ headerFontWeight: 'normal',
rowBorder: borderConfig,
columnBorder: borderConfig,
headerRowBorder: borderConfig,
+ dataBackgroundColor: '#fefefe'
+})
- // 可选:偶数行背景色(轻微区分,更清新)
- dataBackgroundColor: "#fefefe"
-});
-export const gridOptions: GridOptions = {
+export const gridOptions: GridOptions = {
treeData: true,
animateRows: true,
tooltipShowMode: 'whenTruncated',
@@ -32,13 +27,9 @@ export const gridOptions: GridOptions = {
singleClickEdit: true,
suppressClickEdit: false,
suppressContextMenu: false,
- // autoSizeStrategy: {
- // type: 'fitGridWidth',
- // defaultMinWidth: 100,
- // },
groupDefaultExpanded: -1,
suppressFieldDotNotation: true,
- // Keep group expand/collapse state when rowData updates after edits/saves.
+ // rowData 更新后通过稳定 ID 维持展开状态和编辑上下文。
getRowId: params => String(params.data?.id ?? params.data?.path?.join('/') ?? ''),
getDataPath: data => data.path,
getContextMenuItems: () => ['copy', 'paste', 'separator', 'export'],
diff --git a/src/lib/number.ts b/src/lib/number.ts
new file mode 100644
index 0000000..0a293ae
--- /dev/null
+++ b/src/lib/number.ts
@@ -0,0 +1,20 @@
+export const isFiniteNumber = (value: unknown): value is number =>
+ typeof value === 'number' && Number.isFinite(value)
+
+export const toFiniteNumberOrNull = (value: unknown): number | null =>
+ isFiniteNumber(value) ? value : null
+
+export const parseNumberOrNull = (
+ value: unknown,
+ options?: { sanitize?: boolean }
+): number | null => {
+ if (value === '' || value == null) return null
+
+ const normalized =
+ options?.sanitize && typeof value === 'string'
+ ? value.replace(/[^0-9.\-]/g, '')
+ : value
+
+ const numericValue = Number(normalized)
+ return Number.isFinite(numericValue) ? numericValue : null
+}
diff --git a/src/lib/numberFormat.ts b/src/lib/numberFormat.ts
index 792d516..24092b8 100644
--- a/src/lib/numberFormat.ts
+++ b/src/lib/numberFormat.ts
@@ -1,19 +1,42 @@
+import { parseNumberOrNull } from '@/lib/number'
+
+const fixedFormatterCache = new Map()
+const flexibleFormatterCache = new Map()
+
+const getFixedFormatter = (fractionDigits: number) => {
+ if (!fixedFormatterCache.has(fractionDigits)) {
+ fixedFormatterCache.set(
+ fractionDigits,
+ new Intl.NumberFormat('zh-CN', {
+ minimumFractionDigits: fractionDigits,
+ maximumFractionDigits: fractionDigits
+ })
+ )
+ }
+ return fixedFormatterCache.get(fractionDigits)!
+}
+
+const getFlexibleFormatter = (maxFractionDigits: number) => {
+ if (!flexibleFormatterCache.has(maxFractionDigits)) {
+ flexibleFormatterCache.set(
+ maxFractionDigits,
+ new Intl.NumberFormat('zh-CN', {
+ minimumFractionDigits: 0,
+ maximumFractionDigits: maxFractionDigits
+ })
+ )
+ }
+ return flexibleFormatterCache.get(maxFractionDigits)!
+}
+
export const formatThousands = (value: unknown, fractionDigits = 2) => {
- if (value === '' || value == null) return ''
- const numericValue = Number(value)
- if (!Number.isFinite(numericValue)) return ''
- return numericValue.toLocaleString('zh-CN', {
- minimumFractionDigits: fractionDigits,
- maximumFractionDigits: fractionDigits
- })
+ const numericValue = parseNumberOrNull(value)
+ if (numericValue == null) return ''
+ return getFixedFormatter(fractionDigits).format(numericValue)
}
export const formatThousandsFlexible = (value: unknown, maxFractionDigits = 20) => {
- if (value === '' || value == null) return ''
- const numericValue = Number(value)
- if (!Number.isFinite(numericValue)) return ''
- return numericValue.toLocaleString('zh-CN', {
- minimumFractionDigits: 0,
- maximumFractionDigits: maxFractionDigits
- })
+ const numericValue = parseNumberOrNull(value)
+ if (numericValue == null) return ''
+ return getFlexibleFormatter(maxFractionDigits).format(numericValue)
}
diff --git a/src/lib/pricingMethodTotals.ts b/src/lib/pricingMethodTotals.ts
index 3448d6f..1cbf66a 100644
--- a/src/lib/pricingMethodTotals.ts
+++ b/src/lib/pricingMethodTotals.ts
@@ -1,6 +1,8 @@
import localforage from 'localforage'
-import { expertList, getBasicFeeFromScale, majorList, serviceList, taskList } from '@/sql'
-import { addNumbers, roundTo, sumByNumber, toDecimal } from '@/lib/decimal'
+import { expertList, majorList, serviceList, taskList } from '@/sql'
+import { roundTo, sumByNumber, toDecimal } from '@/lib/decimal'
+import { toFiniteNumberOrNull } from '@/lib/number'
+import { getBenchmarkBudgetByScale, getScaleBudgetFee } from '@/lib/pricingScaleFee'
interface StoredDetailRowsState {
detailRows?: T[]
@@ -58,12 +60,17 @@ export interface PricingMethodTotals {
hourly: number | null
}
-const toFiniteNumberOrNull = (value: unknown): number | null =>
- typeof value === 'number' && Number.isFinite(value) ? value : null
-
const hasOwn = (obj: unknown, key: string) =>
Object.prototype.hasOwnProperty.call(obj || {}, key)
+const toRowMap = (rows?: TRow[]) => {
+ const map = new Map()
+ for (const row of rows || []) {
+ map.set(String(row.id), row)
+ }
+ return map
+}
+
const getDefaultConsultCategoryFactor = (serviceId: string | number) => {
const service = (serviceList as Record)[String(serviceId)]
return toFiniteNumberOrNull(service?.defCoe)
@@ -95,10 +102,7 @@ const mergeScaleRows = (
serviceId: string | number,
rowsFromDb: Array & Pick> | undefined
): ScaleRow[] => {
- const dbValueMap = new Map & Pick>()
- for (const row of rowsFromDb || []) {
- dbValueMap.set(String(row.id), row)
- }
+ const dbValueMap = toRowMap(rowsFromDb)
const defaultConsultCategoryFactor = getDefaultConsultCategoryFactor(serviceId)
return buildDefaultScaleRows(serviceId).map(row => {
@@ -122,26 +126,26 @@ const mergeScaleRows = (
})
}
-const getBenchmarkBudgetByAmount = (amount: MaybeNumber) => {
- const result = getBasicFeeFromScale(amount, 'cost')
- return result ? roundTo(addNumbers(result.basic, result.optional), 2) : null
-}
+const getBenchmarkBudgetByAmount = (amount: MaybeNumber) =>
+ getBenchmarkBudgetByScale(amount, 'cost')
-const getBenchmarkBudgetByLandArea = (landArea: MaybeNumber) => {
- const result = getBasicFeeFromScale(landArea, 'area')
- return result ? roundTo(addNumbers(result.basic, result.optional), 2) : null
-}
+const getBenchmarkBudgetByLandArea = (landArea: MaybeNumber) =>
+ getBenchmarkBudgetByScale(landArea, 'area')
const getInvestmentBudgetFee = (row: ScaleRow) => {
- const benchmarkBudget = getBenchmarkBudgetByAmount(row.amount)
- if (benchmarkBudget == null || row.majorFactor == null || row.consultCategoryFactor == null) return null
- return roundTo(toDecimal(benchmarkBudget).mul(row.majorFactor).mul(row.consultCategoryFactor), 2)
+ return getScaleBudgetFee({
+ benchmarkBudget: getBenchmarkBudgetByAmount(row.amount),
+ majorFactor: row.majorFactor,
+ consultCategoryFactor: row.consultCategoryFactor
+ })
}
const getLandBudgetFee = (row: ScaleRow) => {
- const benchmarkBudget = getBenchmarkBudgetByLandArea(row.landArea)
- if (benchmarkBudget == null || row.majorFactor == null || row.consultCategoryFactor == null) return null
- return roundTo(toDecimal(benchmarkBudget).mul(row.majorFactor).mul(row.consultCategoryFactor), 2)
+ return getScaleBudgetFee({
+ benchmarkBudget: getBenchmarkBudgetByLandArea(row.landArea),
+ majorFactor: row.majorFactor,
+ consultCategoryFactor: row.consultCategoryFactor
+ })
}
const getTaskEntriesByServiceId = (serviceId: string | number) =>
@@ -164,10 +168,7 @@ const mergeWorkloadRows = (
serviceId: string | number,
rowsFromDb: Array & Pick> | undefined
): WorkloadRow[] => {
- const dbValueMap = new Map & Pick>()
- for (const row of rowsFromDb || []) {
- dbValueMap.set(String(row.id), row)
- }
+ const dbValueMap = toRowMap(rowsFromDb)
return buildDefaultWorkloadRows(serviceId).map(row => {
const fromDb = dbValueMap.get(row.id)
@@ -216,10 +217,7 @@ const buildDefaultHourlyRows = (): HourlyRow[] =>
const mergeHourlyRows = (
rowsFromDb: Array & Pick> | undefined
): HourlyRow[] => {
- const dbValueMap = new Map & Pick>()
- for (const row of rowsFromDb || []) {
- dbValueMap.set(String(row.id), row)
- }
+ const dbValueMap = toRowMap(rowsFromDb)
return buildDefaultHourlyRows().map(row => {
const fromDb = dbValueMap.get(row.id)
@@ -239,6 +237,20 @@ const calcHourlyServiceBudget = (row: HourlyRow) => {
return roundTo(toDecimal(row.adoptedBudgetUnitPrice).mul(row.personnelCount).mul(row.workdayCount), 2)
}
+const resolveScaleRows = (
+ serviceId: string,
+ pricingData: StoredDetailRowsState | null,
+ htData: StoredDetailRowsState | null
+) => {
+ if (pricingData?.detailRows != null) {
+ return mergeScaleRows(serviceId, pricingData.detailRows as any)
+ }
+ if (htData?.detailRows != null) {
+ return mergeScaleRows(serviceId, htData.detailRows as any)
+ }
+ return buildDefaultScaleRows(serviceId)
+}
+
export const getPricingMethodTotalsForService = async (params: {
contractId: string
serviceId: string | number
@@ -258,20 +270,11 @@ export const getPricingMethodTotalsForService = async (params: {
localforage.getItem(htDbKey)
])
- const investRows =
- investData?.detailRows != null
- ? mergeScaleRows(serviceId, investData.detailRows as any)
- : htData?.detailRows != null
- ? mergeScaleRows(serviceId, htData.detailRows as any)
- : buildDefaultScaleRows(serviceId)
+ // 优先使用对应计费页的数据;不存在时回退合同段规模信息,再回退默认字典行。
+ const investRows = resolveScaleRows(serviceId, investData, htData)
const investScale = sumByNumber(investRows, row => getInvestmentBudgetFee(row))
- const landRows =
- landData?.detailRows != null
- ? mergeScaleRows(serviceId, landData.detailRows as any)
- : htData?.detailRows != null
- ? mergeScaleRows(serviceId, htData.detailRows as any)
- : buildDefaultScaleRows(serviceId)
+ const landRows = resolveScaleRows(serviceId, landData, htData)
const landScale = sumByNumber(landRows, row => getLandBudgetFee(row))
const defaultWorkloadRows = buildDefaultWorkloadRows(serviceId)
diff --git a/src/lib/pricingScaleFee.ts b/src/lib/pricingScaleFee.ts
new file mode 100644
index 0000000..fd6bd52
--- /dev/null
+++ b/src/lib/pricingScaleFee.ts
@@ -0,0 +1,27 @@
+import { getBasicFeeFromScale } from '@/sql'
+import { addNumbers, roundTo, toDecimal } from '@/lib/decimal'
+import { toFiniteNumberOrNull } from '@/lib/number'
+
+type ScaleMode = 'cost' | 'area'
+
+export const getBenchmarkBudgetByScale = (value: unknown, mode: ScaleMode) => {
+ const scaleValue = toFiniteNumberOrNull(value)
+ const result = getBasicFeeFromScale(scaleValue, mode)
+ return result ? roundTo(addNumbers(result.basic, result.optional), 2) : null
+}
+
+export const getScaleBudgetFee = (params: {
+ benchmarkBudget: unknown
+ majorFactor: unknown
+ consultCategoryFactor: unknown
+}) => {
+ const benchmarkBudget = toFiniteNumberOrNull(params.benchmarkBudget)
+ const majorFactor = toFiniteNumberOrNull(params.majorFactor)
+ const consultCategoryFactor = toFiniteNumberOrNull(params.consultCategoryFactor)
+
+ if (benchmarkBudget == null || majorFactor == null || consultCategoryFactor == null) {
+ return null
+ }
+
+ return roundTo(toDecimal(benchmarkBudget).mul(majorFactor).mul(consultCategoryFactor), 2)
+}
diff --git a/src/lib/xmFactorDefaults.ts b/src/lib/xmFactorDefaults.ts
index ee89961..0caef46 100644
--- a/src/lib/xmFactorDefaults.ts
+++ b/src/lib/xmFactorDefaults.ts
@@ -1,5 +1,6 @@
import localforage from 'localforage'
import { majorList, serviceList } from '@/sql'
+import { toFiniteNumberOrNull } from '@/lib/number'
const CONSULT_CATEGORY_FACTOR_KEY = 'xm-consult-category-factor-v1'
const MAJOR_FACTOR_KEY = 'xm-major-factor-v1'
@@ -20,9 +21,6 @@ type FactorDictItem = {
type FactorDict = Record
-const toFiniteNumberOrNull = (value: unknown): number | null =>
- typeof value === 'number' && Number.isFinite(value) ? value : null
-
const buildStandardFactorMap = (dict: FactorDict): Map => {
const map = new Map()
for (const [id, item] of Object.entries(dict)) {
diff --git a/src/lib/zxFwPricingSync.ts b/src/lib/zxFwPricingSync.ts
index 09a8245..0d76f26 100644
--- a/src/lib/zxFwPricingSync.ts
+++ b/src/lib/zxFwPricingSync.ts
@@ -1,4 +1,5 @@
import localforage from 'localforage'
+import { toFiniteNumberOrNull } from '@/lib/number'
export type ZxFwPricingField = 'investScale' | 'landScale' | 'workload' | 'hourly'
@@ -18,9 +19,6 @@ interface ZxFwState {
export const ZXFW_RELOAD_SERVICE_KEY = 'zxfw-main'
-const toFiniteNumberOrNull = (value: number | null | undefined) =>
- typeof value === 'number' && Number.isFinite(value) ? value : null
-
export const syncPricingTotalToZxFw = async (params: {
contractId: string
serviceId: string | number
diff --git a/src/main.ts b/src/main.ts
index edde7d7..fa58441 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -1,18 +1,18 @@
-import { createApp } from 'vue'
-import './style.css'
-import App from './App.vue'
-import { createPinia } from 'pinia'
import {
- ModuleRegistry,
+ CellStyleModule,
ClientSideRowModelModule,
ColumnAutoSizeModule,
CsvExportModule,
LargeTextEditorModule,
+ LocaleModule,
+ ModuleRegistry,
NumberEditorModule,
PinnedRowModule,
+ RowAutoHeightModule,
TextEditorModule,
TooltipModule,
- UndoRedoEditModule,ValidationModule,LocaleModule ,CellStyleModule ,RowAutoHeightModule
+ UndoRedoEditModule,
+ ValidationModule
} from 'ag-grid-community'
import {
AggregationModule,
@@ -24,18 +24,26 @@ import {
RowGroupingModule,
TreeDataModule
} from 'ag-grid-enterprise'
-LicenseManager.setLicenseKey("[v3][RELEASE][0102]_NDg2Njc4MzY3MDgzNw==16d78ca762fb5d2ff740aed081e2af7b")
-import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' // 引入
-const pinia = createPinia()
-pinia.use(piniaPluginPersistedstate)
-ModuleRegistry.registerModules([
+import { createPinia } from 'pinia'
+import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
+import { createApp } from 'vue'
+import App from './App.vue'
+import './style.css'
+
+LicenseManager.setLicenseKey(
+ '[v3][RELEASE][0102]_NDg2Njc4MzY3MDgzNw==16d78ca762fb5d2ff740aed081e2af7b'
+)
+
+const AG_GRID_MODULES = [
ClientSideRowModelModule,
ColumnAutoSizeModule,
CsvExportModule,
TextEditorModule,
- NumberEditorModule,RowAutoHeightModule,
+ NumberEditorModule,
+ RowAutoHeightModule,
LargeTextEditorModule,
- UndoRedoEditModule,CellStyleModule ,
+ UndoRedoEditModule,
+ CellStyleModule,
PinnedRowModule,
TooltipModule,
TreeDataModule,
@@ -44,8 +52,15 @@ ModuleRegistry.registerModules([
MenuModule,
CellSelectionModule,
ContextMenuModule,
- ClipboardModule,LocaleModule ,
+ ClipboardModule,
+ LocaleModule,
ValidationModule
-])
+]
+
+const pinia = createPinia()
+pinia.use(piniaPluginPersistedstate)
+
+// 在应用启动时一次性注册 AG Grid 运行所需模块。
+ModuleRegistry.registerModules(AG_GRID_MODULES)
createApp(App).use(pinia).mount('#app')
diff --git a/src/pinia/tab.ts b/src/pinia/tab.ts
index cff8792..8405b50 100644
--- a/src/pinia/tab.ts
+++ b/src/pinia/tab.ts
@@ -1,112 +1,115 @@
-// src/stores/tab.ts
import { defineStore } from 'pinia'
import { ref } from 'vue'
-export const useTabStore = defineStore('tabs', () => {
- interface TabItem> {
- id: string; // 标签唯一标识
- title: string; // 标签标题
- componentName: string; // 组件名称
- props?: T; // 传递给组件的 props(可选,泛型适配不同组件)
+export interface TabItem> {
+ id: string
+ title: string
+ componentName: string
+ props?: TProps
}
- const defaultTabs :TabItem[]= [
- { id: 'XmView', title: '项目卡片', componentName: 'XmView' }
- ]
- const tabs = ref([
- ...defaultTabs
- ])
- const activeTabId = ref('XmView')
+const HOME_TAB_ID = 'XmView'
+const DEFAULT_TAB: TabItem = {
+ id: HOME_TAB_ID,
+ title: '项目卡片',
+ componentName: HOME_TAB_ID
+}
- const ensureActiveValid = () => {
- const activeExists = tabs.value.some(t => t.id === activeTabId.value)
- if (!activeExists) {
- activeTabId.value = tabs.value[0]?.id || 'XmView'
- }
- }
+const createDefaultTabs = (): TabItem[] => [{ ...DEFAULT_TAB }]
- const openTab = (config: { id: string; title: string; componentName: string; props?: any }) => {
- const exists = tabs.value.some(t => t.id === config.id)
- if (!exists) {
- tabs.value = [...tabs.value, config]
- }
- activeTabId.value = config.id
- }
+export const useTabStore = defineStore(
+ 'tabs',
+ () => {
+ const tabs = ref(createDefaultTabs())
+ const activeTabId = ref(HOME_TAB_ID)
- const removeTab = (id: string) => {
- if (id === 'XmView') return // 首页不可删除
- const index = tabs.value.findIndex(t => t.id === id)
- if (index < 0) return
- const wasActive = activeTabId.value === id
- tabs.value = tabs.value.filter(t => t.id !== id)
-
- if (tabs.value.length === 0) {
- tabs.value = [...defaultTabs]
+ const ensureHomeTab = () => {
+ if (tabs.value.some(tab => tab.id === HOME_TAB_ID)) return
+ tabs.value = [...createDefaultTabs(), ...tabs.value]
}
- if (wasActive) {
- const fallbackIndex = Math.max(0, Math.min(index - 1, tabs.value.length - 1))
- activeTabId.value = tabs.value[fallbackIndex]?.id || 'XmView'
- return
+ const ensureActiveValid = () => {
+ ensureHomeTab()
+ if (tabs.value.length === 0) tabs.value = createDefaultTabs()
+ if (!tabs.value.some(tab => tab.id === activeTabId.value)) {
+ activeTabId.value = tabs.value[0]?.id ?? HOME_TAB_ID
+ }
}
- const activeStillExists = tabs.value.some(t => t.id === activeTabId.value)
- if (!activeStillExists) {
- activeTabId.value = tabs.value[0]?.id || 'XmView'
+ const openTab = (config: TabItem) => {
+ if (!tabs.value.some(tab => tab.id === config.id)) {
+ tabs.value = [...tabs.value, config]
+ }
+ activeTabId.value = config.id
+ }
+
+ const removeTab = (id: string) => {
+ // 首页标签固定保留,不允许关闭。
+ if (id === HOME_TAB_ID) return
+
+ const index = tabs.value.findIndex(tab => tab.id === id)
+ if (index < 0) return
+
+ const wasActive = activeTabId.value === id
+ tabs.value = tabs.value.filter(tab => tab.id !== id)
+ ensureHomeTab()
+
+ if (wasActive) {
+ const fallbackIndex = Math.max(0, Math.min(index - 1, tabs.value.length - 1))
+ activeTabId.value = tabs.value[fallbackIndex]?.id ?? HOME_TAB_ID
+ return
+ }
+
+ ensureActiveValid()
+ }
+
+ const closeAllTabs = () => {
+ tabs.value = createDefaultTabs()
+ activeTabId.value = HOME_TAB_ID
+ }
+
+ const closeLeftTabs = (targetId: string) => {
+ const targetIndex = tabs.value.findIndex(tab => tab.id === targetId)
+ if (targetIndex < 0) return
+ tabs.value = tabs.value.filter((tab, index) => tab.id === HOME_TAB_ID || index >= targetIndex)
+ ensureActiveValid()
+ }
+
+ const closeRightTabs = (targetId: string) => {
+ const targetIndex = tabs.value.findIndex(tab => tab.id === targetId)
+ if (targetIndex < 0) return
+ tabs.value = tabs.value.filter((tab, index) => tab.id === HOME_TAB_ID || index <= targetIndex)
+ ensureActiveValid()
+ }
+
+ const closeOtherTabs = (targetId: string) => {
+ tabs.value = tabs.value.filter(tab => tab.id === HOME_TAB_ID || tab.id === targetId)
+ ensureHomeTab()
+ activeTabId.value = tabs.value.some(tab => tab.id === targetId) ? targetId : HOME_TAB_ID
+ }
+
+ const resetTabs = () => {
+ tabs.value = createDefaultTabs()
+ activeTabId.value = HOME_TAB_ID
+ }
+
+ return {
+ tabs,
+ activeTabId,
+ openTab,
+ removeTab,
+ closeAllTabs,
+ closeLeftTabs,
+ closeRightTabs,
+ closeOtherTabs,
+ resetTabs
+ }
+ },
+ {
+ persist: {
+ key: 'tabs',
+ storage: localStorage,
+ pick: ['tabs', 'activeTabId']
}
}
-
- const closeAllTabs = () => {
- tabs.value = tabs.value.filter(t => t.id === 'XmView')
- if (tabs.value.length === 0) tabs.value = [...defaultTabs]
- activeTabId.value = 'XmView'
- }
-
- const closeLeftTabs = (targetId: string) => {
- const targetIndex = tabs.value.findIndex(t => t.id === targetId)
- if (targetIndex < 0) return
- tabs.value = tabs.value.filter((tab, index) => tab.id === 'XmView' || index >= targetIndex)
- ensureActiveValid()
- }
-
- const closeRightTabs = (targetId: string) => {
- const targetIndex = tabs.value.findIndex(t => t.id === targetId)
- if (targetIndex < 0) return
- tabs.value = tabs.value.filter((tab, index) => tab.id === 'XmView' || index <= targetIndex)
- ensureActiveValid()
- }
-
- const closeOtherTabs = (targetId: string) => {
- tabs.value = tabs.value.filter(tab => tab.id === 'XmView' || tab.id === targetId)
- if (tabs.value.length === 0) tabs.value = [...defaultTabs]
- if (targetId === 'XmView') {
- activeTabId.value = 'XmView'
- return
- }
- activeTabId.value = tabs.value.some(t => t.id === targetId) ? targetId : 'XmView'
- }
-
- const resetTabs = () => {
- tabs.value = [...defaultTabs]
- activeTabId.value = 'XmView'
- }
-
- return {
- tabs,
- activeTabId,
- openTab,
- removeTab,
- closeAllTabs,
- closeLeftTabs,
- closeRightTabs,
- closeOtherTabs,
- resetTabs
- }
-}, {
- // --- 关键配置:开启持久化 ---
- persist: {
- key: 'tabs', // 存储在 localStorage 里的 key
- storage: localStorage, // 也可以改用 sessionStorage
- pick: ['tabs', 'activeTabId'], // 指定哪些变量需要持久化
- }
-})
+)
diff --git a/src/sql.ts b/src/sql.ts
index efe7a4c..90617fb 100644
--- a/src/sql.ts
+++ b/src/sql.ts
@@ -53,8 +53,8 @@ export const serviceList = {
14: { code: 'D4', name: '专项造价咨询', maxCoe: null, minCoe: null, defCoe: null, desc: '' },
15: { code: 'D4-1', name: '工程造价顾问', maxCoe: null, minCoe: null, defCoe: 1, desc: '本表系数适用于采用工作量计价法基准预算的调整系数。' },
16: { code: 'D4-2', name: '造价政策制(修)订', maxCoe: null, minCoe: null, defCoe: 1, desc: '' },
- 17: { code: 'D4-3', name: '造价科学与技术研究', maxCoe: null, minCoe: null, defCoe: 1, desc: ''},
- 18: { code: 'D4-4', name: '定额测定', maxCoe: null, minCoe: null, defCoe: 1, desc: ''},
+ 17: { code: 'D4-3', name: '造价科学与技术研究', maxCoe: null, minCoe: null, defCoe: 1, desc: '' },
+ 18: { code: 'D4-4', name: '定额测定', maxCoe: null, minCoe: null, defCoe: 1, desc: '' },
19: { code: 'D4-5', name: '造价信息咨询', maxCoe: null, minCoe: null, defCoe: 1, desc: '' },
20: { code: 'D4-6', name: '造价鉴定', maxCoe: null, minCoe: null, defCoe: 0.5, desc: '本表系数适用于采用规模计价法基准预算的调整系数。' },
21: { code: 'D4-7', name: '工程成本测算', maxCoe: null, minCoe: null, defCoe: 0.1, desc: '' },
@@ -63,12 +63,12 @@ export const serviceList = {
24: { code: 'D4-10', name: '工程变更费用咨询', maxCoe: null, minCoe: null, defCoe: 0.5, desc: '' },
25: { code: 'D4-11', name: '调整估算', maxCoe: 0.2, minCoe: 0.1, defCoe: 0.15, desc: '' },
26: { code: 'D4-12', name: '调整概算', maxCoe: 0.3, minCoe: 0.15, defCoe: 0.225, desc: '本表系数适用于采用规模计价法基准预算的系数;依据其调整时期所在建设阶段和基础资料的不同,其系数取值不同。' },
- 27: { code: 'D4-13', name: '造价检查', maxCoe: null, minCoe: null, defCoe: null, desc: '可按照服务工日数量×服务工日人工单价×综合预算系数;也可按照服务工日数量×服务工日综合预算单价。' ,notshowByzxflxs:true},
- 28: { code: 'D4-14', name: '其他专项咨询', maxCoe: null, minCoe: null, defCoe: null, desc: '可参照相同或相似服务的系数。' ,notshowByzxflxs:true},
+ 27: { code: 'D4-13', name: '造价检查', maxCoe: null, minCoe: null, defCoe: null, desc: '可按照服务工日数量×服务工日人工单价×综合预算系数;也可按照服务工日数量×服务工日综合预算单价。', notshowByzxflxs: true },
+ 28: { code: 'D4-14', name: '其他专项咨询', maxCoe: null, minCoe: null, defCoe: null, desc: '可参照相同或相似服务的系数。', notshowByzxflxs: true },
};
//basicParam预算基数
-export const taskList = {
+export const taskList = {
0: { serviceID: 15, ref: 'C4-1', name: '工程造价日常顾问', basicParam: '服务月份数', required: true, unit: '万元/月', conversion: 10000, maxPrice: 0.5, minPrice: 0.3, defPrice: 0.4, desc: '' },
1: { serviceID: 15, ref: 'C4-2', name: '工程造价专项顾问', basicParam: '服务项目的造价金额', required: true, unit: '%', conversion: 0.01, maxPrice: null, minPrice: null, defPrice: 0.01, desc: '适用于涉及造价费用类的顾问' },
2: { serviceID: 16, ref: 'C5-1', name: '组织与调研工作', basicParam: '调研次数', required: true, unit: '万元/次', conversion: 10000, maxPrice: 2, minPrice: 1, defPrice: 1.5, desc: '' },
@@ -124,7 +124,7 @@ export const expertList = {
7: { code: 'C9-3-2', name: '一级造价工程师且具备高级工程师资格', maxPrice: 2000, minPrice: 1800, defPrice: 1900, manageCoe: 2.05 },
};
- const costScaleCal = [
+const costScaleCal = [
{ code: 'C1-1', staLine: 0, endLine: 100, basic: { staPrice: 0, rate: 0.01 }, optional: { staPrice: 0, rate: 0.002 } },
{ code: 'C1-2', staLine: 100, endLine: 300, basic: { staPrice: 10000, rate: 0.008 }, optional: { staPrice: 2000, rate: 0.0016 } },
{ code: 'C1-3', staLine: 300, endLine: 500, basic: { staPrice: 26000, rate: 0.005 }, optional: { staPrice: 5200, rate: 0.001 } },
@@ -144,7 +144,7 @@ export const expertList = {
{ code: 'C1-17', staLine: 1000000, endLine: null, basic: { staPrice: 5906000, rate: 0.00025 }, optional: { staPrice: 1181200, rate: 0.00005 } },
];
- const areaScaleCal = [
+const areaScaleCal = [
{ code: 'C2-1', staLine: 0, endLine: 50, basic: { staPrice: 0, rate: 200 }, optional: { staPrice: 0, rate: 40 } },
{ code: 'C2-2', staLine: 50, endLine: 100, basic: { staPrice: 10000, rate: 160 }, optional: { staPrice: 2000, rate: 32 } },
{ code: 'C2-3', staLine: 100, endLine: 500, basic: { staPrice: 18000, rate: 120 }, optional: { staPrice: 3600, rate: 24 } },
@@ -238,7 +238,7 @@ export function getBasicFeeFromScale(scaleValue: unknown, scaleType: 'cost' | 'a
}
-async function exportFile(fileName, data) {
+export async function exportFile(fileName, data) {
if (window.showSaveFilePicker) {
const handle = await window.showSaveFilePicker({
suggestedName: fileName,
@@ -817,15 +817,15 @@ function cloneCellValue(value) {
}
-
+//demo
let data1 = {
- name: 'test001',
- fee: 10000,
- scale: [
+ name: 'test001',//项目名称
+ fee: 10000, //所有合同段总费用
+ scale: [//项目明细aggrid数据
{
- major: 0,
- cost: 100000,
- area: 200,
+ major: 0, //专业id,对应专业majorList中的key
+ cost: 100000,//造价金额
+ area: 200,//用地面积
},
{
major: 1,
@@ -833,15 +833,15 @@ let data1 = {
area: 200,
},
],
- contracts: [
+ contracts: [//合同段数据
{
- name: 'A合同段',
- fee: 10000,
- scale: [
+ name: 'A合同段',//合同段名称
+ fee: 10000,//合同段费用(该合同段咨询服务zxfw.vue里面aggrid的合同预算行的小计)
+ scale: [//合同段明细aggrid数据(htinfo.vue)
{
- major: 0,
- cost: 100000,
- area: 200,
+ major: 0, //专业id,对应专业majorList中的key
+ cost: 100000,//造价金额
+ area: 200,//用地面积
},
{
major: 1,
@@ -849,12 +849,12 @@ let data1 = {
area: 200,
},
],
- services: [
+ services: [//咨询服务数据(zxfw.vue里面aggrid的数据,不用输出合同预算)
{
- id: 0,
- fee: 100000,
- method1: { // 投资规模法
- cost: 100000,
+ id: 0, //服务id,对应serviceList中的key
+ fee: 100000 ,//服务费用(该服务咨询服务小计)
+ method1: { // 投资规模法InvestmentScalePricingPane.vue的数据
+ cost: 100000, //zxfw.vue里面aggrid该数据的投资规模法金额
basicFee: 200,
basicFee_basic: 200,
basicFee_optional: 0,