This commit is contained in:
wintsa 2026-03-02 17:52:32 +08:00
parent 3950057707
commit a10359f7e0
19 changed files with 349 additions and 299 deletions

View File

@ -3,9 +3,5 @@ import Tab from '@/layout/tab.vue'
</script> </script>
<template> <template>
<tab></tab> <Tab />
</template> </template>
<style scoped>
</style>

View File

@ -4,6 +4,7 @@ import { AgGridVue } from 'ag-grid-vue3'
import type { ColDef, FirstDataRenderedEvent, GridApi, GridReadyEvent, GridSizeChangedEvent } from 'ag-grid-community' import type { ColDef, FirstDataRenderedEvent, GridApi, GridReadyEvent, GridSizeChangedEvent } from 'ag-grid-community'
import localforage from 'localforage' import localforage from 'localforage'
import { myTheme, gridOptions } from '@/lib/diyAgGridOptions' import { myTheme, gridOptions } from '@/lib/diyAgGridOptions'
import { parseNumberOrNull } from '@/lib/number'
import { AG_GRID_LOCALE_CN } from '@ag-grid-community/locale' import { AG_GRID_LOCALE_CN } from '@ag-grid-community/locale'
interface DictItem { interface DictItem {
@ -41,12 +42,6 @@ const props = defineProps<{
const detailRows = ref<FactorRow[]>([]) const detailRows = ref<FactorRow[]>([])
const gridApi = ref<GridApi<FactorRow> | null>(null) const gridApi = ref<GridApi<FactorRow> | null>(null)
const parseNumberOrNull = (value: unknown) => {
if (value === '' || value == null) return null
const v = Number(value)
return Number.isFinite(v) ? v : null
}
const formatReadonlyFactor = (value: unknown) => { const formatReadonlyFactor = (value: unknown) => {
if (value == null || value === '') return '' if (value == null || value === '') return ''
return Number(value).toFixed(2) return Number(value).toFixed(2)
@ -277,7 +272,7 @@ onBeforeUnmount(() => {
<div class="rounded-lg border bg-card xmMx flex min-h-0 flex-1 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 justify-between border-b px-4 py-3">
<h3 class="text-sm font-semibold text-foreground">{{ title }}</h3> <h3 class="text-sm font-semibold text-foreground">{{ title }}</h3>
<div class="text-xs text-muted-foreground">导入导出</div> <div class="text-xs text-muted-foreground"></div>
</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">

View File

@ -327,7 +327,7 @@ const processCellFromClipboard = (params:any) => {
<div class="rounded-lg border bg-card xmMx flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden"> <div class="rounded-lg border bg-card xmMx flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden">
<div class="flex items-center justify-between border-b px-4 py-3"> <div class="flex items-center justify-between border-b px-4 py-3">
<h3 class="text-sm font-semibold text-foreground">合同规模明细</h3> <h3 class="text-sm font-semibold text-foreground">合同规模明细</h3>
<div class="text-xs text-muted-foreground">导入导出</div> <div class="text-xs text-muted-foreground"></div>
</div> </div>
<div class="ag-theme-quartz h-full min-h-0 min-w-0 w-full flex-1 overflow-hidden"> <div class="ag-theme-quartz h-full min-h-0 min-w-0 w-full flex-1 overflow-hidden">

View File

@ -7,6 +7,7 @@ import { expertList } from '@/sql'
import { myTheme, gridOptions } from '@/lib/diyAgGridOptions' import { myTheme, gridOptions } from '@/lib/diyAgGridOptions'
import { decimalAggSum, roundTo, sumByNumber, toDecimal } from '@/lib/decimal' import { decimalAggSum, roundTo, sumByNumber, toDecimal } from '@/lib/decimal'
import { formatThousands } from '@/lib/numberFormat' import { formatThousands } from '@/lib/numberFormat'
import { parseNumberOrNull } from '@/lib/number'
import { syncPricingTotalToZxFw, ZXFW_RELOAD_SERVICE_KEY } from '@/lib/zxFwPricingSync' import { syncPricingTotalToZxFw, ZXFW_RELOAD_SERVICE_KEY } from '@/lib/zxFwPricingSync'
import { usePricingPaneReloadStore } from '@/pinia/pricingPaneReload' import { usePricingPaneReloadStore } from '@/pinia/pricingPaneReload'
@ -173,12 +174,6 @@ const mergeWithDictRows = (rowsFromDb: DetailRow[] | undefined): DetailRow[] =>
}) })
} }
const parseNumberOrNull = (value: unknown) => {
if (value === '' || value == null) return null
const v = Number(value)
return Number.isFinite(v) ? v : null
}
const parseNonNegativeIntegerOrNull = (value: unknown) => { const parseNonNegativeIntegerOrNull = (value: unknown) => {
if (value === '' || value == null) return null if (value === '' || value == null) return null
if (typeof value === 'number') { if (typeof value === 'number') {
@ -482,7 +477,7 @@ const handleGridReady = (params: any) => {
<div class="rounded-lg border bg-card xmMx flex min-h-0 flex-1 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 justify-between border-b px-4 py-3">
<h3 class="text-sm font-semibold text-foreground">工时法明细</h3> <h3 class="text-sm font-semibold text-foreground">工时法明细</h3>
<div class="text-xs text-muted-foreground">导入导出</div> <div class="text-xs text-muted-foreground"></div>
</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">

View File

@ -3,13 +3,15 @@ import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { AgGridVue } from 'ag-grid-vue3' import { AgGridVue } from 'ag-grid-vue3'
import type { ColDef } from 'ag-grid-community' import type { ColDef } from 'ag-grid-community'
import localforage from 'localforage' import localforage from 'localforage'
import { getBasicFeeFromScale, majorList } from '@/sql' import { majorList } from '@/sql'
import { myTheme, gridOptions } from '@/lib/diyAgGridOptions' 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 { formatThousands } from '@/lib/numberFormat'
import { syncPricingTotalToZxFw, ZXFW_RELOAD_SERVICE_KEY } from '@/lib/zxFwPricingSync' import { syncPricingTotalToZxFw, ZXFW_RELOAD_SERVICE_KEY } from '@/lib/zxFwPricingSync'
import { usePricingPaneReloadStore } from '@/pinia/pricingPaneReload' import { usePricingPaneReloadStore } from '@/pinia/pricingPaneReload'
import { loadConsultCategoryFactorMap, loadMajorFactorMap } from '@/lib/xmFactorDefaults' import { loadConsultCategoryFactorMap, loadMajorFactorMap } from '@/lib/xmFactorDefaults'
import { parseNumberOrNull } from '@/lib/number'
import { getBenchmarkBudgetByScale, getScaleBudgetFee } from '@/lib/pricingScaleFee'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { import {
AlertDialogAction, 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) => { const formatEditableNumber = (params: any) => {
if (!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')) { if (!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')) {
return '点击输入' return '点击输入'
@ -285,15 +281,15 @@ const formatReadonlyMoney = (params: any) => {
return formatThousands(roundTo(params.value, 2)) return formatThousands(roundTo(params.value, 2))
} }
const getBenchmarkBudgetByAmount = (row?: Pick<DetailRow, 'amount'>) => { const getBenchmarkBudgetByAmount = (row?: Pick<DetailRow, 'amount'>) =>
const result = getBasicFeeFromScale(row?.amount, 'cost') getBenchmarkBudgetByScale(row?.amount, 'cost')
return result ? roundTo(addNumbers(result.basic, result.optional), 2) : null
}
const getBudgetFee = (row?: Pick<DetailRow, 'amount' | 'majorFactor' | 'consultCategoryFactor'>) => { const getBudgetFee = (row?: Pick<DetailRow, 'amount' | 'majorFactor' | 'consultCategoryFactor'>) => {
const benchmarkBudget = getBenchmarkBudgetByAmount(row) return getScaleBudgetFee({
if (benchmarkBudget == null || row?.majorFactor == null || row?.consultCategoryFactor == null) return null benchmarkBudget: getBenchmarkBudgetByAmount(row),
return roundTo(toDecimal(benchmarkBudget).mul(row.majorFactor).mul(row.consultCategoryFactor), 2) majorFactor: row?.majorFactor,
consultCategoryFactor: row?.consultCategoryFactor
})
} }
const columnDefs: ColDef<DetailRow>[] = [ const columnDefs: ColDef<DetailRow>[] = [

View File

@ -3,13 +3,15 @@ import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { AgGridVue } from 'ag-grid-vue3' import { AgGridVue } from 'ag-grid-vue3'
import type { ColDef } from 'ag-grid-community' import type { ColDef } from 'ag-grid-community'
import localforage from 'localforage' import localforage from 'localforage'
import { getBasicFeeFromScale, majorList } from '@/sql' import { majorList } from '@/sql'
import { myTheme, gridOptions } from '@/lib/diyAgGridOptions' 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 { formatThousands } from '@/lib/numberFormat'
import { syncPricingTotalToZxFw, ZXFW_RELOAD_SERVICE_KEY } from '@/lib/zxFwPricingSync' import { syncPricingTotalToZxFw, ZXFW_RELOAD_SERVICE_KEY } from '@/lib/zxFwPricingSync'
import { usePricingPaneReloadStore } from '@/pinia/pricingPaneReload' import { usePricingPaneReloadStore } from '@/pinia/pricingPaneReload'
import { loadConsultCategoryFactorMap, loadMajorFactorMap } from '@/lib/xmFactorDefaults' import { loadConsultCategoryFactorMap, loadMajorFactorMap } from '@/lib/xmFactorDefaults'
import { parseNumberOrNull } from '@/lib/number'
import { getBenchmarkBudgetByScale, getScaleBudgetFee } from '@/lib/pricingScaleFee'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { import {
AlertDialogAction, 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) => { const formatEditableNumber = (params: any) => {
if (!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')) { if (!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')) {
return '点击输入' return '点击输入'
@ -280,15 +276,15 @@ const formatReadonlyMoney = (params: any) => {
return formatThousands(roundTo(params.value, 2)) return formatThousands(roundTo(params.value, 2))
} }
const getBenchmarkBudgetByLandArea = (row?: Pick<DetailRow, 'landArea'>) => { const getBenchmarkBudgetByLandArea = (row?: Pick<DetailRow, 'landArea'>) =>
const result = getBasicFeeFromScale(row?.landArea, 'area') getBenchmarkBudgetByScale(row?.landArea, 'area')
return result ? roundTo(addNumbers(result.basic, result.optional), 2) : null
}
const getBudgetFee = (row?: Pick<DetailRow, 'landArea' | 'majorFactor' | 'consultCategoryFactor'>) => { const getBudgetFee = (row?: Pick<DetailRow, 'landArea' | 'majorFactor' | 'consultCategoryFactor'>) => {
const benchmarkBudget = getBenchmarkBudgetByLandArea(row) return getScaleBudgetFee({
if (benchmarkBudget == null || row?.majorFactor == null || row?.consultCategoryFactor == null) return null benchmarkBudget: getBenchmarkBudgetByLandArea(row),
return roundTo(toDecimal(benchmarkBudget).mul(row.majorFactor).mul(row.consultCategoryFactor), 2) majorFactor: row?.majorFactor,
consultCategoryFactor: row?.consultCategoryFactor
})
} }
const formatEditableFlexibleNumber = (params: any) => { const formatEditableFlexibleNumber = (params: any) => {

View File

@ -7,6 +7,7 @@ import { taskList } from '@/sql'
import { myTheme, gridOptions } from '@/lib/diyAgGridOptions' import { myTheme, gridOptions } from '@/lib/diyAgGridOptions'
import { decimalAggSum, roundTo, sumByNumber, toDecimal } from '@/lib/decimal' import { decimalAggSum, roundTo, sumByNumber, toDecimal } from '@/lib/decimal'
import { formatThousands } from '@/lib/numberFormat' import { formatThousands } from '@/lib/numberFormat'
import { parseNumberOrNull } from '@/lib/number'
import { syncPricingTotalToZxFw, ZXFW_RELOAD_SERVICE_KEY } from '@/lib/zxFwPricingSync' import { syncPricingTotalToZxFw, ZXFW_RELOAD_SERVICE_KEY } from '@/lib/zxFwPricingSync'
import { usePricingPaneReloadStore } from '@/pinia/pricingPaneReload' import { usePricingPaneReloadStore } from '@/pinia/pricingPaneReload'
import { loadConsultCategoryFactorMap } from '@/lib/xmFactorDefaults' import { loadConsultCategoryFactorMap } from '@/lib/xmFactorDefaults'
@ -180,12 +181,8 @@ const mergeWithDictRows = (rowsFromDb: DetailRow[] | undefined): DetailRow[] =>
}) })
} }
const parseNumberOrNull = (value: unknown) => { const parseSanitizedNumberOrNull = (value: unknown) =>
if (value === '' || value == null) return null parseNumberOrNull(value, { sanitize: true })
const normalized = typeof value === 'string' ? value.replace(/[^0-9.\-]/g, '') : value
const v = Number(normalized)
return Number.isFinite(v) ? v : null
}
const calcServiceFee = (row: DetailRow | undefined) => { const calcServiceFee = (row: DetailRow | undefined) => {
if (!row || isNoTaskRow(row)) return null if (!row || isNoTaskRow(row)) return null
@ -281,7 +278,7 @@ const columnDefs: ColDef<DetailRow>[] = [
!isNoTaskRow(params.data) && !isNoTaskRow(params.data) &&
(params.value == null || params.value === '') (params.value == null || params.value === '')
}, },
valueParser: params => parseNumberOrNull(params.newValue), valueParser: params => parseSanitizedNumberOrNull(params.newValue),
valueFormatter: params => { valueFormatter: params => {
if (isNoTaskRow(params.data)) return '无' if (isNoTaskRow(params.data)) return '无'
if (!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')) { if (!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')) {
@ -307,7 +304,7 @@ const columnDefs: ColDef<DetailRow>[] = [
(params.value == null || params.value === '') (params.value == null || params.value === '')
}, },
aggFunc: decimalAggSum, aggFunc: decimalAggSum,
valueParser: params => parseNumberOrNull(params.newValue), valueParser: params => parseSanitizedNumberOrNull(params.newValue),
valueFormatter: formatEditableNumber valueFormatter: formatEditableNumber
}, },
{ {
@ -325,7 +322,7 @@ const columnDefs: ColDef<DetailRow>[] = [
!isNoTaskRow(params.data) && !isNoTaskRow(params.data) &&
(params.value == null || params.value === '') (params.value == null || params.value === '')
}, },
valueParser: params => parseNumberOrNull(params.newValue), valueParser: params => parseSanitizedNumberOrNull(params.newValue),
valueFormatter: formatEditableNumber valueFormatter: formatEditableNumber
}, },
{ {
@ -511,7 +508,7 @@ const mydiyTheme = myTheme.withParams({
<div class="rounded-lg border bg-card xmMx flex min-h-0 flex-1 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 justify-between border-b px-4 py-3">
<h3 class="text-sm font-semibold text-foreground">工作量明细</h3> <h3 class="text-sm font-semibold text-foreground">工作量明细</h3>
<div class="text-xs text-muted-foreground">导入导出</div> <div class="text-xs text-muted-foreground"></div>
</div> </div>
<div v-if="isWorkloadMethodApplicable" class="ag-theme-quartz h-full min-h-0 w-full flex-1"> <div v-if="isWorkloadMethodApplicable" class="ag-theme-quartz h-full min-h-0 w-full flex-1">

View File

@ -433,7 +433,7 @@ const scrollToGridSection = () => {
> >
项目明细 项目明细
</h3> </h3>
<div class="text-xs text-muted-foreground">导入导出</div> <div class="text-xs text-muted-foreground"></div>
</div> </div>
<div ref="agGridRef" class="ag-theme-quartz w-full flex-1 min-h-0"> <div ref="agGridRef" class="ag-theme-quartz w-full flex-1 min-h-0">

View File

@ -1,4 +1,5 @@
import Decimal from 'decimal.js' import Decimal from 'decimal.js'
import { isFiniteNumber } from '@/lib/number'
type MaybeNumber = number | null | undefined type MaybeNumber = number | null | undefined
type DecimalInput = Decimal.Value type DecimalInput = Decimal.Value
@ -8,30 +9,26 @@ export const toDecimal = (value: DecimalInput) => new Decimal(value)
export const roundTo = (value: DecimalInput, decimalPlaces = 2) => export const roundTo = (value: DecimalInput, decimalPlaces = 2) =>
new Decimal(value).toDecimalPlaces(decimalPlaces, Decimal.ROUND_HALF_UP).toNumber() new Decimal(value).toDecimalPlaces(decimalPlaces, Decimal.ROUND_HALF_UP).toNumber()
export const addNumbers = (...values: MaybeNumber[]) => { const sumFiniteValues = (values: Iterable<unknown>) => {
let total = new Decimal(0) let total = new Decimal(0)
for (const value of values) { for (const value of values) {
if (typeof value !== 'number' || !Number.isFinite(value)) continue if (!isFiniteNumber(value)) continue
total = total.plus(value) total = total.plus(value)
} }
return total.toNumber() return total.toNumber()
} }
export const addNumbers = (...values: MaybeNumber[]) => sumFiniteValues(values)
export const sumByNumber = <T>(list: T[], pick: (item: T) => MaybeNumber) => { export const sumByNumber = <T>(list: T[], pick: (item: T) => MaybeNumber) => {
let total = new Decimal(0) let total = new Decimal(0)
for (const item of list) { for (const item of list) {
const value = pick(item) const value = pick(item)
if (typeof value !== 'number' || !Number.isFinite(value)) continue if (!isFiniteNumber(value)) continue
total = total.plus(value) total = total.plus(value)
} }
return total.toNumber() return total.toNumber()
} }
export const decimalAggSum = (params: { values?: unknown[] }) => { export const decimalAggSum = (params: { values?: unknown[] }) =>
let total = new Decimal(0) sumFiniteValues(params.values || [])
for (const value of params.values || []) {
if (typeof value !== 'number' || !Number.isFinite(value)) continue
total = total.plus(value)
}
return total.toNumber()
}

View File

@ -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 = { const borderConfig = {
style: "solid", // 虚线改实线更简洁,也可保留 dotted 但建议用 solid style: 'solid',
width: 0.3, // 更细的边框,减少视觉干扰 width: 0.3,
color: "#d3d3d3" // 浅灰色边框,清新不刺眼 color: '#d3d3d3'
}; }
// 简洁清新风格的主题配置
export const myTheme = themeQuartz.withParams({ export const myTheme = themeQuartz.withParams({
// 核心:移除外边框,减少视觉包裹感
wrapperBorder: false, wrapperBorder: false,
headerBackgroundColor: '#f0f2f3',
// 表头样式(柔和浅蓝,无加粗,更轻盈) headerTextColor: '#374151',
headerBackgroundColor: "#f0f2f3", // 极浅的背景色,替代深一点的 #e7f3fc headerFontSize: 15,
headerTextColor: "#374151", // 深灰色文字,比纯黑更柔和 headerFontWeight: 'normal',
headerFontSize: 15, // 字体稍大一点,更易读
headerFontWeight: "normal", // 取消加粗,降低视觉重量
// 行/列/表头边框(统一浅灰细边框)
rowBorder: borderConfig, rowBorder: borderConfig,
columnBorder: borderConfig, columnBorder: borderConfig,
headerRowBorder: borderConfig, headerRowBorder: borderConfig,
dataBackgroundColor: '#fefefe'
})
// 可选:偶数行背景色(轻微区分,更清新) export const gridOptions: GridOptions = {
dataBackgroundColor: "#fefefe"
});
export const gridOptions: GridOptions<any> = {
treeData: true, treeData: true,
animateRows: true, animateRows: true,
tooltipShowMode: 'whenTruncated', tooltipShowMode: 'whenTruncated',
@ -32,13 +27,9 @@ export const gridOptions: GridOptions<any> = {
singleClickEdit: true, singleClickEdit: true,
suppressClickEdit: false, suppressClickEdit: false,
suppressContextMenu: false, suppressContextMenu: false,
// autoSizeStrategy: {
// type: 'fitGridWidth',
// defaultMinWidth: 100,
// },
groupDefaultExpanded: -1, groupDefaultExpanded: -1,
suppressFieldDotNotation: true, 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('/') ?? ''), getRowId: params => String(params.data?.id ?? params.data?.path?.join('/') ?? ''),
getDataPath: data => data.path, getDataPath: data => data.path,
getContextMenuItems: () => ['copy', 'paste', 'separator', 'export'], getContextMenuItems: () => ['copy', 'paste', 'separator', 'export'],

20
src/lib/number.ts Normal file
View File

@ -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
}

View File

@ -1,19 +1,42 @@
export const formatThousands = (value: unknown, fractionDigits = 2) => { import { parseNumberOrNull } from '@/lib/number'
if (value === '' || value == null) return ''
const numericValue = Number(value) const fixedFormatterCache = new Map<number, Intl.NumberFormat>()
if (!Number.isFinite(numericValue)) return '' const flexibleFormatterCache = new Map<number, Intl.NumberFormat>()
return numericValue.toLocaleString('zh-CN', {
const getFixedFormatter = (fractionDigits: number) => {
if (!fixedFormatterCache.has(fractionDigits)) {
fixedFormatterCache.set(
fractionDigits,
new Intl.NumberFormat('zh-CN', {
minimumFractionDigits: fractionDigits, minimumFractionDigits: fractionDigits,
maximumFractionDigits: fractionDigits maximumFractionDigits: fractionDigits
}) })
)
}
return fixedFormatterCache.get(fractionDigits)!
} }
export const formatThousandsFlexible = (value: unknown, maxFractionDigits = 20) => { const getFlexibleFormatter = (maxFractionDigits: number) => {
if (value === '' || value == null) return '' if (!flexibleFormatterCache.has(maxFractionDigits)) {
const numericValue = Number(value) flexibleFormatterCache.set(
if (!Number.isFinite(numericValue)) return '' maxFractionDigits,
return numericValue.toLocaleString('zh-CN', { new Intl.NumberFormat('zh-CN', {
minimumFractionDigits: 0, minimumFractionDigits: 0,
maximumFractionDigits: maxFractionDigits maximumFractionDigits: maxFractionDigits
}) })
)
}
return flexibleFormatterCache.get(maxFractionDigits)!
}
export const formatThousands = (value: unknown, fractionDigits = 2) => {
const numericValue = parseNumberOrNull(value)
if (numericValue == null) return ''
return getFixedFormatter(fractionDigits).format(numericValue)
}
export const formatThousandsFlexible = (value: unknown, maxFractionDigits = 20) => {
const numericValue = parseNumberOrNull(value)
if (numericValue == null) return ''
return getFlexibleFormatter(maxFractionDigits).format(numericValue)
} }

View File

@ -1,6 +1,8 @@
import localforage from 'localforage' import localforage from 'localforage'
import { expertList, getBasicFeeFromScale, majorList, serviceList, taskList } from '@/sql' import { expertList, majorList, serviceList, taskList } from '@/sql'
import { addNumbers, roundTo, sumByNumber, toDecimal } from '@/lib/decimal' import { roundTo, sumByNumber, toDecimal } from '@/lib/decimal'
import { toFiniteNumberOrNull } from '@/lib/number'
import { getBenchmarkBudgetByScale, getScaleBudgetFee } from '@/lib/pricingScaleFee'
interface StoredDetailRowsState<T = any> { interface StoredDetailRowsState<T = any> {
detailRows?: T[] detailRows?: T[]
@ -58,12 +60,17 @@ export interface PricingMethodTotals {
hourly: number | null hourly: number | null
} }
const toFiniteNumberOrNull = (value: unknown): number | null =>
typeof value === 'number' && Number.isFinite(value) ? value : null
const hasOwn = (obj: unknown, key: string) => const hasOwn = (obj: unknown, key: string) =>
Object.prototype.hasOwnProperty.call(obj || {}, key) Object.prototype.hasOwnProperty.call(obj || {}, key)
const toRowMap = <TRow extends { id: string }>(rows?: TRow[]) => {
const map = new Map<string, TRow>()
for (const row of rows || []) {
map.set(String(row.id), row)
}
return map
}
const getDefaultConsultCategoryFactor = (serviceId: string | number) => { const getDefaultConsultCategoryFactor = (serviceId: string | number) => {
const service = (serviceList as Record<string, ServiceLite | undefined>)[String(serviceId)] const service = (serviceList as Record<string, ServiceLite | undefined>)[String(serviceId)]
return toFiniteNumberOrNull(service?.defCoe) return toFiniteNumberOrNull(service?.defCoe)
@ -95,10 +102,7 @@ const mergeScaleRows = (
serviceId: string | number, serviceId: string | number,
rowsFromDb: Array<Partial<ScaleRow> & Pick<ScaleRow, 'id'>> | undefined rowsFromDb: Array<Partial<ScaleRow> & Pick<ScaleRow, 'id'>> | undefined
): ScaleRow[] => { ): ScaleRow[] => {
const dbValueMap = new Map<string, Partial<ScaleRow> & Pick<ScaleRow, 'id'>>() const dbValueMap = toRowMap(rowsFromDb)
for (const row of rowsFromDb || []) {
dbValueMap.set(String(row.id), row)
}
const defaultConsultCategoryFactor = getDefaultConsultCategoryFactor(serviceId) const defaultConsultCategoryFactor = getDefaultConsultCategoryFactor(serviceId)
return buildDefaultScaleRows(serviceId).map(row => { return buildDefaultScaleRows(serviceId).map(row => {
@ -122,26 +126,26 @@ const mergeScaleRows = (
}) })
} }
const getBenchmarkBudgetByAmount = (amount: MaybeNumber) => { const getBenchmarkBudgetByAmount = (amount: MaybeNumber) =>
const result = getBasicFeeFromScale(amount, 'cost') getBenchmarkBudgetByScale(amount, 'cost')
return result ? roundTo(addNumbers(result.basic, result.optional), 2) : null
}
const getBenchmarkBudgetByLandArea = (landArea: MaybeNumber) => { const getBenchmarkBudgetByLandArea = (landArea: MaybeNumber) =>
const result = getBasicFeeFromScale(landArea, 'area') getBenchmarkBudgetByScale(landArea, 'area')
return result ? roundTo(addNumbers(result.basic, result.optional), 2) : null
}
const getInvestmentBudgetFee = (row: ScaleRow) => { const getInvestmentBudgetFee = (row: ScaleRow) => {
const benchmarkBudget = getBenchmarkBudgetByAmount(row.amount) return getScaleBudgetFee({
if (benchmarkBudget == null || row.majorFactor == null || row.consultCategoryFactor == null) return null benchmarkBudget: getBenchmarkBudgetByAmount(row.amount),
return roundTo(toDecimal(benchmarkBudget).mul(row.majorFactor).mul(row.consultCategoryFactor), 2) majorFactor: row.majorFactor,
consultCategoryFactor: row.consultCategoryFactor
})
} }
const getLandBudgetFee = (row: ScaleRow) => { const getLandBudgetFee = (row: ScaleRow) => {
const benchmarkBudget = getBenchmarkBudgetByLandArea(row.landArea) return getScaleBudgetFee({
if (benchmarkBudget == null || row.majorFactor == null || row.consultCategoryFactor == null) return null benchmarkBudget: getBenchmarkBudgetByLandArea(row.landArea),
return roundTo(toDecimal(benchmarkBudget).mul(row.majorFactor).mul(row.consultCategoryFactor), 2) majorFactor: row.majorFactor,
consultCategoryFactor: row.consultCategoryFactor
})
} }
const getTaskEntriesByServiceId = (serviceId: string | number) => const getTaskEntriesByServiceId = (serviceId: string | number) =>
@ -164,10 +168,7 @@ const mergeWorkloadRows = (
serviceId: string | number, serviceId: string | number,
rowsFromDb: Array<Partial<WorkloadRow> & Pick<WorkloadRow, 'id'>> | undefined rowsFromDb: Array<Partial<WorkloadRow> & Pick<WorkloadRow, 'id'>> | undefined
): WorkloadRow[] => { ): WorkloadRow[] => {
const dbValueMap = new Map<string, Partial<WorkloadRow> & Pick<WorkloadRow, 'id'>>() const dbValueMap = toRowMap(rowsFromDb)
for (const row of rowsFromDb || []) {
dbValueMap.set(String(row.id), row)
}
return buildDefaultWorkloadRows(serviceId).map(row => { return buildDefaultWorkloadRows(serviceId).map(row => {
const fromDb = dbValueMap.get(row.id) const fromDb = dbValueMap.get(row.id)
@ -216,10 +217,7 @@ const buildDefaultHourlyRows = (): HourlyRow[] =>
const mergeHourlyRows = ( const mergeHourlyRows = (
rowsFromDb: Array<Partial<HourlyRow> & Pick<HourlyRow, 'id'>> | undefined rowsFromDb: Array<Partial<HourlyRow> & Pick<HourlyRow, 'id'>> | undefined
): HourlyRow[] => { ): HourlyRow[] => {
const dbValueMap = new Map<string, Partial<HourlyRow> & Pick<HourlyRow, 'id'>>() const dbValueMap = toRowMap(rowsFromDb)
for (const row of rowsFromDb || []) {
dbValueMap.set(String(row.id), row)
}
return buildDefaultHourlyRows().map(row => { return buildDefaultHourlyRows().map(row => {
const fromDb = dbValueMap.get(row.id) 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) 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: { export const getPricingMethodTotalsForService = async (params: {
contractId: string contractId: string
serviceId: string | number serviceId: string | number
@ -258,20 +270,11 @@ export const getPricingMethodTotalsForService = async (params: {
localforage.getItem<StoredDetailRowsState>(htDbKey) localforage.getItem<StoredDetailRowsState>(htDbKey)
]) ])
const investRows = // 优先使用对应计费页的数据;不存在时回退合同段规模信息,再回退默认字典行。
investData?.detailRows != null const investRows = resolveScaleRows(serviceId, investData, htData)
? mergeScaleRows(serviceId, investData.detailRows as any)
: htData?.detailRows != null
? mergeScaleRows(serviceId, htData.detailRows as any)
: buildDefaultScaleRows(serviceId)
const investScale = sumByNumber(investRows, row => getInvestmentBudgetFee(row)) const investScale = sumByNumber(investRows, row => getInvestmentBudgetFee(row))
const landRows = const landRows = resolveScaleRows(serviceId, landData, htData)
landData?.detailRows != null
? mergeScaleRows(serviceId, landData.detailRows as any)
: htData?.detailRows != null
? mergeScaleRows(serviceId, htData.detailRows as any)
: buildDefaultScaleRows(serviceId)
const landScale = sumByNumber(landRows, row => getLandBudgetFee(row)) const landScale = sumByNumber(landRows, row => getLandBudgetFee(row))
const defaultWorkloadRows = buildDefaultWorkloadRows(serviceId) const defaultWorkloadRows = buildDefaultWorkloadRows(serviceId)

View File

@ -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)
}

View File

@ -1,5 +1,6 @@
import localforage from 'localforage' import localforage from 'localforage'
import { majorList, serviceList } from '@/sql' import { majorList, serviceList } from '@/sql'
import { toFiniteNumberOrNull } from '@/lib/number'
const CONSULT_CATEGORY_FACTOR_KEY = 'xm-consult-category-factor-v1' const CONSULT_CATEGORY_FACTOR_KEY = 'xm-consult-category-factor-v1'
const MAJOR_FACTOR_KEY = 'xm-major-factor-v1' const MAJOR_FACTOR_KEY = 'xm-major-factor-v1'
@ -20,9 +21,6 @@ type FactorDictItem = {
type FactorDict = Record<string, FactorDictItem> type FactorDict = Record<string, FactorDictItem>
const toFiniteNumberOrNull = (value: unknown): number | null =>
typeof value === 'number' && Number.isFinite(value) ? value : null
const buildStandardFactorMap = (dict: FactorDict): Map<string, number | null> => { const buildStandardFactorMap = (dict: FactorDict): Map<string, number | null> => {
const map = new Map<string, number | null>() const map = new Map<string, number | null>()
for (const [id, item] of Object.entries(dict)) { for (const [id, item] of Object.entries(dict)) {

View File

@ -1,4 +1,5 @@
import localforage from 'localforage' import localforage from 'localforage'
import { toFiniteNumberOrNull } from '@/lib/number'
export type ZxFwPricingField = 'investScale' | 'landScale' | 'workload' | 'hourly' export type ZxFwPricingField = 'investScale' | 'landScale' | 'workload' | 'hourly'
@ -18,9 +19,6 @@ interface ZxFwState {
export const ZXFW_RELOAD_SERVICE_KEY = 'zxfw-main' 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: { export const syncPricingTotalToZxFw = async (params: {
contractId: string contractId: string
serviceId: string | number serviceId: string | number

View File

@ -1,18 +1,18 @@
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import { createPinia } from 'pinia'
import { import {
ModuleRegistry, CellStyleModule,
ClientSideRowModelModule, ClientSideRowModelModule,
ColumnAutoSizeModule, ColumnAutoSizeModule,
CsvExportModule, CsvExportModule,
LargeTextEditorModule, LargeTextEditorModule,
LocaleModule,
ModuleRegistry,
NumberEditorModule, NumberEditorModule,
PinnedRowModule, PinnedRowModule,
RowAutoHeightModule,
TextEditorModule, TextEditorModule,
TooltipModule, TooltipModule,
UndoRedoEditModule,ValidationModule,LocaleModule ,CellStyleModule ,RowAutoHeightModule UndoRedoEditModule,
ValidationModule
} from 'ag-grid-community' } from 'ag-grid-community'
import { import {
AggregationModule, AggregationModule,
@ -24,18 +24,26 @@ import {
RowGroupingModule, RowGroupingModule,
TreeDataModule TreeDataModule
} from 'ag-grid-enterprise' } from 'ag-grid-enterprise'
LicenseManager.setLicenseKey("[v3][RELEASE][0102]_NDg2Njc4MzY3MDgzNw==16d78ca762fb5d2ff740aed081e2af7b") import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' // 引入 import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
const pinia = createPinia() import { createApp } from 'vue'
pinia.use(piniaPluginPersistedstate) import App from './App.vue'
ModuleRegistry.registerModules([ import './style.css'
LicenseManager.setLicenseKey(
'[v3][RELEASE][0102]_NDg2Njc4MzY3MDgzNw==16d78ca762fb5d2ff740aed081e2af7b'
)
const AG_GRID_MODULES = [
ClientSideRowModelModule, ClientSideRowModelModule,
ColumnAutoSizeModule, ColumnAutoSizeModule,
CsvExportModule, CsvExportModule,
TextEditorModule, TextEditorModule,
NumberEditorModule,RowAutoHeightModule, NumberEditorModule,
RowAutoHeightModule,
LargeTextEditorModule, LargeTextEditorModule,
UndoRedoEditModule,CellStyleModule , UndoRedoEditModule,
CellStyleModule,
PinnedRowModule, PinnedRowModule,
TooltipModule, TooltipModule,
TreeDataModule, TreeDataModule,
@ -44,8 +52,15 @@ ModuleRegistry.registerModules([
MenuModule, MenuModule,
CellSelectionModule, CellSelectionModule,
ContextMenuModule, ContextMenuModule,
ClipboardModule,LocaleModule , ClipboardModule,
LocaleModule,
ValidationModule ValidationModule
]) ]
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
// 在应用启动时一次性注册 AG Grid 运行所需模块。
ModuleRegistry.registerModules(AG_GRID_MODULES)
createApp(App).use(pinia).mount('#app') createApp(App).use(pinia).mount('#app')

View File

@ -1,94 +1,96 @@
// src/stores/tab.ts
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { ref } from 'vue' import { ref } from 'vue'
export const useTabStore = defineStore('tabs', () => { export interface TabItem<TProps = Record<string, unknown>> {
interface TabItem<T = Record<string, any>> { id: string
id: string; // 标签唯一标识 title: string
title: string; // 标签标题 componentName: string
componentName: string; // 组件名称 props?: TProps
props?: T; // 传递给组件的 props可选泛型适配不同组件
} }
const defaultTabs :TabItem[]= [
{ id: 'XmView', title: '项目卡片', componentName: 'XmView' }
]
const tabs = ref([ const HOME_TAB_ID = 'XmView'
...defaultTabs const DEFAULT_TAB: TabItem = {
]) id: HOME_TAB_ID,
const activeTabId = ref('XmView') title: '项目卡片',
componentName: HOME_TAB_ID
}
const createDefaultTabs = (): TabItem[] => [{ ...DEFAULT_TAB }]
export const useTabStore = defineStore(
'tabs',
() => {
const tabs = ref<TabItem[]>(createDefaultTabs())
const activeTabId = ref(HOME_TAB_ID)
const ensureHomeTab = () => {
if (tabs.value.some(tab => tab.id === HOME_TAB_ID)) return
tabs.value = [...createDefaultTabs(), ...tabs.value]
}
const ensureActiveValid = () => { const ensureActiveValid = () => {
const activeExists = tabs.value.some(t => t.id === activeTabId.value) ensureHomeTab()
if (!activeExists) { if (tabs.value.length === 0) tabs.value = createDefaultTabs()
activeTabId.value = tabs.value[0]?.id || 'XmView' if (!tabs.value.some(tab => tab.id === activeTabId.value)) {
activeTabId.value = tabs.value[0]?.id ?? HOME_TAB_ID
} }
} }
const openTab = (config: { id: string; title: string; componentName: string; props?: any }) => { const openTab = (config: TabItem) => {
const exists = tabs.value.some(t => t.id === config.id) if (!tabs.value.some(tab => tab.id === config.id)) {
if (!exists) {
tabs.value = [...tabs.value, config] tabs.value = [...tabs.value, config]
} }
activeTabId.value = config.id activeTabId.value = config.id
} }
const removeTab = (id: string) => { const removeTab = (id: string) => {
if (id === 'XmView') return // 首页不可删除 // 首页标签固定保留,不允许关闭。
const index = tabs.value.findIndex(t => t.id === id) if (id === HOME_TAB_ID) return
if (index < 0) return
const wasActive = activeTabId.value === id
tabs.value = tabs.value.filter(t => t.id !== id)
if (tabs.value.length === 0) { const index = tabs.value.findIndex(tab => tab.id === id)
tabs.value = [...defaultTabs] if (index < 0) return
}
const wasActive = activeTabId.value === id
tabs.value = tabs.value.filter(tab => tab.id !== id)
ensureHomeTab()
if (wasActive) { if (wasActive) {
const fallbackIndex = Math.max(0, Math.min(index - 1, tabs.value.length - 1)) const fallbackIndex = Math.max(0, Math.min(index - 1, tabs.value.length - 1))
activeTabId.value = tabs.value[fallbackIndex]?.id || 'XmView' activeTabId.value = tabs.value[fallbackIndex]?.id ?? HOME_TAB_ID
return return
} }
const activeStillExists = tabs.value.some(t => t.id === activeTabId.value) ensureActiveValid()
if (!activeStillExists) {
activeTabId.value = tabs.value[0]?.id || 'XmView'
}
} }
const closeAllTabs = () => { const closeAllTabs = () => {
tabs.value = tabs.value.filter(t => t.id === 'XmView') tabs.value = createDefaultTabs()
if (tabs.value.length === 0) tabs.value = [...defaultTabs] activeTabId.value = HOME_TAB_ID
activeTabId.value = 'XmView'
} }
const closeLeftTabs = (targetId: string) => { const closeLeftTabs = (targetId: string) => {
const targetIndex = tabs.value.findIndex(t => t.id === targetId) const targetIndex = tabs.value.findIndex(tab => tab.id === targetId)
if (targetIndex < 0) return if (targetIndex < 0) return
tabs.value = tabs.value.filter((tab, index) => tab.id === 'XmView' || index >= targetIndex) tabs.value = tabs.value.filter((tab, index) => tab.id === HOME_TAB_ID || index >= targetIndex)
ensureActiveValid() ensureActiveValid()
} }
const closeRightTabs = (targetId: string) => { const closeRightTabs = (targetId: string) => {
const targetIndex = tabs.value.findIndex(t => t.id === targetId) const targetIndex = tabs.value.findIndex(tab => tab.id === targetId)
if (targetIndex < 0) return if (targetIndex < 0) return
tabs.value = tabs.value.filter((tab, index) => tab.id === 'XmView' || index <= targetIndex) tabs.value = tabs.value.filter((tab, index) => tab.id === HOME_TAB_ID || index <= targetIndex)
ensureActiveValid() ensureActiveValid()
} }
const closeOtherTabs = (targetId: string) => { const closeOtherTabs = (targetId: string) => {
tabs.value = tabs.value.filter(tab => tab.id === 'XmView' || tab.id === targetId) tabs.value = tabs.value.filter(tab => tab.id === HOME_TAB_ID || tab.id === targetId)
if (tabs.value.length === 0) tabs.value = [...defaultTabs] ensureHomeTab()
if (targetId === 'XmView') { activeTabId.value = tabs.value.some(tab => tab.id === targetId) ? targetId : HOME_TAB_ID
activeTabId.value = 'XmView'
return
}
activeTabId.value = tabs.value.some(t => t.id === targetId) ? targetId : 'XmView'
} }
const resetTabs = () => { const resetTabs = () => {
tabs.value = [...defaultTabs] tabs.value = createDefaultTabs()
activeTabId.value = 'XmView' activeTabId.value = HOME_TAB_ID
} }
return { return {
@ -102,11 +104,12 @@ export const useTabStore = defineStore('tabs', () => {
closeOtherTabs, closeOtherTabs,
resetTabs resetTabs
} }
}, { },
// --- 关键配置:开启持久化 --- {
persist: { persist: {
key: 'tabs', // 存储在 localStorage 里的 key key: 'tabs',
storage: localStorage, // 也可以改用 sessionStorage storage: localStorage,
pick: ['tabs', 'activeTabId'], // 指定哪些变量需要持久化 pick: ['tabs', 'activeTabId']
} }
}) }
)

View File

@ -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) { if (window.showSaveFilePicker) {
const handle = await window.showSaveFilePicker({ const handle = await window.showSaveFilePicker({
suggestedName: fileName, suggestedName: fileName,
@ -817,15 +817,15 @@ function cloneCellValue(value) {
} }
//demo
let data1 = { let data1 = {
name: 'test001', name: 'test001',//项目名称
fee: 10000, fee: 10000, //所有合同段总费用
scale: [ scale: [//项目明细aggrid数据
{ {
major: 0, major: 0, //专业id对应专业majorList中的key
cost: 100000, cost: 100000,//造价金额
area: 200, area: 200,//用地面积
}, },
{ {
major: 1, major: 1,
@ -833,15 +833,15 @@ let data1 = {
area: 200, area: 200,
}, },
], ],
contracts: [ contracts: [//合同段数据
{ {
name: 'A合同段', name: 'A合同段',//合同段名称
fee: 10000, fee: 10000,//合同段费用该合同段咨询服务zxfw.vue里面aggrid的合同预算行的小计
scale: [ scale: [//合同段明细aggrid数据htinfo.vue
{ {
major: 0, major: 0, //专业id对应专业majorList中的key
cost: 100000, cost: 100000,//造价金额
area: 200, area: 200,//用地面积
}, },
{ {
major: 1, major: 1,
@ -849,12 +849,12 @@ let data1 = {
area: 200, area: 200,
}, },
], ],
services: [ services: [//咨询服务数据zxfw.vue里面aggrid的数据不用输出合同预算
{ {
id: 0, id: 0, //服务id对应serviceList中的key
fee: 100000, fee: 100000 ,//服务费用(该服务咨询服务小计)
method1: { // 投资规模法 method1: { // 投资规模法InvestmentScalePricingPane.vue的数据
cost: 100000, cost: 100000, //zxfw.vue里面aggrid该数据的投资规模法金额
basicFee: 200, basicFee: 200,
basicFee_basic: 200, basicFee_basic: 200,
basicFee_optional: 0, basicFee_optional: 0,