1
This commit is contained in:
parent
62546bc937
commit
33913c29d2
20
index.html
20
index.html
@ -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>
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
@ -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: {
|
||||
|
||||
310
src/sql.ts
310
src/sql.ts
@ -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: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user