This commit is contained in:
wintsa 2026-03-03 16:16:16 +08:00
parent 62546bc937
commit 33913c29d2
9 changed files with 1153 additions and 239 deletions

View File

@ -8,6 +8,26 @@
</head>
<body>
<div id="app"></div>
<script>
;(() => {
const makeVisitVersion = () => {
if (window.crypto && typeof window.crypto.getRandomValues === 'function') {
const bytes = new Uint32Array(2)
window.crypto.getRandomValues(bytes)
return `${Date.now().toString(36)}-${bytes[0].toString(36)}${bytes[1].toString(36)}`
}
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`
}
const url = new URL(window.location.href)
url.searchParams.set('v', makeVisitVersion())
const nextUrl = `${url.pathname}${url.search}${url.hash}`
const currentUrl = `${window.location.pathname}${window.location.search}${window.location.hash}`
if (nextUrl !== currentUrl) {
window.history.replaceState(null, '', nextUrl)
}
})()
</script>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View File

@ -1,7 +1,7 @@
<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { AgGridVue } from 'ag-grid-vue3'
import type { ColDef } from 'ag-grid-community'
import type { ColDef, ColGroupDef } from 'ag-grid-community'
import localforage from 'localforage'
import { majorList } from '@/sql'
import { myTheme, gridOptions } from '@/lib/diyAgGridOptions'
@ -11,7 +11,7 @@ import { syncPricingTotalToZxFw, ZXFW_RELOAD_SERVICE_KEY } from '@/lib/zxFwPrici
import { usePricingPaneReloadStore } from '@/pinia/pricingPaneReload'
import { loadConsultCategoryFactorMap, loadMajorFactorMap } from '@/lib/xmFactorDefaults'
import { parseNumberOrNull } from '@/lib/number'
import { getBenchmarkBudgetByScale, getScaleBudgetFee } from '@/lib/pricingScaleFee'
import { getBenchmarkBudgetSplitByScale, getScaleBudgetFeeSplit } from '@/lib/pricingScaleFee'
import { Button } from '@/components/ui/button'
import {
AlertDialogAction,
@ -49,9 +49,15 @@ interface DetailRow {
majorName: string
amount: number | null
benchmarkBudget: number | null
benchmarkBudgetBasic: number | null
benchmarkBudgetOptional: number | null
basicFormula: string
optionalFormula: string
consultCategoryFactor: number | null
majorFactor: number | null
budgetFee: number | null
budgetFeeBasic: number | null
budgetFeeOptional: number | null
remark: string
path: string[]
}
@ -197,9 +203,15 @@ const buildDefaultRows = (): DetailRow[] => {
majorName: child.name,
amount: null,
benchmarkBudget: null,
benchmarkBudgetBasic: null,
benchmarkBudgetOptional: null,
basicFormula: '',
optionalFormula: '',
consultCategoryFactor: null,
majorFactor: null,
budgetFee: null,
budgetFeeBasic: null,
budgetFeeOptional: null,
remark: '',
path: [group.id, child.id]
})
@ -208,7 +220,24 @@ const buildDefaultRows = (): DetailRow[] => {
return rows
}
type SourceRow = Pick<DetailRow, 'id'> & Partial<Pick<DetailRow, 'amount' | 'benchmarkBudget' | 'consultCategoryFactor' | 'majorFactor' | 'budgetFee' | 'remark'>>
type SourceRow = Pick<DetailRow, 'id'> &
Partial<
Pick<
DetailRow,
| 'amount'
| 'benchmarkBudget'
| 'benchmarkBudgetBasic'
| 'benchmarkBudgetOptional'
| 'basicFormula'
| 'optionalFormula'
| 'consultCategoryFactor'
| 'majorFactor'
| 'budgetFee'
| 'budgetFeeBasic'
| 'budgetFeeOptional'
| 'remark'
>
>
const mergeWithDictRows = (
rowsFromDb: SourceRow[] | undefined,
options?: { includeAmount?: boolean; includeFactorValues?: boolean }
@ -230,6 +259,10 @@ const mergeWithDictRows = (
...row,
amount: includeAmount && typeof fromDb.amount === 'number' ? fromDb.amount : 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,
basicFormula: typeof fromDb.basicFormula === 'string' ? fromDb.basicFormula : '',
optionalFormula: typeof fromDb.optionalFormula === 'string' ? fromDb.optionalFormula : '',
consultCategoryFactor:
!includeFactorValues
? null
@ -247,6 +280,8 @@ const mergeWithDictRows = (
? null
: getDefaultMajorFactorById(row.id),
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 : ''
}
})
@ -254,7 +289,7 @@ const mergeWithDictRows = (
const formatEditableNumber = (params: any) => {
if (!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')) {
return '点击输入'
return '输入'
}
if (params.value == null) return ''
return Number(params.value).toFixed(2)
@ -281,18 +316,34 @@ const formatReadonlyMoney = (params: any) => {
return formatThousands(roundTo(params.value, 2))
}
const getBenchmarkBudgetByAmount = (row?: Pick<DetailRow, 'amount'>) =>
getBenchmarkBudgetByScale(row?.amount, 'cost')
const getBenchmarkBudgetSplitByAmount = (row?: Pick<DetailRow, 'amount'>) =>
getBenchmarkBudgetSplitByScale(row?.amount, 'cost')
const getBudgetFee = (row?: Pick<DetailRow, 'amount' | 'majorFactor' | 'consultCategoryFactor'>) => {
return getScaleBudgetFee({
benchmarkBudget: getBenchmarkBudgetByAmount(row),
const benchmarkBudgetSplit = getBenchmarkBudgetSplitByAmount(row)
if (!benchmarkBudgetSplit) return null
const splitBudgetFee = getScaleBudgetFeeSplit({
benchmarkBudgetBasic: benchmarkBudgetSplit.basic,
benchmarkBudgetOptional: benchmarkBudgetSplit.optional,
majorFactor: row?.majorFactor,
consultCategoryFactor: row?.consultCategoryFactor
})
return splitBudgetFee ? splitBudgetFee.total : null
}
const getBudgetFeeSplit = (row?: Pick<DetailRow, 'amount' | 'majorFactor' | 'consultCategoryFactor'>) => {
const benchmarkBudgetSplit = getBenchmarkBudgetSplitByAmount(row)
if (!benchmarkBudgetSplit) return null
return getScaleBudgetFeeSplit({
benchmarkBudgetBasic: benchmarkBudgetSplit.basic,
benchmarkBudgetOptional: benchmarkBudgetSplit.optional,
majorFactor: row?.majorFactor,
consultCategoryFactor: row?.consultCategoryFactor
})
}
const columnDefs: ColDef<DetailRow>[] = [
const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
{
headerName: '造价金额(万元)',
field: 'amount',
@ -310,18 +361,7 @@ const columnDefs: ColDef<DetailRow>[] = [
valueParser: params => parseNumberOrNull(params.newValue),
valueFormatter: formatEditableMoney
},
{
headerName: '基准预算(元)',
field: 'benchmarkBudget',
headerClass: 'ag-right-aligned-header',
minWidth: 100,
flex: 2,
cellClass: 'ag-right-aligned-cell',
aggFunc: decimalAggSum,
valueGetter: params => getBenchmarkBudgetByAmount(params.data),
valueParser: params => parseNumberOrNull(params.newValue),
valueFormatter: formatReadonlyMoney
},
{
headerName: '咨询分类系数',
field: 'consultCategoryFactor',
@ -353,16 +393,88 @@ const columnDefs: ColDef<DetailRow>[] = [
valueFormatter: formatMajorFactor
},
{
headerName: '预算费用',
field: 'budgetFee',
headerName: '基准预算(元)',
marryChildren: true,
children: [
{
headerName: '基本工作',
field: 'benchmarkBudgetBasic',
colId: 'benchmarkBudgetBasic',
headerClass: 'ag-right-aligned-header',
minWidth: 100,
flex:2,
minWidth: 130,
flex: 1,
cellClass: 'ag-right-aligned-cell',
aggFunc: decimalAggSum,
valueGetter: params =>
params.node?.rowPinned
? params.data?.benchmarkBudgetBasic ?? null
: getBenchmarkBudgetSplitByAmount(params.data)?.basic ?? null,
valueFormatter: formatReadonlyMoney
},
{
headerName: '可选工作',
field: 'benchmarkBudgetOptional',
colId: 'benchmarkBudgetOptional',
headerClass: 'ag-right-aligned-header',
minWidth: 130,
flex: 1,
cellClass: 'ag-right-aligned-cell',
aggFunc: decimalAggSum,
valueGetter: params =>
params.node?.rowPinned
? params.data?.benchmarkBudgetOptional ?? null
: getBenchmarkBudgetSplitByAmount(params.data)?.optional ?? null,
valueFormatter: formatReadonlyMoney
}
]
},
{
headerName: '预算费用',
marryChildren: true,
children: [
{
headerName: '基本工作',
field: 'budgetFeeBasic',
colId: 'budgetFeeBasic',
headerClass: 'ag-right-aligned-header',
minWidth: 120,
flex: 1,
cellClass: 'ag-right-aligned-cell',
aggFunc: decimalAggSum,
valueGetter: params =>
params.node?.rowPinned
? params.data?.budgetFeeBasic ?? null
: getBudgetFeeSplit(params.data)?.basic ?? null,
valueFormatter: formatReadonlyMoney
},
{
headerName: '可选工作',
field: 'budgetFeeOptional',
colId: 'budgetFeeOptional',
headerClass: 'ag-right-aligned-header',
minWidth: 120,
flex: 1,
cellClass: 'ag-right-aligned-cell',
aggFunc: decimalAggSum,
valueGetter: params =>
params.node?.rowPinned
? params.data?.budgetFeeOptional ?? null
: getBudgetFeeSplit(params.data)?.optional ?? null,
valueFormatter: formatReadonlyMoney
},
{
headerName: '合计',
field: 'budgetFee',
colId: 'budgetFeeTotal',
headerClass: 'ag-right-aligned-header',
minWidth: 130,
flex: 1,
cellClass: 'ag-right-aligned-cell',
aggFunc: decimalAggSum,
valueGetter: params => (params.node?.rowPinned ? params.data?.budgetFee ?? null : getBudgetFee(params.data)),
valueParser: params => parseNumberOrNull(params.newValue),
valueFormatter: formatReadonlyMoney
}
]
},
{
headerName: '说明',
@ -416,8 +528,12 @@ const autoGroupColumnDef: ColDef = {
const totalAmount = computed(() => sumByNumber(detailRows.value, row => row.amount))
const totalBenchmarkBudget = computed(() => sumByNumber(detailRows.value, row => getBenchmarkBudgetByAmount(row)))
const totalBenchmarkBudgetBasic = computed(() => sumByNumber(detailRows.value, row => getBenchmarkBudgetSplitByAmount(row)?.basic))
const totalBenchmarkBudgetOptional = computed(() => sumByNumber(detailRows.value, row => getBenchmarkBudgetSplitByAmount(row)?.optional))
const totalBenchmarkBudget = computed(() => sumByNumber(detailRows.value, row => getBenchmarkBudgetSplitByAmount(row)?.total))
const totalBudgetFeeBasic = computed(() => sumByNumber(detailRows.value, row => getBudgetFeeSplit(row)?.basic))
const totalBudgetFeeOptional = computed(() => sumByNumber(detailRows.value, row => getBudgetFeeSplit(row)?.optional))
const totalBudgetFee = computed(() => sumByNumber(detailRows.value, row => getBudgetFee(row)))
const pinnedTopRowData = computed(() => [
{
@ -428,21 +544,50 @@ const pinnedTopRowData = computed(() => [
majorName: '',
amount: totalAmount.value,
benchmarkBudget: totalBenchmarkBudget.value,
benchmarkBudgetBasic: totalBenchmarkBudgetBasic.value,
benchmarkBudgetOptional: totalBenchmarkBudgetOptional.value,
basicFormula: '',
optionalFormula: '',
consultCategoryFactor: null,
majorFactor: null,
budgetFee: totalBudgetFee.value,
budgetFeeBasic: totalBudgetFeeBasic.value,
budgetFeeOptional: totalBudgetFeeOptional.value,
remark: '',
path: ['TOTAL']
}
])
const buildPersistDetailRows = () =>
detailRows.value.map(row => {
const benchmarkBudgetSplit = getBenchmarkBudgetSplitByAmount(row)
const budgetFeeSplit = benchmarkBudgetSplit
? getScaleBudgetFeeSplit({
benchmarkBudgetBasic: benchmarkBudgetSplit.basic,
benchmarkBudgetOptional: benchmarkBudgetSplit.optional,
majorFactor: row.majorFactor,
consultCategoryFactor: row.consultCategoryFactor
})
: null
return {
...row,
benchmarkBudget: benchmarkBudgetSplit?.total ?? null,
benchmarkBudgetBasic: benchmarkBudgetSplit?.basic ?? null,
benchmarkBudgetOptional: benchmarkBudgetSplit?.optional ?? null,
basicFormula: benchmarkBudgetSplit?.basicFormula ?? '',
optionalFormula: benchmarkBudgetSplit?.optionalFormula ?? '',
budgetFee: budgetFeeSplit?.total ?? null,
budgetFeeBasic: budgetFeeSplit?.basic ?? null,
budgetFeeOptional: budgetFeeSplit?.optional ?? null
}
})
const saveToIndexedDB = async () => {
if (shouldSkipPersist()) return
try {
const payload = {
detailRows: JSON.parse(JSON.stringify(detailRows.value))
detailRows: JSON.parse(JSON.stringify(buildPersistDetailRows()))
}
console.log('Saving to IndexedDB:', payload)
await localforage.setItem(DB_KEY.value, payload)

View File

@ -1,7 +1,7 @@
<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { AgGridVue } from 'ag-grid-vue3'
import type { ColDef } from 'ag-grid-community'
import type { ColDef, ColGroupDef } from 'ag-grid-community'
import localforage from 'localforage'
import { majorList } from '@/sql'
import { myTheme, gridOptions } from '@/lib/diyAgGridOptions'
@ -11,7 +11,7 @@ import { syncPricingTotalToZxFw, ZXFW_RELOAD_SERVICE_KEY } from '@/lib/zxFwPrici
import { usePricingPaneReloadStore } from '@/pinia/pricingPaneReload'
import { loadConsultCategoryFactorMap, loadMajorFactorMap } from '@/lib/xmFactorDefaults'
import { parseNumberOrNull } from '@/lib/number'
import { getBenchmarkBudgetByScale, getScaleBudgetFee } from '@/lib/pricingScaleFee'
import { getBenchmarkBudgetSplitByScale, getScaleBudgetFeeSplit } from '@/lib/pricingScaleFee'
import { Button } from '@/components/ui/button'
import {
AlertDialogAction,
@ -50,9 +50,15 @@ interface DetailRow {
amount: number | null
landArea: number | null
benchmarkBudget: number | null
benchmarkBudgetBasic: number | null
benchmarkBudgetOptional: number | null
basicFormula: string
optionalFormula: string
consultCategoryFactor: number | null
majorFactor: number | null
budgetFee: number | null
budgetFeeBasic: number | null
budgetFeeOptional: number | null
remark: string
path: string[]
}
@ -199,9 +205,15 @@ const buildDefaultRows = (): DetailRow[] => {
amount: null,
landArea: null,
benchmarkBudget: null,
benchmarkBudgetBasic: null,
benchmarkBudgetOptional: null,
basicFormula: '',
optionalFormula: '',
consultCategoryFactor: null,
majorFactor: null,
budgetFee: null,
budgetFeeBasic: null,
budgetFeeOptional: null,
remark: '',
path: [group.id, child.id]
})
@ -210,7 +222,25 @@ const buildDefaultRows = (): DetailRow[] => {
return rows
}
type SourceRow = Pick<DetailRow, 'id'> & Partial<Pick<DetailRow, 'amount' | 'landArea' | 'benchmarkBudget' | 'consultCategoryFactor' | 'majorFactor' | 'budgetFee' | 'remark'>>
type SourceRow = Pick<DetailRow, 'id'> &
Partial<
Pick<
DetailRow,
| 'amount'
| 'landArea'
| 'benchmarkBudget'
| 'benchmarkBudgetBasic'
| 'benchmarkBudgetOptional'
| 'basicFormula'
| 'optionalFormula'
| 'consultCategoryFactor'
| 'majorFactor'
| 'budgetFee'
| 'budgetFeeBasic'
| 'budgetFeeOptional'
| 'remark'
>
>
const mergeWithDictRows = (
rowsFromDb: SourceRow[] | undefined,
options?: { includeScaleValues?: boolean; includeFactorValues?: boolean }
@ -233,6 +263,10 @@ const mergeWithDictRows = (
amount: includeScaleValues && typeof fromDb.amount === 'number' ? fromDb.amount : null,
landArea: includeScaleValues && 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,
basicFormula: typeof fromDb.basicFormula === 'string' ? fromDb.basicFormula : '',
optionalFormula: typeof fromDb.optionalFormula === 'string' ? fromDb.optionalFormula : '',
consultCategoryFactor:
!includeFactorValues
? null
@ -250,6 +284,8 @@ const mergeWithDictRows = (
? null
: getDefaultMajorFactorById(row.id),
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 : ''
}
})
@ -257,7 +293,7 @@ const mergeWithDictRows = (
const formatEditableNumber = (params: any) => {
if (!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')) {
return '点击输入'
return '输入'
}
if (params.value == null) return ''
return Number(params.value).toFixed(2)
@ -276,12 +312,28 @@ const formatReadonlyMoney = (params: any) => {
return formatThousands(roundTo(params.value, 2))
}
const getBenchmarkBudgetByLandArea = (row?: Pick<DetailRow, 'landArea'>) =>
getBenchmarkBudgetByScale(row?.landArea, 'area')
const getBenchmarkBudgetSplitByLandArea = (row?: Pick<DetailRow, 'landArea'>) =>
getBenchmarkBudgetSplitByScale(row?.landArea, 'area')
const getBudgetFee = (row?: Pick<DetailRow, 'landArea' | 'majorFactor' | 'consultCategoryFactor'>) => {
return getScaleBudgetFee({
benchmarkBudget: getBenchmarkBudgetByLandArea(row),
const benchmarkBudgetSplit = getBenchmarkBudgetSplitByLandArea(row)
if (!benchmarkBudgetSplit) return null
const splitBudgetFee = getScaleBudgetFeeSplit({
benchmarkBudgetBasic: benchmarkBudgetSplit.basic,
benchmarkBudgetOptional: benchmarkBudgetSplit.optional,
majorFactor: row?.majorFactor,
consultCategoryFactor: row?.consultCategoryFactor
})
return splitBudgetFee ? splitBudgetFee.total : null
}
const getBudgetFeeSplit = (row?: Pick<DetailRow, 'landArea' | 'majorFactor' | 'consultCategoryFactor'>) => {
const benchmarkBudgetSplit = getBenchmarkBudgetSplitByLandArea(row)
if (!benchmarkBudgetSplit) return null
return getScaleBudgetFeeSplit({
benchmarkBudgetBasic: benchmarkBudgetSplit.basic,
benchmarkBudgetOptional: benchmarkBudgetSplit.optional,
majorFactor: row?.majorFactor,
consultCategoryFactor: row?.consultCategoryFactor
})
@ -295,7 +347,7 @@ const formatEditableFlexibleNumber = (params: any) => {
return String(Number(params.value))
}
const columnDefs: ColDef<DetailRow>[] = [
const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
{
headerName: '用地面积(亩)',
field: 'landArea',
@ -312,18 +364,7 @@ const columnDefs: ColDef<DetailRow>[] = [
valueParser: params => parseNumberOrNull(params.newValue),
valueFormatter: formatEditableFlexibleNumber
},
{
headerName: '基准预算(元)',
field: 'benchmarkBudget',
headerClass: 'ag-right-aligned-header',
minWidth: 170,
flex: 1,
cellClass: 'ag-right-aligned-cell',
aggFunc: decimalAggSum,
valueGetter: params => getBenchmarkBudgetByLandArea(params.data),
valueParser: params => parseNumberOrNull(params.newValue),
valueFormatter: formatReadonlyMoney
},
{
headerName: '咨询分类系数',
field: 'consultCategoryFactor',
@ -355,16 +396,88 @@ const columnDefs: ColDef<DetailRow>[] = [
valueFormatter: formatMajorFactor
},
{
headerName: '预算费用',
field: 'budgetFee',
headerName: '基准预算(元)',
marryChildren: true,
children: [
{
headerName: '基本工作',
field: 'benchmarkBudgetBasic',
colId: 'benchmarkBudgetBasic',
headerClass: 'ag-right-aligned-header',
minWidth: 150,
minWidth: 140,
flex: 1,
cellClass: 'ag-right-aligned-cell',
aggFunc: decimalAggSum,
valueGetter: params =>
params.node?.rowPinned
? params.data?.benchmarkBudgetBasic ?? null
: getBenchmarkBudgetSplitByLandArea(params.data)?.basic ?? null,
valueFormatter: formatReadonlyMoney
},
{
headerName: '可选工作',
field: 'benchmarkBudgetOptional',
colId: 'benchmarkBudgetOptional',
headerClass: 'ag-right-aligned-header',
minWidth: 140,
flex: 1,
cellClass: 'ag-right-aligned-cell',
aggFunc: decimalAggSum,
valueGetter: params =>
params.node?.rowPinned
? params.data?.benchmarkBudgetOptional ?? null
: getBenchmarkBudgetSplitByLandArea(params.data)?.optional ?? null,
valueFormatter: formatReadonlyMoney
}
]
},
{
headerName: '预算费用',
marryChildren: true,
children: [
{
headerName: '基本工作',
field: 'budgetFeeBasic',
colId: 'budgetFeeBasic',
headerClass: 'ag-right-aligned-header',
minWidth: 130,
flex: 1,
cellClass: 'ag-right-aligned-cell',
aggFunc: decimalAggSum,
valueGetter: params =>
params.node?.rowPinned
? params.data?.budgetFeeBasic ?? null
: getBudgetFeeSplit(params.data)?.basic ?? null,
valueFormatter: formatReadonlyMoney
},
{
headerName: '可选工作',
field: 'budgetFeeOptional',
colId: 'budgetFeeOptional',
headerClass: 'ag-right-aligned-header',
minWidth: 130,
flex: 1,
cellClass: 'ag-right-aligned-cell',
aggFunc: decimalAggSum,
valueGetter: params =>
params.node?.rowPinned
? params.data?.budgetFeeOptional ?? null
: getBudgetFeeSplit(params.data)?.optional ?? null,
valueFormatter: formatReadonlyMoney
},
{
headerName: '合计',
field: 'budgetFee',
colId: 'budgetFeeTotal',
headerClass: 'ag-right-aligned-header',
minWidth: 140,
flex: 1,
cellClass: 'ag-right-aligned-cell',
aggFunc: decimalAggSum,
valueGetter: params => (params.node?.rowPinned ? params.data?.budgetFee ?? null : getBudgetFee(params.data)),
valueParser: params => parseNumberOrNull(params.newValue),
valueFormatter: formatReadonlyMoney
}
]
},
{
headerName: '说明',
@ -418,8 +531,12 @@ const totalAmount = computed(() => sumByNumber(detailRows.value, row => row.amou
const totalLandArea = computed(() => sumByNumber(detailRows.value, row => row.landArea))
const totalBenchmarkBudget = computed(() => sumByNumber(detailRows.value, row => getBenchmarkBudgetByLandArea(row)))
const totalBenchmarkBudgetBasic = computed(() => sumByNumber(detailRows.value, row => getBenchmarkBudgetSplitByLandArea(row)?.basic))
const totalBenchmarkBudgetOptional = computed(() => sumByNumber(detailRows.value, row => getBenchmarkBudgetSplitByLandArea(row)?.optional))
const totalBenchmarkBudget = computed(() => sumByNumber(detailRows.value, row => getBenchmarkBudgetSplitByLandArea(row)?.total))
const totalBudgetFeeBasic = computed(() => sumByNumber(detailRows.value, row => getBudgetFeeSplit(row)?.basic))
const totalBudgetFeeOptional = computed(() => sumByNumber(detailRows.value, row => getBudgetFeeSplit(row)?.optional))
const totalBudgetFee = computed(() => sumByNumber(detailRows.value, row => getBudgetFee(row)))
const pinnedTopRowData = computed(() => [
{
@ -431,21 +548,50 @@ const pinnedTopRowData = computed(() => [
amount: totalAmount.value,
landArea: totalLandArea.value,
benchmarkBudget: totalBenchmarkBudget.value,
benchmarkBudgetBasic: totalBenchmarkBudgetBasic.value,
benchmarkBudgetOptional: totalBenchmarkBudgetOptional.value,
basicFormula: '',
optionalFormula: '',
consultCategoryFactor: null,
majorFactor: null,
budgetFee: totalBudgetFee.value,
budgetFeeBasic: totalBudgetFeeBasic.value,
budgetFeeOptional: totalBudgetFeeOptional.value,
remark: '',
path: ['TOTAL']
}
])
const buildPersistDetailRows = () =>
detailRows.value.map(row => {
const benchmarkBudgetSplit = getBenchmarkBudgetSplitByLandArea(row)
const budgetFeeSplit = benchmarkBudgetSplit
? getScaleBudgetFeeSplit({
benchmarkBudgetBasic: benchmarkBudgetSplit.basic,
benchmarkBudgetOptional: benchmarkBudgetSplit.optional,
majorFactor: row.majorFactor,
consultCategoryFactor: row.consultCategoryFactor
})
: null
return {
...row,
benchmarkBudget: benchmarkBudgetSplit?.total ?? null,
benchmarkBudgetBasic: benchmarkBudgetSplit?.basic ?? null,
benchmarkBudgetOptional: benchmarkBudgetSplit?.optional ?? null,
basicFormula: benchmarkBudgetSplit?.basicFormula ?? '',
optionalFormula: benchmarkBudgetSplit?.optionalFormula ?? '',
budgetFee: budgetFeeSplit?.total ?? null,
budgetFeeBasic: budgetFeeSplit?.basic ?? null,
budgetFeeOptional: budgetFeeSplit?.optional ?? null
}
})
const saveToIndexedDB = async () => {
if (shouldSkipPersist()) return
try {
const payload = {
detailRows: JSON.parse(JSON.stringify(detailRows.value))
detailRows: JSON.parse(JSON.stringify(buildPersistDetailRows()))
}
console.log('Saving to IndexedDB:', payload)
await localforage.setItem(DB_KEY.value, payload)

View File

@ -21,6 +21,7 @@ interface DetailRow {
unit: string
conversion: number | null
workload: number | null
basicFee: number | null
budgetBase: string
budgetReferenceUnitPrice: string
budgetAdoptedUnitPrice: number | null
@ -142,6 +143,7 @@ const buildDefaultRows = (): DetailRow[] => {
unit: task.unit || '',
conversion: typeof task.conversion === 'number' && Number.isFinite(task.conversion) ? task.conversion : null,
workload: null,
basicFee: null,
budgetBase: task.basicParam || '',
budgetReferenceUnitPrice: formatTaskReferenceUnitPrice(task),
budgetAdoptedUnitPrice:
@ -171,6 +173,7 @@ const mergeWithDictRows = (rowsFromDb: DetailRow[] | undefined): DetailRow[] =>
return {
...row,
workload: typeof fromDb.workload === 'number' ? fromDb.workload : null,
basicFee: typeof fromDb.basicFee === 'number' ? fromDb.basicFee : null,
budgetAdoptedUnitPrice:
typeof fromDb.budgetAdoptedUnitPrice === 'number' ? fromDb.budgetAdoptedUnitPrice : null,
consultCategoryFactor:
@ -184,25 +187,36 @@ const mergeWithDictRows = (rowsFromDb: DetailRow[] | undefined): DetailRow[] =>
const parseSanitizedNumberOrNull = (value: unknown) =>
parseNumberOrNull(value, { sanitize: true })
const calcServiceFee = (row: DetailRow | undefined) => {
const calcBasicFee = (row: DetailRow | undefined) => {
if (!row || isNoTaskRow(row)) return null
const price = row.budgetAdoptedUnitPrice
const conversion = row.conversion
const workload = row.workload
const factor = row.consultCategoryFactor
if (
typeof price !== 'number' ||
!Number.isFinite(price) ||
typeof conversion !== 'number' ||
!Number.isFinite(conversion) ||
typeof workload !== 'number' ||
!Number.isFinite(workload) ||
!Number.isFinite(workload)
) {
return null
}
return roundTo(toDecimal(price).mul(conversion).mul(workload), 2)
}
const calcServiceFee = (row: DetailRow | undefined) => {
if (!row || isNoTaskRow(row)) return null
const factor = row.consultCategoryFactor
const basicFee = calcBasicFee(row)
if (
basicFee == null ||
typeof factor !== 'number' ||
!Number.isFinite(factor)
) {
return null
}
return roundTo(toDecimal(price).mul(conversion).mul(workload).mul(factor), 2)
return roundTo(toDecimal(basicFee).mul(factor), 2)
}
const formatEditableNumber = (params: any) => {
@ -368,6 +382,7 @@ const columnDefs: ColDef<DetailRow>[] = [
]
const totalWorkload = computed(() => sumByNumber(detailRows.value, row => row.workload))
const totalBasicFee = computed(() => sumByNumber(detailRows.value, row => calcBasicFee(row)))
const totalServiceFee = computed(() => sumByNumber(detailRows.value, row => calcServiceFee(row)))
const pinnedTopRowData = computed(() => [
@ -378,6 +393,7 @@ const pinnedTopRowData = computed(() => [
unit: '',
conversion: null,
workload: totalWorkload.value,
basicFee: totalBasicFee.value,
budgetBase: '',
budgetReferenceUnitPrice: '',
budgetAdoptedUnitPrice: null,
@ -390,12 +406,18 @@ const pinnedTopRowData = computed(() => [
const buildPersistDetailRows = () =>
detailRows.value.map(row => ({
...row,
basicFee: calcBasicFee(row),
serviceFee: calcServiceFee(row)
}))
const saveToIndexedDB = async () => {
if (!isWorkloadMethodApplicable.value) return
if (shouldSkipPersist()) return
try {
const payload = {
detailRows: JSON.parse(JSON.stringify(detailRows.value))
detailRows: JSON.parse(JSON.stringify(buildPersistDetailRows()))
}
console.log('Saving to IndexedDB:', payload)
await localforage.setItem(DB_KEY.value, payload)
@ -537,3 +559,4 @@ const mydiyTheme = myTheme.withParams({
</div>
</div>
</template>

View File

@ -19,6 +19,7 @@ import {
AlertDialogTrigger,
} from 'reka-ui'
import { decodeZwArchive, encodeZwArchive, ZW_FILE_EXTENSION } from '@/lib/zwArchive'
import { exportFile, serviceList } from '@/sql'
interface DataEntry {
key: string
@ -43,6 +44,176 @@ type XmInfoLike = {
projectName?: unknown
}
interface ScaleRowLike {
id: string
amount?: unknown
landArea?: unknown
}
interface XmInfoStorageLike extends XmInfoLike {
detailRows?: ScaleRowLike[]
}
interface ContractCardItem {
id: string
name?: string
order?: number
}
interface ZxFwRowLike {
id: string
subtotal?: unknown
investScale?: unknown
landScale?: unknown
workload?: unknown
hourly?: unknown
}
interface ZxFwStorageLike {
detailRows?: ZxFwRowLike[]
}
interface ScaleMethodRowLike extends ScaleRowLike {
basicFormula?: unknown
optionalFormula?: unknown
budgetFee?: unknown
budgetFeeBasic?: unknown
budgetFeeOptional?: unknown
consultCategoryFactor?: unknown
majorFactor?: unknown
remark?: unknown
}
interface WorkloadMethodRowLike {
id: string
budgetAdoptedUnitPrice?: unknown
workload?: unknown
basicFee?: unknown
consultCategoryFactor?: unknown
serviceFee?: unknown
remark?: unknown
}
interface HourlyMethodRowLike {
id: string
adoptedBudgetUnitPrice?: unknown
personnelCount?: unknown
workdayCount?: unknown
serviceBudget?: unknown
remark?: unknown
}
interface DetailRowsStorageLike<T> {
detailRows?: T[]
}
interface ExportScaleRow {
major: number
cost: number
area: number
}
interface ExportMethod1Detail {
major: number
cost: number
basicFee: number
basicFormula: string
basicFee_basic: number
optionalFormula: string
basicFee_optional: number
serviceCoe: number
majorCoe: number
fee: number
desc: string
}
interface ExportMethod1 {
cost: number
basicFee: number
basicFee_basic: number
basicFee_optional: number
fee: number
det: ExportMethod1Detail[]
}
interface ExportMethod2Detail {
major: number
area: number
basicFee: number
basicFormula: string
basicFee_basic: number
optionalFormula: string
basicFee_optional: number
serviceCoe: number
majorCoe: number
fee: number
desc: string
}
interface ExportMethod2 {
area: number
basicFee: number
basicFee_basic: number
basicFee_optional: number
fee: number
det: ExportMethod2Detail[]
}
interface ExportMethod3Detail {
task: number
price: number
amount: number
basicFee: number
serviceCoe: number
fee: number
desc: string
}
interface ExportMethod3 {
basicFee: number
fee: number
det: ExportMethod3Detail[]
}
interface ExportMethod4Detail {
expert: number
price: number
person_num: number
work_day: number
fee: number
desc: string
}
interface ExportMethod4 {
person_num: number
work_day: number
fee: number
det: ExportMethod4Detail[]
}
interface ExportService {
id: number
fee: number
method1?: ExportMethod1
method2?: ExportMethod2
method3?: ExportMethod3
method4?: ExportMethod4
}
interface ExportContract {
name: string
fee: number
scale: ExportScaleRow[]
services: ExportService[]
}
interface ExportReportPayload {
name: string
fee: number
scale: ExportScaleRow[]
contracts: ExportContract[]
}
const USER_GUIDE_COMPLETED_KEY = 'jgjs-user-guide-completed-v1'
const userGuideSteps: UserGuideStep[] = [
{
@ -453,6 +624,321 @@ const getExportProjectName = (entries: DataEntry[]): string => {
return typeof data.projectName === 'string' ? sanitizeFileNamePart(data.projectName) : '造价项目'
}
const toFiniteNumber = (value: unknown): number | null => {
const num = Number(value)
return Number.isFinite(num) ? num : null
}
const toFiniteNumberOrZero = (value: unknown): number => toFiniteNumber(value) ?? 0
const toSafeInteger = (value: unknown): number | null => {
const num = Number(value)
if (!Number.isInteger(num)) return null
if (!Number.isSafeInteger(num)) return null
return num
}
const sumNumbers = (values: Array<number | null | undefined>): number =>
values.reduce<number>(
(sum, value) => sum + (typeof value === 'number' && Number.isFinite(value) ? value : 0),
0
)
const isNonEmptyString = (value: unknown): value is string =>
typeof value === 'string' && value.trim().length > 0
const getTaskIdFromRowId = (value: string): number | null => {
const match = /^task-(\d+)-\d+$/.exec(value)
return match ? toSafeInteger(match[1]) : null
}
const getExpertIdFromRowId = (value: string): number | null => {
const match = /^expert-(\d+)$/.exec(value)
return match ? toSafeInteger(match[1]) : null
}
const hasServiceId = (serviceId: string) =>
Object.prototype.hasOwnProperty.call(serviceList as Record<string, unknown>, serviceId)
const buildScaleRows = (rows: ScaleRowLike[] | undefined): ExportScaleRow[] => {
if (!Array.isArray(rows)) return []
return rows
.map(row => {
const major = toSafeInteger(row.id)
const cost = toFiniteNumber(row.amount)
const area = toFiniteNumber(row.landArea)
if (major == null || (cost == null && area == null)) return null
return {
major,
cost: cost ?? 0,
area: area ?? 0
}
})
.filter((item): item is ExportScaleRow => Boolean(item))
}
const buildMethod1 = (rows: ScaleMethodRowLike[] | undefined): ExportMethod1 | null => {
if (!Array.isArray(rows)) return null
const det = rows
.map(row => {
const major = toSafeInteger(row.id)
if (major == null) return null
const cost = toFiniteNumber(row.amount)
const basicFee = toFiniteNumber(row.budgetFee)
const basicFeeBasic = toFiniteNumber(row.budgetFeeBasic)
const basicFeeOptional = toFiniteNumber(row.budgetFeeOptional)
const desc = typeof row.remark === 'string' ? row.remark : ''
const hasValue =
cost != null ||
basicFee != null ||
basicFeeBasic != null ||
basicFeeOptional != null ||
isNonEmptyString(desc)
if (!hasValue) return null
return {
major,
cost: cost ?? 0,
basicFee: basicFee ?? 0,
basicFormula: typeof row.basicFormula === 'string' ? row.basicFormula : '',
basicFee_basic: basicFeeBasic ?? 0,
optionalFormula: typeof row.optionalFormula === 'string' ? row.optionalFormula : '',
basicFee_optional: basicFeeOptional ?? 0,
serviceCoe: toFiniteNumberOrZero(row.consultCategoryFactor),
majorCoe: toFiniteNumberOrZero(row.majorFactor),
fee: basicFee ?? 0,
desc
}
})
.filter((item): item is ExportMethod1Detail => Boolean(item))
if (det.length === 0) return null
return {
cost: sumNumbers(det.map(item => item.cost)),
basicFee: sumNumbers(det.map(item => item.basicFee)),
basicFee_basic: sumNumbers(det.map(item => item.basicFee_basic)),
basicFee_optional: sumNumbers(det.map(item => item.basicFee_optional)),
fee: sumNumbers(det.map(item => item.fee)),
det
}
}
const buildMethod2 = (rows: ScaleMethodRowLike[] | undefined): ExportMethod2 | null => {
if (!Array.isArray(rows)) return null
const det = rows
.map(row => {
const major = toSafeInteger(row.id)
if (major == null) return null
const area = toFiniteNumber(row.landArea)
const basicFee = toFiniteNumber(row.budgetFee)
const basicFeeBasic = toFiniteNumber(row.budgetFeeBasic)
const basicFeeOptional = toFiniteNumber(row.budgetFeeOptional)
const desc = typeof row.remark === 'string' ? row.remark : ''
const hasValue =
area != null ||
basicFee != null ||
basicFeeBasic != null ||
basicFeeOptional != null ||
isNonEmptyString(desc)
if (!hasValue) return null
return {
major,
area: area ?? 0,
basicFee: basicFee ?? 0,
basicFormula: typeof row.basicFormula === 'string' ? row.basicFormula : '',
basicFee_basic: basicFeeBasic ?? 0,
optionalFormula: typeof row.optionalFormula === 'string' ? row.optionalFormula : '',
basicFee_optional: basicFeeOptional ?? 0,
serviceCoe: toFiniteNumberOrZero(row.consultCategoryFactor),
majorCoe: toFiniteNumberOrZero(row.majorFactor),
fee: basicFee ?? 0,
desc
}
})
.filter((item): item is ExportMethod2Detail => Boolean(item))
if (det.length === 0) return null
return {
area: sumNumbers(det.map(item => item.area)),
basicFee: sumNumbers(det.map(item => item.basicFee)),
basicFee_basic: sumNumbers(det.map(item => item.basicFee_basic)),
basicFee_optional: sumNumbers(det.map(item => item.basicFee_optional)),
fee: sumNumbers(det.map(item => item.fee)),
det
}
}
const buildMethod3 = (rows: WorkloadMethodRowLike[] | undefined): ExportMethod3 | null => {
if (!Array.isArray(rows)) return null
const det = rows
.map(row => {
const task = getTaskIdFromRowId(row.id)
if (task == null) return null
const amount = toFiniteNumber(row.workload)
const basicFee = toFiniteNumber(row.basicFee)
const fee = toFiniteNumber(row.serviceFee)
const desc = typeof row.remark === 'string' ? row.remark : ''
const hasValue = amount != null || basicFee != null || fee != null || isNonEmptyString(desc)
if (!hasValue) return null
return {
task,
price: toFiniteNumberOrZero(row.budgetAdoptedUnitPrice),
amount: amount ?? 0,
basicFee: basicFee ?? 0,
serviceCoe: toFiniteNumberOrZero(row.consultCategoryFactor),
fee: fee ?? 0,
desc
}
})
.filter((item): item is ExportMethod3Detail => Boolean(item))
if (det.length === 0) return null
return {
basicFee: sumNumbers(det.map(item => item.basicFee)),
fee: sumNumbers(det.map(item => item.fee)),
det
}
}
const buildMethod4 = (rows: HourlyMethodRowLike[] | undefined): ExportMethod4 | null => {
if (!Array.isArray(rows)) return null
const det = rows
.map(row => {
const expert = getExpertIdFromRowId(row.id)
if (expert == null) return null
const personNum = toFiniteNumber(row.personnelCount)
const workDay = toFiniteNumber(row.workdayCount)
const fee = toFiniteNumber(row.serviceBudget)
const desc = typeof row.remark === 'string' ? row.remark : ''
const hasValue = personNum != null || workDay != null || fee != null || isNonEmptyString(desc)
if (!hasValue) return null
return {
expert,
price: toFiniteNumberOrZero(row.adoptedBudgetUnitPrice),
person_num: personNum ?? 0,
work_day: workDay ?? 0,
fee: fee ?? 0,
desc
}
})
.filter((item): item is ExportMethod4Detail => Boolean(item))
if (det.length === 0) return null
return {
person_num: sumNumbers(det.map(item => item.person_num)),
work_day: sumNumbers(det.map(item => item.work_day)),
fee: sumNumbers(det.map(item => item.fee)),
det
}
}
const buildServiceFee = (
row: ZxFwRowLike,
method1: ExportMethod1 | null,
method2: ExportMethod2 | null,
method3: ExportMethod3 | null,
method4: ExportMethod4 | null
) => {
const subtotal = toFiniteNumber(row.subtotal)
if (subtotal != null) return subtotal
const methodSum = sumNumbers([method1?.fee, method2?.fee, method3?.fee, method4?.fee])
if (methodSum !== 0) return methodSum
return sumNumbers([
toFiniteNumber(row.investScale),
toFiniteNumber(row.landScale),
toFiniteNumber(row.workload),
toFiniteNumber(row.hourly)
])
}
const buildExportReportPayload = async (): Promise<ExportReportPayload> => {
const [xmInfoRaw, contractCardsRaw] = await Promise.all([
localforage.getItem<XmInfoStorageLike>('xm-info-v3'),
localforage.getItem<ContractCardItem[]>('ht-card-v1')
])
const xmInfo = xmInfoRaw || {}
const projectScale = buildScaleRows(xmInfo.detailRows)
const projectName = isNonEmptyString(xmInfo.projectName) ? xmInfo.projectName.trim() : '造价项目'
const contractCards = (Array.isArray(contractCardsRaw) ? contractCardsRaw : [])
.filter(item => item && typeof item.id === 'string')
.sort((a, b) => (typeof a.order === 'number' ? a.order : Number.MAX_SAFE_INTEGER) - (typeof b.order === 'number' ? b.order : Number.MAX_SAFE_INTEGER))
const contracts: ExportContract[] = []
for (let index = 0; index < contractCards.length; index++) {
const contract = contractCards[index]
const contractId = contract.id
const [htInfoRaw, zxFwRaw] = await Promise.all([
localforage.getItem<DetailRowsStorageLike<ScaleRowLike>>(`ht-info-v3-${contractId}`),
localforage.getItem<ZxFwStorageLike>(`zxFW-${contractId}`)
])
const zxRows = Array.isArray(zxFwRaw?.detailRows) ? zxFwRaw.detailRows : []
const fixedRow = zxRows.find(row => row.id === 'fixed-budget-c')
const serviceRows = zxRows.filter(row => row.id !== 'fixed-budget-c' && hasServiceId(String(row.id)))
const services = (
await Promise.all(
serviceRows.map(async row => {
const serviceIdText = String(row.id)
const serviceId = toSafeInteger(serviceIdText)
if (serviceId == null) return null
const [method1Raw, method2Raw, method3Raw, method4Raw] = await Promise.all([
localforage.getItem<DetailRowsStorageLike<ScaleMethodRowLike>>(`tzGMF-${contractId}-${serviceIdText}`),
localforage.getItem<DetailRowsStorageLike<ScaleMethodRowLike>>(`ydGMF-${contractId}-${serviceIdText}`),
localforage.getItem<DetailRowsStorageLike<WorkloadMethodRowLike>>(`gzlF-${contractId}-${serviceIdText}`),
localforage.getItem<DetailRowsStorageLike<HourlyMethodRowLike>>(`hourlyPricing-${contractId}-${serviceIdText}`)
])
const method1 = buildMethod1(method1Raw?.detailRows)
const method2 = buildMethod2(method2Raw?.detailRows)
const method3 = buildMethod3(method3Raw?.detailRows)
const method4 = buildMethod4(method4Raw?.detailRows)
const fee = buildServiceFee(row, method1, method2, method3, method4)
const service: ExportService = {
id: serviceId,
fee
}
if (method1) service.method1 = method1
if (method2) service.method2 = method2
if (method3) service.method3 = method3
if (method4) service.method4 = method4
return service
})
)
).filter((item): item is ExportService => Boolean(item))
const fixedSubtotal = toFiniteNumber(fixedRow?.subtotal)
const serviceFeeSum = sumNumbers(services.map(item => item.fee))
const fixedMethodSum = sumNumbers([
toFiniteNumber(fixedRow?.investScale),
toFiniteNumber(fixedRow?.landScale),
toFiniteNumber(fixedRow?.workload),
toFiniteNumber(fixedRow?.hourly)
])
const contractFee = fixedSubtotal ?? (serviceFeeSum !== 0 ? serviceFeeSum : fixedMethodSum)
contracts.push({
name: isNonEmptyString(contract.name) ? contract.name : `合同段-${index + 1}`,
fee: contractFee,
scale: buildScaleRows(htInfoRaw?.detailRows),
services
})
}
return {
name: projectName,
fee: sumNumbers(contracts.map(item => item.fee)),
scale: projectScale,
contracts
}
}
const exportData = async () => {
try {
const now = new Date()
@ -485,8 +971,19 @@ const exportData = async () => {
dataMenuOpen.value = false
}
}
const exportReport = async ()=>{
const exportReport = async () => {
try {
const now = new Date()
const payload = await buildExportReportPayload()
const fileName = `${sanitizeFileNamePart(payload.name)}-报表-${formatExportTimestamp(now)}`
await exportFile(fileName, payload)
} catch (error) {
console.error('export report failed:', error)
window.alert('导出报表失败,请重试。')
} finally {
dataMenuOpen.value = false
}
}
const triggerImport = () => {
importFileRef.value?.click()

View File

@ -11,7 +11,6 @@ import {
DialogTitle,
DialogTrigger,DialogDescription
} from 'reka-ui'
import { Icon } from '@iconify/vue'
import { useWindowSize } from '@vueuse/core'
import { animate, AnimatePresence, Motion, useMotionValue, useMotionValueEvent, useTransform } from 'motion-v'
interface TypeLineCategory {
@ -285,7 +284,16 @@ useMotionValueEvent(
<div class="mb-3">
<div class="flex justify-end">
<DialogClose class="inline-flex h-8 w-8 cursor-pointer items-center justify-center rounded-md border border-muted-foreground/30 text-muted-foreground transition-colors hover:border-foreground/40 hover:text-foreground">
<Icon icon="lucide:x" class="h-4 w-4" />
<svg viewBox="0 0 24 24" class="h-4 w-4" aria-hidden="true">
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M18 6L6 18M6 6l12 12"
/>
</svg>
</DialogClose>
</div>
<DialogTitle class="mt-2">
@ -314,7 +322,16 @@ useMotionValueEvent(
aria-label="跳转到官网首页"
title="官网首页"
>
<Icon icon="lucide:arrow-up-right" class="h-4 w-4" />
<svg viewBox="0 0 24 24" class="h-4 w-4" aria-hidden="true">
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M7 7h10v10M7 17L17 7"
/>
</svg>
</a>
</div>
</DialogDescription>

View File

@ -22,6 +22,7 @@ interface WorkloadRow {
id: string
conversion: number | null
workload: number | null
basicFee: number | null
budgetAdoptedUnitPrice: number | null
consultCategoryFactor: number | null
}
@ -159,6 +160,7 @@ const buildDefaultWorkloadRows = (serviceId: string | number): WorkloadRow[] =>
id: `task-${taskId}-${order}`,
conversion: toFiniteNumberOrNull(task.conversion),
workload: null,
basicFee: null,
budgetAdoptedUnitPrice: toFiniteNumberOrNull(task.defPrice),
consultCategoryFactor: defaultConsultCategoryFactor
}))
@ -177,23 +179,35 @@ const mergeWorkloadRows = (
return {
...row,
workload: toFiniteNumberOrNull(fromDb.workload),
basicFee: toFiniteNumberOrNull(fromDb.basicFee),
budgetAdoptedUnitPrice: toFiniteNumberOrNull(fromDb.budgetAdoptedUnitPrice),
consultCategoryFactor: toFiniteNumberOrNull(fromDb.consultCategoryFactor)
}
})
}
const calcWorkloadServiceFee = (row: WorkloadRow) => {
const calcWorkloadBasicFee = (row: WorkloadRow) => {
if (
row.budgetAdoptedUnitPrice == null ||
row.conversion == null ||
row.workload == null ||
row.consultCategoryFactor == null
row.workload == null
) {
return null
}
return roundTo(
toDecimal(row.budgetAdoptedUnitPrice).mul(row.conversion).mul(row.workload).mul(row.consultCategoryFactor),
toDecimal(row.budgetAdoptedUnitPrice).mul(row.conversion).mul(row.workload),
2
)
}
const calcWorkloadServiceFee = (row: WorkloadRow) => {
if (row.consultCategoryFactor == null) {
return null
}
const basicFee = row.basicFee ?? calcWorkloadBasicFee(row)
if (basicFee == null) return null
return roundTo(
toDecimal(basicFee).mul(row.consultCategoryFactor),
2
)
}

View File

@ -4,10 +4,70 @@ import { toFiniteNumberOrNull } from '@/lib/number'
type ScaleMode = 'cost' | 'area'
export const getBenchmarkBudgetByScale = (value: unknown, mode: ScaleMode) => {
export interface ScaleFeeSplitResult {
basic: number
optional: number
total: number
basicFormula: string
optionalFormula: string
}
export const getBenchmarkBudgetSplitByScale = (
value: unknown,
mode: ScaleMode
): ScaleFeeSplitResult | null => {
const scaleValue = toFiniteNumberOrNull(value)
const result = getBasicFeeFromScale(scaleValue, mode)
return result ? roundTo(addNumbers(result.basic, result.optional), 2) : null
if (!result) return null
const basic = roundTo(result.basic, 2)
const optional = roundTo(result.optional, 2)
const basicFormula = typeof result.basicFormula === 'string' ? result.basicFormula : ''
const optionalFormula = typeof result.optionalFormula === 'string' ? result.optionalFormula : ''
return {
basic,
optional,
total: roundTo(addNumbers(basic, optional), 2),
basicFormula,
optionalFormula
}
}
export const getBenchmarkBudgetByScale = (value: unknown, mode: ScaleMode) => {
const splitResult = getBenchmarkBudgetSplitByScale(value, mode)
return splitResult ? splitResult.total : null
}
export const getScaleBudgetFeeSplit = (params: {
benchmarkBudgetBasic: unknown
benchmarkBudgetOptional: unknown
majorFactor: unknown
consultCategoryFactor: unknown
}): ScaleFeeSplitResult | null => {
const benchmarkBudgetBasic = toFiniteNumberOrNull(params.benchmarkBudgetBasic)
const benchmarkBudgetOptional = toFiniteNumberOrNull(params.benchmarkBudgetOptional)
const majorFactor = toFiniteNumberOrNull(params.majorFactor)
const consultCategoryFactor = toFiniteNumberOrNull(params.consultCategoryFactor)
if (
benchmarkBudgetBasic == null ||
benchmarkBudgetOptional == null ||
majorFactor == null ||
consultCategoryFactor == null
) {
return null
}
const basic = roundTo(toDecimal(benchmarkBudgetBasic).mul(majorFactor).mul(consultCategoryFactor), 2)
const optional = roundTo(toDecimal(benchmarkBudgetOptional).mul(majorFactor).mul(consultCategoryFactor), 2)
return {
basic,
optional,
total: roundTo(addNumbers(basic, optional), 2),
basicFormula: '',
optionalFormula: ''
}
}
export const getScaleBudgetFee = (params: {

View File

@ -1,5 +1,14 @@
// @ts-nocheck
import { roundTo, toDecimal } from '@/lib/decimal'
import { addNumbers, roundTo, toDecimal } from '@/lib/decimal'
import { formatThousands } from '@/lib/numberFormat'
const numberFormatter = (value: unknown, fractionDigits = 2) =>
formatThousands(value, fractionDigits)
const toFiniteNumber = (value: unknown) => {
const num = Number(value)
return Number.isFinite(num) ? num : 0
}
export const majorList = {
0: { code: 'E1', name: '交通运输工程通用专业', maxCoe: null, minCoe: null, defCoe: 1, desc: '' },
@ -176,7 +185,29 @@ const calcScaleFee = (params: {
)
}
export function getBasicFeeFromScale1(scaleValue: unknown, scaleType: 'cost' | 'area' | 'amount') {
const scaleRatePermillage = (rate: number) => roundTo(toDecimal(rate).mul(1000), 2)
const buildScaleFormula = (params: {
staPrice: number
sv: number
staLine: number
rate: number
multiplier?: number
showPermillage?: boolean
}) => {
const multiplier = params.multiplier ?? 1
const currentValue = toDecimal(params.sv).mul(multiplier).toNumber()
const staLineValue = toDecimal(params.staLine).mul(multiplier).toNumber()
const rateText = params.showPermillage ? `${scaleRatePermillage(params.rate)}` : params.rate
if (params.staPrice) {
return `${numberFormatter(params.staPrice, 0)}+(${numberFormatter(currentValue, 0)}-${numberFormatter(staLineValue, 0)})×${rateText}`
}
return `${numberFormatter(currentValue, 0)}×${rateText}`
}
export function getBasicFeeFromScale(scaleValue: unknown, scaleType: 'cost' | 'area' | 'amount') {
const sv = Number(scaleValue)
if (!Number.isFinite(sv) || sv <= 0) return null
@ -197,6 +228,22 @@ export function getBasicFeeFromScale1(scaleValue: unknown, scaleType: 'cost' | '
staLine: targetRange.staLine,
rate: targetRange.optional.rate,
multiplier: 10000
}),
basicFormula: buildScaleFormula({
staPrice: targetRange.basic.staPrice,
sv,
staLine: targetRange.staLine,
rate: targetRange.basic.rate,
multiplier: 10000,
showPermillage: true
}),
optionalFormula: buildScaleFormula({
staPrice: targetRange.optional.staPrice,
sv,
staLine: targetRange.staLine,
rate: targetRange.optional.rate,
multiplier: 10000,
showPermillage: true
})
}
}
@ -216,6 +263,18 @@ export function getBasicFeeFromScale1(scaleValue: unknown, scaleType: 'cost' | '
sv,
staLine: targetRange.staLine,
rate: targetRange.optional.rate
}),
basicFormula: buildScaleFormula({
staPrice: targetRange.basic.staPrice,
sv,
staLine: targetRange.staLine,
rate: targetRange.basic.rate
}),
optionalFormula: buildScaleFormula({
staPrice: targetRange.optional.staPrice,
sv,
staLine: targetRange.staLine,
rate: targetRange.optional.rate
})
}
}
@ -229,13 +288,17 @@ export function getBasicFeeFromScale1(scaleValue: unknown, scaleType: 'cost' | '
staLine: targetRange.staLine,
rate: targetRange.rate
}),
optional: 0
optional: 0,
basicFormula: buildScaleFormula({
staPrice: targetRange.staPrice,
sv,
staLine: targetRange.staLine,
rate: targetRange.rate
}),
optionalFormula: ''
}
}
export function getBasicFeeFromScale(scaleValue: unknown, scaleType: 'cost' | 'area' | 'amount') {
return getBasicFeeFromScale1(scaleValue, scaleType)
}
export async function exportFile(fileName, data) {
@ -281,8 +344,9 @@ export async function exportFile(fileName, data) {
}
async function generateTemplate(data) {
console.log(data)
// 获取模板
let templateExcel = 'template20260226001test009';
let templateExcel = 'template20260226001test010';
let templateUrl = `https://oa.zwgczx.com/myExcelTemplate/${templateExcel}.xlsx`;
let buf = await (await fetch(templateUrl)).arrayBuffer();
let workbook = new ExcelJS.Workbook();
@ -327,12 +391,6 @@ async function generateTemplate(data) {
insertAndCopyColumn(7 * (i + 1) + 1, [1, 2, 3, 4, 5, 6, 7], yz01_sheet);
}
}
for (let i = 0; i < yz01Num; i++) {
yz01_sheet.mergeCells(6, i * 7 + 2, 6, i * 7 + 7);
}
}
if (yz01Mod > 0) {
yz01_sheet.mergeCells(6, yz01Num * 7 + 2, 6, yz01Num * 7 + 3 + yz01Mod);
}
let f01Mod = (data.contracts.length) % 3;
@ -362,8 +420,11 @@ async function generateTemplate(data) {
f01_sheet.mergeCells(1, i * 10 + 3, 2, i * 10 + 3);
f01_sheet.mergeCells(1, i * 10 + 10, 2, i * 10 + 10);
f01_sheet.mergeCells(1, i * 10 + 4, 1, i * 10 + 5);
f01_sheet.getRow(1).getCell(i * 10 + 4).value = data.contracts[i * 3].name;
f01_sheet.mergeCells(1, i * 10 + 6, 1, i * 10 + 7);
f01_sheet.getRow(1).getCell(i * 10 + 6).value = data.contracts[i * 3 + 1].name;
f01_sheet.mergeCells(1, i * 10 + 8, 1, i * 10 + 9);
f01_sheet.getRow(1).getCell(i * 10 + 8).value = data.contracts[i * 3 + 2].name;
}
}
if (f01Mod > 0) {
@ -372,7 +433,11 @@ async function generateTemplate(data) {
f01_sheet.mergeCells(1, f01Num * 10 + 3, 2, f01Num * 10 + 3);
f01_sheet.mergeCells(1, f01Num * 10 + 2 * f01Mod + 4, 2, f01Num * 10 + 2 * f01Mod + 4);
f01_sheet.mergeCells(1, f01Num * 10 + 4, 1, f01Num * 10 + 5);
if (f01Mod == 2) f01_sheet.mergeCells(1, f01Num * 10 + 6, 1, f01Num * 10 + 7);
f01_sheet.getRow(1).getCell(f01Num * 10 + 4).value = data.contracts[f01Num * 3].name;
if (f01Mod == 2) {
f01_sheet.mergeCells(1, f01Num * 10 + 6, 1, f01Num * 10 + 7);
f01_sheet.getRow(1).getCell(f01Num * 10 + 6).value = data.contracts[f01Num * 3 + 1].name;
}
}
let ml_slotRow = 13;
@ -396,6 +461,8 @@ async function generateTemplate(data) {
let ml_sourceRows = [ml_sheet.getRow(6)];
let sheet_1 = copyWorksheet(workbook, '预i-1表', `${index + 1}-1表`);
sheet_1.headerFooter.oddHeader = sheet_1.headerFooter.oddHeader.replace(/×××/g, ci.name).replace(/预 i-1 表/g, `${index + 1}-1 表`).replace(/第i合同/g, ci.name);
sheet_1.getRow(1).getCell(4).value = sheet_1.getRow(1).getCell(4).value.replace(/第i合同/g, ci.name);
let sheet_2;
let sheet_2_1;
let sheet_2_2;
@ -405,24 +472,47 @@ async function generateTemplate(data) {
if (ci.method1.length || ci.method2.length) {
ml_sourceRows.push(ml_sheet.getRow(7));
sheet_2 = copyWorksheet(workbook, '预i-2表', `${index + 1}-2表`);
sheet_2.headerFooter.oddHeader = sheet_2.headerFooter.oddHeader.replace(/×××/g, ci.name).replace(/预 i-2 表/g, `${index + 1}-2 表`).replace(/第i合同/g, ci.name);
if (ci.method1.length) {
ml_sourceRows.push(ml_sheet.getRow(8));
sheet_2_1 = copyWorksheet(workbook, '预i-2-1表', `${index + 1}-2-1表`);
sheet_2_1.headerFooter.oddHeader = sheet_2_1.headerFooter.oddHeader.replace(/×××/g, ci.name).replace(/预 i-2-1 表/g, `${index + 1}-2-1 表`).replace(/第i合同/g, ci.name);
}
if (ci.method2.length) {
ml_sourceRows.push(ml_sheet.getRow(9));
sheet_2_2 = copyWorksheet(workbook, '预i-2-2表', `${index + 1}-2-2表`);
sheet_2_2.headerFooter.oddHeader = sheet_2_2.headerFooter.oddHeader.replace(/×××/g, ci.name).replace(/预 i-2-2 表/g, `${index + 1}-2-2 表`).replace(/第i合同/g, ci.name);
}
}
if (ci.method3.length) {
ml_sourceRows.push(ml_sheet.getRow(10));
sheet_3 = copyWorksheet(workbook, '预i-3表', `${index + 1}-3表`);
sheet_3.headerFooter.oddHeader = sheet_3.headerFooter.oddHeader.replace(/×××/g, ci.name).replace(/预 i-3 表/g, `${index + 1}-3 表`).replace(/第i合同/g, ci.name);
}
if (ci.method4.length) {
ml_sourceRows.push(ml_sheet.getRow(11));
ml_sourceRows.push(ml_sheet.getRow(12));
sheet_4 = copyWorksheet(workbook, '预i-4表', `${index + 1}-4表`);
sheet_4.headerFooter.oddHeader = sheet_4.headerFooter.oddHeader.replace(/×××/g, ci.name).replace(/预 i-4 表/g, `${index + 1}-4 表`).replace(/第i合同/g, ci.name);
sheet_4_1 = copyWorksheet(workbook, '预i-4-1表', `${index + 1}-4-1表`);
sheet_4_1.headerFooter.oddHeader = sheet_4_1.headerFooter.oddHeader.replace(/×××/g, ci.name).replace(/预 i-4-1 表/g, `${index + 1}-4-1 表`).replace(/第i合同/g, ci.name);
let sumObj = ci.method4.reduce((a, b) => {
const m4 = ci.services.find(f => f.id == b)?.method4;
return {
person_num: addNumbers(a.person_num, toFiniteNumber(m4?.person_num)),
work_day: addNumbers(a.work_day, toFiniteNumber(m4?.work_day)),
fee: addNumbers(a.fee, toFiniteNumber(m4?.fee))
};
}, { person_num: 0, work_day: 0, fee: 0 });
sheet_4.getRow(3).getCell(4).value = sumObj.person_num;
sheet_4.getRow(3).getCell(5).value = sumObj.work_day;
sheet_4.getRow(3).getCell(6).value = sumObj.fee;
sheet_4_1.getRow(4).getCell(4).value = '/';
sheet_4_1.getRow(4).getCell(5).value = '/';
sheet_4_1.getRow(4).getCell(6).value = '/';
sheet_4_1.getRow(4).getCell(7).value = sumObj.person_num;
sheet_4_1.getRow(4).getCell(8).value = sumObj.work_day;
sheet_4_1.getRow(4).getCell(9).value = sumObj.fee
}
cusInsertRowFunc(ml_slotRow, ml_sourceRows, ml_sheet, () => ml_slotRow++, (targetCell, sourceCell, colNumber) => {
@ -614,7 +704,7 @@ async function generateTemplate(data) {
targetRow.getCell(2).value = expertX.ref;
targetRow.getCell(3).value = expertX.name;
targetRow.getCell(4).value = `${expertX.minPrice}${expertX.maxPrice}`;
targetRow.getCell(5).value = `${Math.round(expertX.minPrice * expertX.manageCoe)}${Math.round(expertX.maxPrice * expertX.manageCoe)}`;
targetRow.getCell(5).value = `${roundTo(toDecimal(toFiniteNumber(expertX.minPrice)).mul(toFiniteNumber(expertX.manageCoe)), 0)}${roundTo(toDecimal(toFiniteNumber(expertX.maxPrice)).mul(toFiniteNumber(expertX.manageCoe)), 0)}`;
targetRow.getCell(6).value = eobj.price;
targetRow.getCell(7).value = eobj.person_num;
targetRow.getCell(8).value = eobj.work_day;
@ -630,7 +720,6 @@ async function generateTemplate(data) {
allServices.forEach((s, sindex) => {
const serviceX = serviceList[s.id];
cusInsertRowFunc(3 + sindex, [yz01_sheet.getRow(2)], yz01_sheet, (targetRow) => {
const sumCol = data.contracts.length;
let siSum = 0;
for (let i = 0; i < yz01Num; i++) {
targetRow.getCell(i * 7 + 1).value = sindex + 1;
@ -639,12 +728,17 @@ async function generateTemplate(data) {
targetRow.getCell(i * 7 + 4).value = s.contracts[i * 4];
targetRow.getCell(i * 7 + 5).value = s.contracts[i * 4 + 1];
targetRow.getCell(i * 7 + 6).value = s.contracts[i * 4 + 2];
siSum = siSum + (Number(s.contracts[i * 4]) || 0 + Number(s.contracts[i * 4 + 1]) || 0 + Number(s.contracts[i * 4 + 2]) || 0);
if (i * 4 + 3 == sumCol) {
siSum = addNumbers(
siSum,
toFiniteNumber(s.contracts[i * 4]),
toFiniteNumber(s.contracts[i * 4 + 1]),
toFiniteNumber(s.contracts[i * 4 + 2])
);
if (i == yz01Num - 1 && yz01Mod == 0) {
targetRow.getCell(i * 7 + 7).value = numberFormatter(siSum, 2);
} else {
targetRow.getCell(i * 7 + 7).value = s.contracts[i * 4 + 3];
siSum = siSum + (Number(s.contracts[i * 4 + 3]) || 0);
siSum = addNumbers(siSum, toFiniteNumber(s.contracts[i * 4 + 3]));
}
}
if (yz01Mod) {
@ -655,12 +749,16 @@ async function generateTemplate(data) {
targetRow.getCell(yz01Num * 7 + 4).value = numberFormatter(siSum, 2);
} else if (yz01Mod == 2) {
targetRow.getCell(yz01Num * 7 + 4).value = s.contracts[yz01Num * 4];
siSum = siSum + (Number(s.contracts[yz01Num * 4]) || 0);
siSum = addNumbers(siSum, toFiniteNumber(s.contracts[yz01Num * 4]));
targetRow.getCell(yz01Num * 7 + 5).value = numberFormatter(siSum, 2);
} else {
targetRow.getCell(yz01Num * 7 + 4).value = s.contracts[yz01Num * 4];
targetRow.getCell(yz01Num * 7 + 5).value = s.contracts[yz01Num * 4 + 1];
siSum = siSum + (Number(s.contracts[yz01Num * 4]) || 0 + Number(s.contracts[yz01Num * 4 + 1]) || 0);
siSum = addNumbers(
siSum,
toFiniteNumber(s.contracts[yz01Num * 4]),
toFiniteNumber(s.contracts[yz01Num * 4 + 1])
);
targetRow.getCell(yz01Num * 7 + 6).value = numberFormatter(siSum, 2);
}
}
@ -669,6 +767,31 @@ async function generateTemplate(data) {
ml_sheet.spliceRows(6, 6);
ml_sheet.spliceRows(6, 1);
// 合并说明
if (yz01Num) {
for (let i = 0; i < yz01Num; i++) {
yz01_sheet.getRow(1).getCell(i * 7 + 4).value = `${data.contracts[i * 4].name}预算\n(元)`;
yz01_sheet.getRow(1).getCell(i * 7 + 4).value = `${data.contracts[i * 4 + 1].name}预算\n(元)`;
yz01_sheet.getRow(1).getCell(i * 7 + 4).value = `${data.contracts[i * 4 + 2].name}预算\n(元)`;
if (i == yz01Num - 1 && yz01Mod == 0) {
yz01_sheet.getRow(1).getCell(i * 7 + 4).value = `预算小计\n(元)`;
} else {
yz01_sheet.getRow(1).getCell(i * 7 + 4).value = `${data.contracts[i * 4 + 3].name}预算\n(元)`;
}
yz01_sheet.mergeCells(6, i * 7 + 2, 6, i * 7 + 7);
}
}
if (yz01Mod) {
for (let i = 0; i < yz01Mod; i++) {
if (i == yz01Mod - 1) {
yz01_sheet.getRow(1).getCell(yz01Num * 7 + 4 + i).value = `预算小计\n(元)`;
} else {
yz01_sheet.getRow(1).getCell(yz01Num * 7 + 4 + i).value = `${data.contracts[yz01Num * 4 + i].name}预算\n(元)`;
}
}
yz01_sheet.mergeCells(6, yz01Num * 7 + 2, 6, yz01Num * 7 + 3 + yz01Mod);
}
ml_sheet.mergeCells(ml_slotRow - 7, 1, ml_slotRow - 7, 4);
workbook.removeWorksheet('预i-1表');
@ -682,18 +805,19 @@ async function generateTemplate(data) {
workbook.getWorksheet('辅02表').orderNo = ml_number + 3 + 10;
workbook.getWorksheet('辅03表').orderNo = ml_number + 4 + 10;
// workbook._worksheets.forEach(sheet => {
// if (sheet) {
// if (sheet.headerFooter.oddHeader) sheet.headerFooter.oddHeader = sheet.headerFooter.oddHeader.replace(/&([CLR])&/g, '&$1&"宋体"&');
// if (sheet.headerFooter.oddFooter) sheet.headerFooter.oddFooter = sheet.headerFooter.oddFooter.replace(/&([CLR])&/g, '&$1&"宋体"&');
// }
// });
workbook._worksheets.forEach(sheet => {
if (sheet) {
if (sheet.headerFooter.oddHeader) sheet.headerFooter.oddHeader = sheet.headerFooter.oddHeader.replace(/&([CLR])&/g, '&$1&"宋体"&');
if (sheet.headerFooter.oddFooter) sheet.headerFooter.oddFooter = sheet.headerFooter.oddFooter.replace(/&([CLR])&/g, '&$1&"宋体"&');
}
});
window.workbook = workbook;
return workbook;
}
function cusInsertRowFunc(insertRowNum, sourceRows, worksheet, RowFun, cellFun) {
// 插入行
let newRows = [];
@ -817,135 +941,3 @@ function cloneCellValue(value) {
}
//demo
let data1 = {
name: 'test001',//项目名称
fee: 10000, //所有合同段总费用
scale: [//项目明细aggrid数据
{
major: 0, //专业id对应专业majorList中的key
cost: 100000,//造价金额
area: 200,//用地面积
},
{
major: 1,
cost: 100000,
area: 200,
},
],
contracts: [//合同段数据
{
name: 'A合同段',//合同段名称
fee: 10000,//合同段费用该合同段咨询服务zxfw.vue里面aggrid的合同预算行的小计
scale: [//合同段明细aggrid数据htinfo.vue
{
major: 0, //专业id对应专业majorList中的key
cost: 100000,//造价金额
area: 200,//用地面积
},
{
major: 1,
cost: 100000,
area: 200,
},
],
services: [//咨询服务数据zxfw.vue里面aggrid的数据不用输出合同预算
{
id: 0, //服务id对应serviceList中的key
fee: 100000 ,//服务费用(该服务咨询服务小计)
method1: { // 投资规模法InvestmentScalePricingPane.vue的数据
cost: 100000, //zxfw.vue里面aggrid该数据的投资规模法金额
basicFee: 200,
basicFee_basic: 200,
basicFee_optional: 0,
fee: 250000,
det: [
{
major: 0,
cost: 100000,
basicFee: 200,
basicFormula: '856,000+(1,000,000,000-500,000,000)×1‰',
basicFee_basic: 200,
optionalFormula: '171,200+(1,000,000,000-500,000,000)×0.2‰',
basicFee_optional: 0,
serviceCoe: 1.1,
majorCoe: 1.2,
fee: 100000,
desc: '',
},
],
},
method2: { // 用地规模法
area: 1200,
basicFee: 200,
basicFee_basic: 200,
basicFee_optional: 0,
fee: 250000,
det: [
{
major: 0,
area: 1200,
basicFee: 200,
basicFormula: '106,000+(1,200-1,000)×60',
basicFee_basic: 200,
optionalFormula: '21,200+(1,200-1,000)×12',
basicFee_optional: 0,
serviceCoe: 1.1,
majorCoe: 1.2,
fee: 100000,
desc: '',
},
],
},
method3: { // 工作量法
basicFee: 200,
fee: 250000,
det: [
{
task: 0,
price: 100000,
amount: 10,
basicFee: 200,
serviceCoe: 1.1,
fee: 100000,
desc: '',
},
{
task: 1,
price: 100000,
amount: 10,
basicFee: 200,
serviceCoe: 1.1,
fee: 100000,
desc: '',
},
],
},
method4: { // 工时法
person_num: 10,
work_day: 10,
fee: 250000,
det: [
{
expert: 0,
price: 100000,
person_num: 10,
work_day: 3,
fee: 100000,
desc: '',
},
{
expert: 1,
price: 100000,
person_num: 10,
work_day: 3,
fee: 100000,
desc: '',
},
],
},
},
],
},
],
};