1
This commit is contained in:
parent
c4d04cbee3
commit
a280dfb975
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
@ -322,7 +322,9 @@ const columnDefs: ColDef<SummaryRow>[] = [
|
|||||||
minWidth: 120,
|
minWidth: 120,
|
||||||
flex: 1.2,
|
flex: 1.2,
|
||||||
headerClass: 'ag-right-aligned-header',
|
headerClass: 'ag-right-aligned-header',
|
||||||
cellClass: 'ag-right-aligned-cell',
|
cellClassRules: {
|
||||||
|
'ag-right-aligned-cell': () => true
|
||||||
|
},
|
||||||
colSpan: params => {
|
colSpan: params => {
|
||||||
if (!params.data) return 1
|
if (!params.data) return 1
|
||||||
if (params.data.rowType === 'total') return 4
|
if (params.data.rowType === 'total') return 4
|
||||||
@ -340,7 +342,9 @@ const columnDefs: ColDef<SummaryRow>[] = [
|
|||||||
minWidth: 120,
|
minWidth: 120,
|
||||||
flex: 1.2,
|
flex: 1.2,
|
||||||
headerClass: 'ag-right-aligned-header',
|
headerClass: 'ag-right-aligned-header',
|
||||||
cellClass: 'ag-right-aligned-cell',
|
cellClassRules: {
|
||||||
|
'ag-right-aligned-cell': () => true
|
||||||
|
},
|
||||||
valueFormatter: params => {
|
valueFormatter: params => {
|
||||||
if (params.data?.rowType === 'total') return ''
|
if (params.data?.rowType === 'total') return ''
|
||||||
return params.value == null ? '' : formatThousandsFlexible(params.value, 3)
|
return params.value == null ? '' : formatThousandsFlexible(params.value, 3)
|
||||||
@ -352,7 +356,9 @@ const columnDefs: ColDef<SummaryRow>[] = [
|
|||||||
minWidth: 110,
|
minWidth: 110,
|
||||||
flex: 1.2,
|
flex: 1.2,
|
||||||
headerClass: 'ag-right-aligned-header',
|
headerClass: 'ag-right-aligned-header',
|
||||||
cellClass: 'ag-right-aligned-cell',
|
cellClassRules: {
|
||||||
|
'ag-right-aligned-cell': () => true
|
||||||
|
},
|
||||||
valueFormatter: params => {
|
valueFormatter: params => {
|
||||||
if (params.data?.rowType === 'total') return ''
|
if (params.data?.rowType === 'total') return ''
|
||||||
return params.value == null ? '' : formatThousandsFlexible(params.value, 3)
|
return params.value == null ? '' : formatThousandsFlexible(params.value, 3)
|
||||||
@ -364,7 +370,9 @@ const columnDefs: ColDef<SummaryRow>[] = [
|
|||||||
minWidth: 110,
|
minWidth: 110,
|
||||||
flex: 1.2,
|
flex: 1.2,
|
||||||
headerClass: 'ag-right-aligned-header',
|
headerClass: 'ag-right-aligned-header',
|
||||||
cellClass: 'ag-right-aligned-cell',
|
cellClassRules: {
|
||||||
|
'ag-right-aligned-cell': () => true
|
||||||
|
},
|
||||||
valueFormatter: params => {
|
valueFormatter: params => {
|
||||||
if (params.data?.rowType === 'total') return ''
|
if (params.data?.rowType === 'total') return ''
|
||||||
return params.value == null ? '' : formatThousandsFlexible(params.value, 3)
|
return params.value == null ? '' : formatThousandsFlexible(params.value, 3)
|
||||||
@ -376,7 +384,9 @@ const columnDefs: ColDef<SummaryRow>[] = [
|
|||||||
minWidth: 120,
|
minWidth: 120,
|
||||||
flex: 1.2,
|
flex: 1.2,
|
||||||
headerClass: 'ag-right-aligned-header',
|
headerClass: 'ag-right-aligned-header',
|
||||||
cellClass: 'ag-right-aligned-cell',
|
cellClassRules: {
|
||||||
|
'ag-right-aligned-cell': () => true
|
||||||
|
},
|
||||||
valueFormatter: params => (params.value == null ? '' : formatThousands(params.value, 2))
|
valueFormatter: params => (params.value == null ? '' : formatThousands(params.value, 2))
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -385,7 +395,9 @@ const columnDefs: ColDef<SummaryRow>[] = [
|
|||||||
minWidth: 120,
|
minWidth: 120,
|
||||||
flex: 1.2,
|
flex: 1.2,
|
||||||
headerClass: 'ag-right-aligned-header',
|
headerClass: 'ag-right-aligned-header',
|
||||||
cellClass: 'ag-right-aligned-cell',
|
cellClassRules: {
|
||||||
|
'ag-right-aligned-cell': () => true
|
||||||
|
},
|
||||||
valueFormatter: params => (params.value == null ? '' : formatThousands(params.value, 2))
|
valueFormatter: params => (params.value == null ? '' : formatThousands(params.value, 2))
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@ -337,9 +337,9 @@ const summaryView = markRaw(
|
|||||||
const xmCategories: XmCategoryItem[] = [
|
const xmCategories: XmCategoryItem[] = [
|
||||||
{ key: 'base-info', label: '基础信息', component: htBaseInfoView },
|
{ key: 'base-info', label: '基础信息', component: htBaseInfoView },
|
||||||
{ key: 'info', label: '规模信息', component: htView },
|
{ key: 'info', label: '规模信息', component: htView },
|
||||||
|
{ key: 'contract', label: '咨询服务', component: zxfwView },
|
||||||
{ key: 'consult-category-factor', label: '咨询分类系数', component: consultCategoryFactorView },
|
{ key: 'consult-category-factor', label: '咨询分类系数', component: consultCategoryFactorView },
|
||||||
{ key: 'major-factor', label: '工程专业系数', component: majorFactorView },
|
{ key: 'major-factor', label: '工程专业系数', component: majorFactorView },
|
||||||
{ key: 'contract', label: '咨询服务', component: zxfwView },
|
|
||||||
{ key: 'additional-work-fee', label: '附加工作费', component: additionalWorkFeeView },
|
{ key: 'additional-work-fee', label: '附加工作费', component: additionalWorkFeeView },
|
||||||
{ key: 'reserve-fee', label: '预备费', component: reserveFeeView },
|
{ key: 'reserve-fee', label: '预备费', component: reserveFeeView },
|
||||||
{ key: 'all', label: '汇总', component: summaryView },
|
{ key: 'all', label: '汇总', component: summaryView },
|
||||||
|
|||||||
@ -401,8 +401,10 @@ const createMethodColumn = (
|
|||||||
headerClass: 'ag-right-aligned-header',
|
headerClass: 'ag-right-aligned-header',
|
||||||
minWidth,
|
minWidth,
|
||||||
flex: 1.5,
|
flex: 1.5,
|
||||||
cellClass: 'ag-right-aligned-cell',
|
|
||||||
editable: false,
|
editable: false,
|
||||||
|
cellClassRules: {
|
||||||
|
'ag-right-aligned-cell': () => true
|
||||||
|
},
|
||||||
valueGetter: params => {
|
valueGetter: params => {
|
||||||
if (!params.data) return null
|
if (!params.data) return null
|
||||||
if (isFixedRow(params.data)) return getMethodTotal(field)
|
if (isFixedRow(params.data)) return getMethodTotal(field)
|
||||||
@ -656,8 +658,10 @@ const columnDefs: ColDef<DetailRow>[] = [
|
|||||||
headerClass: 'ag-right-aligned-header',
|
headerClass: 'ag-right-aligned-header',
|
||||||
flex: 2,
|
flex: 2,
|
||||||
minWidth: 100,
|
minWidth: 100,
|
||||||
cellClass: 'ag-right-aligned-cell',
|
|
||||||
editable: false,
|
editable: false,
|
||||||
|
cellClassRules: {
|
||||||
|
'ag-right-aligned-cell': () => true
|
||||||
|
},
|
||||||
valueGetter: params => {
|
valueGetter: params => {
|
||||||
if (!params.data) return null
|
if (!params.data) return null
|
||||||
return params.data.subtotal
|
return params.data.subtotal
|
||||||
@ -665,13 +669,16 @@ const columnDefs: ColDef<DetailRow>[] = [
|
|||||||
valueFormatter: params => (params.value == null ? '' : formatThousands(params.value, 2))
|
valueFormatter: params => (params.value == null ? '' : formatThousands(params.value, 2))
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
headerName: '确认金额',
|
headerName: '确认金额 ✎',
|
||||||
field: 'finalFee',
|
field: 'finalFee',
|
||||||
headerClass: 'ag-right-aligned-header',
|
headerClass: 'ag-right-aligned-header',
|
||||||
|
headerTooltip: '该列支持手动修改,修改后会自动汇总到固定小计行',
|
||||||
flex: 2,
|
flex: 2,
|
||||||
minWidth: 110,
|
minWidth: 110,
|
||||||
cellClass: 'ag-right-aligned-cell',
|
|
||||||
editable: params => !isFixedRow(params.data),
|
editable: params => !isFixedRow(params.data),
|
||||||
|
cellClassRules: {
|
||||||
|
'ag-right-aligned-cell': () => true
|
||||||
|
},
|
||||||
valueGetter: params => {
|
valueGetter: params => {
|
||||||
if (!params.data) return null
|
if (!params.data) return null
|
||||||
return params.data.finalFee
|
return params.data.finalFee
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onActivated, onBeforeUnmount, onMounted, ref } from 'vue'
|
import { computed, nextTick, onActivated, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||||
import { AgGridVue } from 'ag-grid-vue3'
|
import { AgGridVue } from 'ag-grid-vue3'
|
||||||
import type { ColDef, ColGroupDef, GridApi, GridReadyEvent } from 'ag-grid-community'
|
import type { ColDef, ColGroupDef, GridApi, GridReadyEvent } from 'ag-grid-community'
|
||||||
import { getMajorDictEntries, getServiceDictItemById, industryTypeList, isMajorIdInIndustryScope } from '@/sql'
|
import { getMajorDictEntries, getServiceDictItemById, industryTypeList, isMajorIdInIndustryScope } from '@/sql'
|
||||||
@ -12,6 +12,7 @@ import { useKvStore } from '@/pinia/kv'
|
|||||||
import { loadConsultCategoryFactorMap, loadMajorFactorMap } from '@/lib/xmFactorDefaults'
|
import { loadConsultCategoryFactorMap, loadMajorFactorMap } from '@/lib/xmFactorDefaults'
|
||||||
import { parseNumberOrNull } from '@/lib/number'
|
import { parseNumberOrNull } from '@/lib/number'
|
||||||
import { getBenchmarkBudgetSplitByScale, getScaleBudgetFeeSplit } from '@/lib/pricingScaleFee'
|
import { getBenchmarkBudgetSplitByScale, getScaleBudgetFeeSplit } from '@/lib/pricingScaleFee'
|
||||||
|
import { AgGridResetHeader } from '@/lib/agGridResetHeader'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
import {
|
||||||
AlertDialogAction,
|
AlertDialogAction,
|
||||||
@ -616,23 +617,22 @@ const formatReadonlyMoney = (params: any) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type BudgetCheckField = 'benchmarkBudgetBasicChecked' | 'benchmarkBudgetOptionalChecked'
|
type BudgetCheckField = 'benchmarkBudgetBasicChecked' | 'benchmarkBudgetOptionalChecked'
|
||||||
|
const isBenchmarkBudgetFullyUnchecked = (
|
||||||
|
row?: Pick<DetailRow, 'benchmarkBudgetBasicChecked' | 'benchmarkBudgetOptionalChecked'>
|
||||||
|
) => row?.benchmarkBudgetBasicChecked === false && row?.benchmarkBudgetOptionalChecked === false
|
||||||
|
|
||||||
const updateBudgetCheckState = (rowId: string, checkField: BudgetCheckField, checked: boolean) => {
|
const updateBudgetCheckState = (rowId: string, checkField: BudgetCheckField, checked: boolean) => {
|
||||||
detailRows.value = detailRows.value.map(row => {
|
for (const row of detailRows.value) {
|
||||||
if (row.id !== rowId) return row
|
if (row.id !== rowId) continue
|
||||||
if (checkField === 'benchmarkBudgetBasicChecked') {
|
if (checkField === 'benchmarkBudgetBasicChecked') {
|
||||||
return {
|
row.benchmarkBudgetBasicChecked = checked
|
||||||
...row,
|
row.benchmarkBudgetBasic = checked ? row.benchmarkBudgetBasic : 0
|
||||||
benchmarkBudgetBasicChecked: checked,
|
return
|
||||||
benchmarkBudgetBasic: checked ? row.benchmarkBudgetBasic : 0
|
|
||||||
}
|
}
|
||||||
|
row.benchmarkBudgetOptionalChecked = checked
|
||||||
|
row.benchmarkBudgetOptional = checked ? row.benchmarkBudgetOptional : 0
|
||||||
|
return
|
||||||
}
|
}
|
||||||
return {
|
|
||||||
...row,
|
|
||||||
benchmarkBudgetOptionalChecked: checked,
|
|
||||||
benchmarkBudgetOptional: checked ? row.benchmarkBudgetOptional : 0
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const createBudgetCellRendererWithCheck = (checkField: BudgetCheckField) => (params: any) => {
|
const createBudgetCellRendererWithCheck = (checkField: BudgetCheckField) => (params: any) => {
|
||||||
@ -648,28 +648,42 @@ const createBudgetCellRendererWithCheck = (checkField: BudgetCheckField) => (par
|
|||||||
wrapper.style.justifyContent = 'space-between'
|
wrapper.style.justifyContent = 'space-between'
|
||||||
wrapper.style.gap = '6px'
|
wrapper.style.gap = '6px'
|
||||||
wrapper.style.width = '100%'
|
wrapper.style.width = '100%'
|
||||||
|
wrapper.addEventListener('pointerdown', event => event.stopPropagation())
|
||||||
|
wrapper.addEventListener('mousedown', event => event.stopPropagation())
|
||||||
|
wrapper.addEventListener('click', event => event.stopPropagation())
|
||||||
|
wrapper.addEventListener('dblclick', event => event.stopPropagation())
|
||||||
|
|
||||||
const checkbox = document.createElement('input')
|
const checkbox = document.createElement('input')
|
||||||
checkbox.type = 'checkbox'
|
checkbox.type = 'checkbox'
|
||||||
checkbox.className = 'cursor-pointer'
|
checkbox.className = 'cursor-pointer'
|
||||||
|
|
||||||
checkbox.checked = params.data[checkField] !== false
|
checkbox.checked = params.data[checkField] !== false
|
||||||
|
checkbox.addEventListener('pointerdown', event => event.stopPropagation())
|
||||||
checkbox.addEventListener('mousedown', event => event.stopPropagation())
|
checkbox.addEventListener('mousedown', event => event.stopPropagation())
|
||||||
checkbox.addEventListener('click', event => event.stopPropagation())
|
checkbox.addEventListener('click', event => event.stopPropagation())
|
||||||
checkbox.addEventListener('change', () => {
|
checkbox.addEventListener('change', event => {
|
||||||
|
event.stopPropagation()
|
||||||
const targetRow = params.data as DetailRow | undefined
|
const targetRow = params.data as DetailRow | undefined
|
||||||
if (!targetRow) return
|
if (!targetRow) return
|
||||||
|
|
||||||
updateBudgetCheckState(targetRow.id, checkField, checkbox.checked)
|
updateBudgetCheckState(targetRow.id, checkField, checkbox.checked)
|
||||||
handleCellValueChanged()
|
handleCellValueChanged()
|
||||||
|
void nextTick(() => {
|
||||||
|
params.api?.redrawRows?.({
|
||||||
|
rowNodes: params.node ? [params.node] : undefined
|
||||||
|
})
|
||||||
params.api?.refreshCells?.({
|
params.api?.refreshCells?.({
|
||||||
rowNodes: params.node ? [params.node] : undefined,
|
rowNodes: params.node ? [params.node] : undefined,
|
||||||
force: true
|
force: true
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
})
|
||||||
|
|
||||||
const valueSpan = document.createElement('span')
|
const valueSpan = document.createElement('span')
|
||||||
valueSpan.textContent = valueText
|
valueSpan.textContent = valueText
|
||||||
|
valueSpan.addEventListener('pointerdown', event => event.stopPropagation())
|
||||||
|
valueSpan.addEventListener('mousedown', event => event.stopPropagation())
|
||||||
|
valueSpan.addEventListener('click', event => event.stopPropagation())
|
||||||
wrapper.append(checkbox, valueSpan)
|
wrapper.append(checkbox, valueSpan)
|
||||||
|
|
||||||
return wrapper
|
return wrapper
|
||||||
@ -685,11 +699,12 @@ const getCheckedBenchmarkBudgetSplitByAmount = (
|
|||||||
if (!split) return null
|
if (!split) return null
|
||||||
const basic = row?.benchmarkBudgetBasicChecked === false ? 0 : split.basic
|
const basic = row?.benchmarkBudgetBasicChecked === false ? 0 : split.basic
|
||||||
const optional = row?.benchmarkBudgetOptionalChecked === false ? 0 : split.optional
|
const optional = row?.benchmarkBudgetOptionalChecked === false ? 0 : split.optional
|
||||||
|
const total = isBenchmarkBudgetFullyUnchecked(row) ? null : roundTo(addNumbers(basic, optional), 2)
|
||||||
return {
|
return {
|
||||||
...split,
|
...split,
|
||||||
basic,
|
basic,
|
||||||
optional,
|
optional,
|
||||||
total: roundTo(addNumbers(basic, optional), 2)
|
total
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -705,6 +720,7 @@ const getBudgetFee = (
|
|||||||
| 'workRatio'
|
| 'workRatio'
|
||||||
>
|
>
|
||||||
) => {
|
) => {
|
||||||
|
if (isBenchmarkBudgetFullyUnchecked(row)) return null
|
||||||
const benchmarkBudgetSplit = getCheckedBenchmarkBudgetSplitByAmount(row)
|
const benchmarkBudgetSplit = getCheckedBenchmarkBudgetSplitByAmount(row)
|
||||||
if (!benchmarkBudgetSplit) return null
|
if (!benchmarkBudgetSplit) return null
|
||||||
|
|
||||||
@ -731,6 +747,7 @@ const getBudgetFeeSplit = (
|
|||||||
| 'workRatio'
|
| 'workRatio'
|
||||||
>
|
>
|
||||||
) => {
|
) => {
|
||||||
|
if (isBenchmarkBudgetFullyUnchecked(row)) return null
|
||||||
const benchmarkBudgetSplit = getCheckedBenchmarkBudgetSplitByAmount(row)
|
const benchmarkBudgetSplit = getCheckedBenchmarkBudgetSplitByAmount(row)
|
||||||
if (!benchmarkBudgetSplit) return null
|
if (!benchmarkBudgetSplit) return null
|
||||||
return getScaleBudgetFeeSplit({
|
return getScaleBudgetFeeSplit({
|
||||||
@ -753,10 +770,79 @@ const getMergeColSpanBeforeTotal = (params: any) => {
|
|||||||
return totalIndex - currentIndex
|
return totalIndex - currentIndex
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const refreshGridAfterColumnReset = async () => {
|
||||||
|
await nextTick()
|
||||||
|
gridApi.value?.refreshHeader()
|
||||||
|
gridApi.value?.refreshCells({ force: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
const restoreAmountColumnDefaults = async () => {
|
||||||
|
gridApi.value?.stopEditing()
|
||||||
|
const htData = await kvStore.getItem<{ detailRows: SourceRow[] }>(HT_DB_KEY.value)
|
||||||
|
const sourceRows = Array.isArray(htData?.detailRows) ? htData.detailRows : []
|
||||||
|
const sourceRowMap = buildContractScaleMap(sourceRows)
|
||||||
|
const onlyCostScaleFallbackAmount = isOnlyCostScaleService.value
|
||||||
|
? calcOnlyCostScaleAmountFromRows(sourceRows as Array<{ amount?: unknown }>)
|
||||||
|
: null
|
||||||
|
|
||||||
|
let changed = false
|
||||||
|
for (const row of detailRows.value) {
|
||||||
|
const sourceRow = getContractScaleRowByMajor(row, sourceRowMap)
|
||||||
|
const nextAmount = row.hasCost
|
||||||
|
? (typeof sourceRow?.amount === 'number' ? sourceRow.amount : onlyCostScaleFallbackAmount)
|
||||||
|
: null
|
||||||
|
if (isSameNullableNumber(row.amount, nextAmount)) continue
|
||||||
|
row.amount = nextAmount
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
if (!changed) return
|
||||||
|
syncComputedValuesToDetailRows()
|
||||||
|
await saveToIndexedDB({ skipComputedSync: true })
|
||||||
|
await refreshGridAfterColumnReset()
|
||||||
|
}
|
||||||
|
|
||||||
|
const restoreConsultCategoryFactorColumnDefaults = async () => {
|
||||||
|
gridApi.value?.stopEditing()
|
||||||
|
await loadFactorDefaults()
|
||||||
|
const nextConsultFactor = getDefaultConsultCategoryFactor()
|
||||||
|
let changed = false
|
||||||
|
for (const row of detailRows.value) {
|
||||||
|
if (isSameNullableNumber(row.consultCategoryFactor, nextConsultFactor)) continue
|
||||||
|
row.consultCategoryFactor = nextConsultFactor
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
if (!changed) return
|
||||||
|
syncComputedValuesToDetailRows()
|
||||||
|
await saveToIndexedDB({ skipComputedSync: true })
|
||||||
|
await refreshGridAfterColumnReset()
|
||||||
|
}
|
||||||
|
|
||||||
|
const restoreMajorFactorColumnDefaults = async () => {
|
||||||
|
gridApi.value?.stopEditing()
|
||||||
|
await loadFactorDefaults()
|
||||||
|
let changed = false
|
||||||
|
for (const row of detailRows.value) {
|
||||||
|
const nextMajorFactor = getDefaultMajorFactorById(resolveRowMajorDictId(row))
|
||||||
|
if (isSameNullableNumber(row.majorFactor, nextMajorFactor)) continue
|
||||||
|
row.majorFactor = nextMajorFactor
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
if (!changed) return
|
||||||
|
syncComputedValuesToDetailRows()
|
||||||
|
await saveToIndexedDB({ skipComputedSync: true })
|
||||||
|
await refreshGridAfterColumnReset()
|
||||||
|
}
|
||||||
|
|
||||||
const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
|
const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
|
||||||
{
|
{
|
||||||
headerName: '造价金额(万元)',
|
headerName: '造价金额(万元)',
|
||||||
field: 'amount',
|
field: 'amount',
|
||||||
|
headerTooltip: '点击右侧↻恢复本列默认造价金额',
|
||||||
|
headerComponent: AgGridResetHeader,
|
||||||
|
headerComponentParams: {
|
||||||
|
onReset: restoreAmountColumnDefaults,
|
||||||
|
resetTitle: '恢复本列默认造价金额'
|
||||||
|
},
|
||||||
headerClass: 'ag-right-aligned-header',
|
headerClass: 'ag-right-aligned-header',
|
||||||
minWidth: 90,
|
minWidth: 90,
|
||||||
flex: 2,
|
flex: 2,
|
||||||
@ -766,9 +852,10 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
|
|||||||
},
|
},
|
||||||
cellClass: params =>
|
cellClass: params =>
|
||||||
!params.node?.group && !params.node?.rowPinned && params.data?.hasCost
|
!params.node?.group && !params.node?.rowPinned && params.data?.hasCost
|
||||||
? 'ag-right-aligned-cell editable-cell-line'
|
? ' editable-cell-line'
|
||||||
: 'ag-right-aligned-cell',
|
: '',
|
||||||
cellClassRules: {
|
cellClassRules: {
|
||||||
|
'ag-right-aligned-cell':()=>true,
|
||||||
'editable-cell-empty': params =>
|
'editable-cell-empty': params =>
|
||||||
!params.node?.group && !params.node?.rowPinned && Boolean(params.data?.hasCost) && (params.value == null || params.value === '')
|
!params.node?.group && !params.node?.rowPinned && Boolean(params.data?.hasCost) && (params.value == null || params.value === '')
|
||||||
},
|
},
|
||||||
@ -786,12 +873,17 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
|
|||||||
headerClass: 'ag-right-aligned-header',
|
headerClass: 'ag-right-aligned-header',
|
||||||
minWidth: 130,
|
minWidth: 130,
|
||||||
flex: 1,
|
flex: 1,
|
||||||
cellClass: 'ag-right-aligned-cell',
|
cellClassRules: {
|
||||||
|
'ag-right-aligned-cell': () => true
|
||||||
|
},
|
||||||
valueGetter: params =>
|
valueGetter: params =>
|
||||||
params.node?.rowPinned
|
params.node?.rowPinned
|
||||||
? null
|
? null
|
||||||
: getCheckedBenchmarkBudgetSplitByAmount(params.data)?.basic ?? null,
|
: getCheckedBenchmarkBudgetSplitByAmount(params.data)?.basic ?? null,
|
||||||
cellRenderer: createBudgetCellRendererWithCheck('benchmarkBudgetBasicChecked'),
|
cellRenderer: createBudgetCellRendererWithCheck('benchmarkBudgetBasicChecked'),
|
||||||
|
cellRendererParams: {
|
||||||
|
suppressMouseEventHandling: () => true
|
||||||
|
},
|
||||||
valueFormatter: formatReadonlyMoney
|
valueFormatter: formatReadonlyMoney
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -801,12 +893,17 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
|
|||||||
headerClass: 'ag-right-aligned-header',
|
headerClass: 'ag-right-aligned-header',
|
||||||
minWidth: 130,
|
minWidth: 130,
|
||||||
flex: 1,
|
flex: 1,
|
||||||
cellClass: 'ag-right-aligned-cell',
|
cellClassRules: {
|
||||||
|
'ag-right-aligned-cell': () => true
|
||||||
|
},
|
||||||
valueGetter: params =>
|
valueGetter: params =>
|
||||||
params.node?.rowPinned
|
params.node?.rowPinned
|
||||||
? null
|
? null
|
||||||
: getCheckedBenchmarkBudgetSplitByAmount(params.data)?.optional ?? null,
|
: getCheckedBenchmarkBudgetSplitByAmount(params.data)?.optional ?? null,
|
||||||
cellRenderer: createBudgetCellRendererWithCheck('benchmarkBudgetOptionalChecked'),
|
cellRenderer: createBudgetCellRendererWithCheck('benchmarkBudgetOptionalChecked'),
|
||||||
|
cellRendererParams: {
|
||||||
|
suppressMouseEventHandling: () => true
|
||||||
|
},
|
||||||
valueFormatter: formatReadonlyMoney
|
valueFormatter: formatReadonlyMoney
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -816,7 +913,9 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
|
|||||||
headerClass: 'ag-right-aligned-header',
|
headerClass: 'ag-right-aligned-header',
|
||||||
minWidth: 100,
|
minWidth: 100,
|
||||||
flex: 1,
|
flex: 1,
|
||||||
cellClass: 'ag-right-aligned-cell',
|
cellClassRules: {
|
||||||
|
'ag-right-aligned-cell': () => true
|
||||||
|
},
|
||||||
valueGetter: params =>
|
valueGetter: params =>
|
||||||
params.node?.rowPinned
|
params.node?.rowPinned
|
||||||
? null
|
? null
|
||||||
@ -833,15 +932,22 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
|
|||||||
headerName: '咨询分类系数',
|
headerName: '咨询分类系数',
|
||||||
field: 'consultCategoryFactor',
|
field: 'consultCategoryFactor',
|
||||||
colId: 'consultCategoryFactor',
|
colId: 'consultCategoryFactor',
|
||||||
|
headerTooltip: '点击右侧↻恢复本列默认咨询分类系数',
|
||||||
|
headerComponent: AgGridResetHeader,
|
||||||
|
headerComponentParams: {
|
||||||
|
onReset: restoreConsultCategoryFactorColumnDefaults,
|
||||||
|
resetTitle: '恢复本列默认咨询分类系数'
|
||||||
|
},
|
||||||
headerClass: 'ag-right-aligned-header',
|
headerClass: 'ag-right-aligned-header',
|
||||||
minWidth: 80,
|
minWidth: 80,
|
||||||
flex: 1,
|
flex: 1,
|
||||||
editable: params => !params.node?.group && !params.node?.rowPinned,
|
editable: params => !params.node?.group && !params.node?.rowPinned,
|
||||||
cellClass: params =>
|
cellClass: params =>
|
||||||
!params.node?.group && !params.node?.rowPinned
|
!params.node?.group && !params.node?.rowPinned
|
||||||
? 'ag-right-aligned-cell editable-cell-line'
|
? 'editable-cell-line'
|
||||||
: 'ag-right-aligned-cell',
|
: '',
|
||||||
cellClassRules: {
|
cellClassRules: {
|
||||||
|
'ag-right-aligned-cell': () => true,
|
||||||
'editable-cell-empty': params =>
|
'editable-cell-empty': params =>
|
||||||
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
|
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
|
||||||
},
|
},
|
||||||
@ -852,15 +958,22 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
|
|||||||
headerName: '专业系数',
|
headerName: '专业系数',
|
||||||
field: 'majorFactor',
|
field: 'majorFactor',
|
||||||
colId: 'majorFactor',
|
colId: 'majorFactor',
|
||||||
|
headerTooltip: '点击右侧↻恢复本列默认专业系数',
|
||||||
|
headerComponent: AgGridResetHeader,
|
||||||
|
headerComponentParams: {
|
||||||
|
onReset: restoreMajorFactorColumnDefaults,
|
||||||
|
resetTitle: '恢复本列默认专业系数'
|
||||||
|
},
|
||||||
headerClass: 'ag-right-aligned-header',
|
headerClass: 'ag-right-aligned-header',
|
||||||
minWidth: 80,
|
minWidth: 80,
|
||||||
flex: 1,
|
flex: 1,
|
||||||
editable: params => !params.node?.group && !params.node?.rowPinned,
|
editable: params => !params.node?.group && !params.node?.rowPinned,
|
||||||
cellClass: params =>
|
cellClass: params =>
|
||||||
!params.node?.group && !params.node?.rowPinned
|
!params.node?.group && !params.node?.rowPinned
|
||||||
? 'ag-right-aligned-cell editable-cell-line'
|
? 'editable-cell-line'
|
||||||
: 'ag-right-aligned-cell',
|
: '',
|
||||||
cellClassRules: {
|
cellClassRules: {
|
||||||
|
'ag-right-aligned-cell': () => true,
|
||||||
'editable-cell-empty': params =>
|
'editable-cell-empty': params =>
|
||||||
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
|
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
|
||||||
},
|
},
|
||||||
@ -877,9 +990,10 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
|
|||||||
editable: params => !params.node?.group && !params.node?.rowPinned,
|
editable: params => !params.node?.group && !params.node?.rowPinned,
|
||||||
cellClass: params =>
|
cellClass: params =>
|
||||||
!params.node?.group && !params.node?.rowPinned
|
!params.node?.group && !params.node?.rowPinned
|
||||||
? 'ag-right-aligned-cell editable-cell-line'
|
? 'editable-cell-line'
|
||||||
: 'ag-right-aligned-cell',
|
: '',
|
||||||
cellClassRules: {
|
cellClassRules: {
|
||||||
|
'ag-right-aligned-cell': () => true,
|
||||||
'editable-cell-empty': params =>
|
'editable-cell-empty': params =>
|
||||||
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
|
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
|
||||||
},
|
},
|
||||||
@ -896,9 +1010,10 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
|
|||||||
editable: params => !params.node?.group && !params.node?.rowPinned,
|
editable: params => !params.node?.group && !params.node?.rowPinned,
|
||||||
cellClass: params =>
|
cellClass: params =>
|
||||||
!params.node?.group && !params.node?.rowPinned
|
!params.node?.group && !params.node?.rowPinned
|
||||||
? 'ag-right-aligned-cell editable-cell-line'
|
? 'editable-cell-line'
|
||||||
: 'ag-right-aligned-cell',
|
: '',
|
||||||
cellClassRules: {
|
cellClassRules: {
|
||||||
|
'ag-right-aligned-cell': () => true,
|
||||||
'editable-cell-empty': params =>
|
'editable-cell-empty': params =>
|
||||||
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
|
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
|
||||||
},
|
},
|
||||||
@ -912,8 +1027,10 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
|
|||||||
headerClass: 'ag-right-aligned-header',
|
headerClass: 'ag-right-aligned-header',
|
||||||
minWidth: 120,
|
minWidth: 120,
|
||||||
flex: 1,
|
flex: 1,
|
||||||
cellClass: 'ag-right-aligned-cell',
|
|
||||||
aggFunc: decimalAggSum,
|
aggFunc: decimalAggSum,
|
||||||
|
cellClassRules: {
|
||||||
|
'ag-right-aligned-cell': () => true
|
||||||
|
},
|
||||||
valueGetter: params => (params.node?.rowPinned ? params.data?.budgetFee ?? null : getBudgetFee(params.data)),
|
valueGetter: params => (params.node?.rowPinned ? params.data?.budgetFee ?? null : getBudgetFee(params.data)),
|
||||||
valueFormatter: formatReadonlyMoney
|
valueFormatter: formatReadonlyMoney
|
||||||
}
|
}
|
||||||
@ -986,7 +1103,9 @@ const autoGroupColumnDef: ColDef = {
|
|||||||
const sumNullableBy = <T>(rows: T[], pick: (row: T) => unknown): number | null => {
|
const sumNullableBy = <T>(rows: T[], pick: (row: T) => unknown): number | null => {
|
||||||
let hasValid = false
|
let hasValid = false
|
||||||
const total = sumByNumber(rows, row => {
|
const total = sumByNumber(rows, row => {
|
||||||
const value = Number(pick(row))
|
const raw = pick(row)
|
||||||
|
if (raw == null || raw === '') return null
|
||||||
|
const value = Number(raw)
|
||||||
if (!Number.isFinite(value)) return null
|
if (!Number.isFinite(value)) return null
|
||||||
hasValid = true
|
hasValid = true
|
||||||
return value
|
return value
|
||||||
@ -996,9 +1115,7 @@ const sumNullableBy = <T>(rows: T[], pick: (row: T) => unknown): number | null =
|
|||||||
|
|
||||||
const totalBudgetFeeBasic = computed(() => sumNullableBy(detailRows.value, row => getBudgetFeeSplit(row)?.basic))
|
const totalBudgetFeeBasic = computed(() => sumNullableBy(detailRows.value, row => getBudgetFeeSplit(row)?.basic))
|
||||||
const totalBudgetFeeOptional = computed(() => sumNullableBy(detailRows.value, row => getBudgetFeeSplit(row)?.optional))
|
const totalBudgetFeeOptional = computed(() => sumNullableBy(detailRows.value, row => getBudgetFeeSplit(row)?.optional))
|
||||||
const totalBudgetFee = computed(() =>
|
const totalBudgetFee = computed(() => sumNullableBy(detailRows.value, row => getBudgetFee(row)))
|
||||||
sumNullableBy(detailRows.value.filter(e => e.budgetFee !== null && e.budgetFee !== undefined), row => getBudgetFee(row))
|
|
||||||
)
|
|
||||||
const pinnedTopRowData = computed(() => {
|
const pinnedTopRowData = computed(() => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@ -1058,14 +1175,14 @@ const syncComputedValuesToDetailRows = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const buildPersistDetailRows = () => {
|
const buildPersistDetailRows = () => detailRows.value.map(row => ({ ...row }))
|
||||||
syncComputedValuesToDetailRows()
|
|
||||||
return detailRows.value.map(row => ({ ...row }))
|
|
||||||
}
|
|
||||||
|
|
||||||
const saveToIndexedDB = async () => {
|
const saveToIndexedDB = async (options?: { skipComputedSync?: boolean }) => {
|
||||||
if (shouldSkipPersist()) return
|
if (shouldSkipPersist()) return
|
||||||
try {
|
try {
|
||||||
|
if (!options?.skipComputedSync) {
|
||||||
|
syncComputedValuesToDetailRows()
|
||||||
|
}
|
||||||
const payload = {
|
const payload = {
|
||||||
detailRows: JSON.parse(JSON.stringify(buildPersistDetailRows())),
|
detailRows: JSON.parse(JSON.stringify(buildPersistDetailRows())),
|
||||||
projectCount: getTargetProjectCount()
|
projectCount: getTargetProjectCount()
|
||||||
@ -1089,6 +1206,101 @@ const saveToIndexedDB = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isSameNullableNumber = (left: number | null | undefined, right: number | null | undefined) => {
|
||||||
|
if (left == null && right == null) return true
|
||||||
|
if (left == null || right == null) return false
|
||||||
|
return roundTo(left, 6) === roundTo(right, 6)
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildContractScaleMap = (rows: SourceRow[] | undefined) => {
|
||||||
|
const map = new Map<string, SourceRow>()
|
||||||
|
for (const row of rows || []) {
|
||||||
|
const majorDictId = resolveRowMajorDictId(row)
|
||||||
|
if (!majorDictId) continue
|
||||||
|
const projectIndex = resolveRowProjectIndex(row)
|
||||||
|
map.set(makeProjectMajorKey(projectIndex, majorDictId), row)
|
||||||
|
}
|
||||||
|
return map
|
||||||
|
}
|
||||||
|
|
||||||
|
const getContractScaleRowByMajor = (row: DetailRow, map: Map<string, SourceRow>) => {
|
||||||
|
const majorDictId = resolveRowMajorDictId(row)
|
||||||
|
if (!majorDictId) return undefined
|
||||||
|
const projectIndex = resolveRowProjectIndex(row)
|
||||||
|
return map.get(makeProjectMajorKey(projectIndex, majorDictId))
|
||||||
|
|| (projectIndex > 1 ? map.get(makeProjectMajorKey(1, majorDictId)) : undefined)
|
||||||
|
}
|
||||||
|
|
||||||
|
const syncLinkedFieldsFromContractAndFactors = async () => {
|
||||||
|
if (detailRows.value.length === 0) return
|
||||||
|
await loadFactorDefaults()
|
||||||
|
const consultFactor = getDefaultConsultCategoryFactor()
|
||||||
|
|
||||||
|
let changed = false
|
||||||
|
detailRows.value = detailRows.value.map(row => {
|
||||||
|
const majorDictId = resolveRowMajorDictId(row)
|
||||||
|
const nextConsultFactor = consultFactor
|
||||||
|
const nextMajorFactor = getDefaultMajorFactorById(majorDictId)
|
||||||
|
if (
|
||||||
|
isSameNullableNumber(row.consultCategoryFactor, nextConsultFactor)
|
||||||
|
&& isSameNullableNumber(row.majorFactor, nextMajorFactor)
|
||||||
|
) {
|
||||||
|
return row
|
||||||
|
}
|
||||||
|
changed = true
|
||||||
|
return {
|
||||||
|
...row,
|
||||||
|
consultCategoryFactor: nextConsultFactor,
|
||||||
|
majorFactor: nextMajorFactor
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (!changed) return
|
||||||
|
syncComputedValuesToDetailRows()
|
||||||
|
await saveToIndexedDB({ skipComputedSync: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
const syncLinkedScaleValuesFromContract = async () => {
|
||||||
|
if (detailRows.value.length === 0) return
|
||||||
|
const htData = await kvStore.getItem<{ detailRows: SourceRow[] }>(HT_DB_KEY.value)
|
||||||
|
const sourceRows = Array.isArray(htData?.detailRows) ? htData.detailRows : []
|
||||||
|
const sourceRowMap = buildContractScaleMap(sourceRows)
|
||||||
|
const onlyCostScaleFallbackAmount = isOnlyCostScaleService.value
|
||||||
|
? calcOnlyCostScaleAmountFromRows(sourceRows as Array<{ amount?: unknown }>)
|
||||||
|
: null
|
||||||
|
|
||||||
|
let changed = false
|
||||||
|
detailRows.value = detailRows.value.map(row => {
|
||||||
|
const sourceRow = getContractScaleRowByMajor(row, sourceRowMap)
|
||||||
|
const nextAmount = row.hasCost
|
||||||
|
? (typeof sourceRow?.amount === 'number' ? sourceRow.amount : onlyCostScaleFallbackAmount)
|
||||||
|
: null
|
||||||
|
if (isSameNullableNumber(row.amount, nextAmount)) return row
|
||||||
|
changed = true
|
||||||
|
return {
|
||||||
|
...row,
|
||||||
|
amount: nextAmount
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (!changed) return
|
||||||
|
syncComputedValuesToDetailRows()
|
||||||
|
await saveToIndexedDB({ skipComputedSync: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
const linkedSourceSignature = computed(() => JSON.stringify({
|
||||||
|
consultFactor:
|
||||||
|
zxFwPricingStore.keyedStates[HT_CONSULT_FACTOR_KEY.value]
|
||||||
|
?? kvStore.entries[HT_CONSULT_FACTOR_KEY.value]
|
||||||
|
?? null,
|
||||||
|
majorFactor:
|
||||||
|
zxFwPricingStore.keyedStates[HT_MAJOR_FACTOR_KEY.value]
|
||||||
|
?? kvStore.entries[HT_MAJOR_FACTOR_KEY.value]
|
||||||
|
?? null
|
||||||
|
}))
|
||||||
|
|
||||||
|
const linkedContractScaleSignature = computed(() => JSON.stringify(
|
||||||
|
kvStore.entries[HT_DB_KEY.value] ?? null
|
||||||
|
))
|
||||||
|
|
||||||
const getProjectMajorKeyFromRow = (row: Partial<DetailRow> | undefined) => {
|
const getProjectMajorKeyFromRow = (row: Partial<DetailRow> | undefined) => {
|
||||||
if (!row) return ''
|
if (!row) return ''
|
||||||
const majorDictId = resolveRowMajorDictId(row)
|
const majorDictId = resolveRowMajorDictId(row)
|
||||||
@ -1134,7 +1346,7 @@ const applyProjectCountChange = async (nextValue: unknown) => {
|
|||||||
? buildOnlyCostScaleRows(previousRows as any, { projectCount: normalized })
|
? buildOnlyCostScaleRows(previousRows as any, { projectCount: normalized })
|
||||||
: mergeWithDictRows(previousRows as any, { projectCount: normalized })
|
: mergeWithDictRows(previousRows as any, { projectCount: normalized })
|
||||||
syncComputedValuesToDetailRows()
|
syncComputedValuesToDetailRows()
|
||||||
await saveToIndexedDB()
|
await saveToIndexedDB({ skipComputedSync: true })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1167,7 +1379,7 @@ const applyProjectCountChange = async (nextValue: unknown) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
syncComputedValuesToDetailRows()
|
syncComputedValuesToDetailRows()
|
||||||
await saveToIndexedDB()
|
await saveToIndexedDB({ skipComputedSync: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadFromIndexedDB = async () => {
|
const loadFromIndexedDB = async () => {
|
||||||
@ -1282,7 +1494,7 @@ const clearAllData = async () => {
|
|||||||
|
|
||||||
const handleCellValueChanged = () => {
|
const handleCellValueChanged = () => {
|
||||||
syncComputedValuesToDetailRows()
|
syncComputedValuesToDetailRows()
|
||||||
void saveToIndexedDB()
|
void saveToIndexedDB({ skipComputedSync: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleGridReady = (event: GridReadyEvent<DetailRow>) => {
|
const handleGridReady = (event: GridReadyEvent<DetailRow>) => {
|
||||||
@ -1291,10 +1503,12 @@ const handleGridReady = (event: GridReadyEvent<DetailRow>) => {
|
|||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await loadFromIndexedDB()
|
await loadFromIndexedDB()
|
||||||
|
await syncLinkedFieldsFromContractAndFactors()
|
||||||
})
|
})
|
||||||
|
|
||||||
onActivated(() => {
|
onActivated(async () => {
|
||||||
void loadFromIndexedDB()
|
await loadFromIndexedDB()
|
||||||
|
await syncLinkedFieldsFromContractAndFactors()
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
@ -1302,6 +1516,14 @@ onBeforeUnmount(() => {
|
|||||||
gridApi.value = null
|
gridApi.value = null
|
||||||
void saveToIndexedDB()
|
void saveToIndexedDB()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
watch(linkedSourceSignature, () => {
|
||||||
|
void syncLinkedFieldsFromContractAndFactors()
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(linkedContractScaleSignature, () => {
|
||||||
|
void syncLinkedScaleValuesFromContract()
|
||||||
|
})
|
||||||
const processCellForClipboard = (params: any) => {
|
const processCellForClipboard = (params: any) => {
|
||||||
if (Array.isArray(params.value)) {
|
if (Array.isArray(params.value)) {
|
||||||
return JSON.stringify(params.value); // 数组转字符串复制
|
return JSON.stringify(params.value); // 数组转字符串复制
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onActivated, onBeforeUnmount, onMounted, ref } from 'vue'
|
import { computed, nextTick, onActivated, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||||
import { AgGridVue } from 'ag-grid-vue3'
|
import { AgGridVue } from 'ag-grid-vue3'
|
||||||
import type { ColDef, ColGroupDef, GridApi, GridReadyEvent } from 'ag-grid-community'
|
import type { ColDef, ColGroupDef, GridApi, GridReadyEvent } from 'ag-grid-community'
|
||||||
import { getMajorDictEntries, getServiceDictItemById, industryTypeList, isMajorIdInIndustryScope } from '@/sql'
|
import { getMajorDictEntries, getServiceDictItemById, industryTypeList, isMajorIdInIndustryScope } from '@/sql'
|
||||||
@ -12,6 +12,7 @@ import { useKvStore } from '@/pinia/kv'
|
|||||||
import { loadConsultCategoryFactorMap, loadMajorFactorMap } from '@/lib/xmFactorDefaults'
|
import { loadConsultCategoryFactorMap, loadMajorFactorMap } from '@/lib/xmFactorDefaults'
|
||||||
import { parseNumberOrNull } from '@/lib/number'
|
import { parseNumberOrNull } from '@/lib/number'
|
||||||
import { getBenchmarkBudgetSplitByScale, getScaleBudgetFeeSplit } from '@/lib/pricingScaleFee'
|
import { getBenchmarkBudgetSplitByScale, getScaleBudgetFeeSplit } from '@/lib/pricingScaleFee'
|
||||||
|
import { AgGridResetHeader } from '@/lib/agGridResetHeader'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
import {
|
||||||
AlertDialogAction,
|
AlertDialogAction,
|
||||||
@ -481,23 +482,22 @@ const formatReadonlyMoney = (params: any) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type BudgetCheckField = 'benchmarkBudgetBasicChecked' | 'benchmarkBudgetOptionalChecked'
|
type BudgetCheckField = 'benchmarkBudgetBasicChecked' | 'benchmarkBudgetOptionalChecked'
|
||||||
|
const isBenchmarkBudgetFullyUnchecked = (
|
||||||
|
row?: Pick<DetailRow, 'benchmarkBudgetBasicChecked' | 'benchmarkBudgetOptionalChecked'>
|
||||||
|
) => row?.benchmarkBudgetBasicChecked === false && row?.benchmarkBudgetOptionalChecked === false
|
||||||
|
|
||||||
const updateBudgetCheckState = (rowId: string, checkField: BudgetCheckField, checked: boolean) => {
|
const updateBudgetCheckState = (rowId: string, checkField: BudgetCheckField, checked: boolean) => {
|
||||||
detailRows.value = detailRows.value.map(row => {
|
for (const row of detailRows.value) {
|
||||||
if (row.id !== rowId) return row
|
if (row.id !== rowId) continue
|
||||||
if (checkField === 'benchmarkBudgetBasicChecked') {
|
if (checkField === 'benchmarkBudgetBasicChecked') {
|
||||||
return {
|
row.benchmarkBudgetBasicChecked = checked
|
||||||
...row,
|
row.benchmarkBudgetBasic = checked ? row.benchmarkBudgetBasic : 0
|
||||||
benchmarkBudgetBasicChecked: checked,
|
return
|
||||||
benchmarkBudgetBasic: checked ? row.benchmarkBudgetBasic : 0
|
|
||||||
}
|
}
|
||||||
|
row.benchmarkBudgetOptionalChecked = checked
|
||||||
|
row.benchmarkBudgetOptional = checked ? row.benchmarkBudgetOptional : 0
|
||||||
|
return
|
||||||
}
|
}
|
||||||
return {
|
|
||||||
...row,
|
|
||||||
benchmarkBudgetOptionalChecked: checked,
|
|
||||||
benchmarkBudgetOptional: checked ? row.benchmarkBudgetOptional : 0
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const createBudgetCellRendererWithCheck = (checkField: BudgetCheckField) => (params: any) => {
|
const createBudgetCellRendererWithCheck = (checkField: BudgetCheckField) => (params: any) => {
|
||||||
@ -513,26 +513,40 @@ const createBudgetCellRendererWithCheck = (checkField: BudgetCheckField) => (par
|
|||||||
wrapper.style.justifyContent = 'space-between'
|
wrapper.style.justifyContent = 'space-between'
|
||||||
wrapper.style.gap = '6px'
|
wrapper.style.gap = '6px'
|
||||||
wrapper.style.width = '100%'
|
wrapper.style.width = '100%'
|
||||||
|
wrapper.addEventListener('pointerdown', event => event.stopPropagation())
|
||||||
|
wrapper.addEventListener('mousedown', event => event.stopPropagation())
|
||||||
|
wrapper.addEventListener('click', event => event.stopPropagation())
|
||||||
|
wrapper.addEventListener('dblclick', event => event.stopPropagation())
|
||||||
|
|
||||||
const checkbox = document.createElement('input')
|
const checkbox = document.createElement('input')
|
||||||
checkbox.type = 'checkbox'
|
checkbox.type = 'checkbox'
|
||||||
checkbox.className = 'cursor-pointer'
|
checkbox.className = 'cursor-pointer'
|
||||||
checkbox.checked = params.data[checkField] !== false
|
checkbox.checked = params.data[checkField] !== false
|
||||||
|
checkbox.addEventListener('pointerdown', event => event.stopPropagation())
|
||||||
checkbox.addEventListener('mousedown', event => event.stopPropagation())
|
checkbox.addEventListener('mousedown', event => event.stopPropagation())
|
||||||
checkbox.addEventListener('click', event => event.stopPropagation())
|
checkbox.addEventListener('click', event => event.stopPropagation())
|
||||||
checkbox.addEventListener('change', () => {
|
checkbox.addEventListener('change', event => {
|
||||||
|
event.stopPropagation()
|
||||||
const targetRow = params.data as DetailRow | undefined
|
const targetRow = params.data as DetailRow | undefined
|
||||||
if (!targetRow) return
|
if (!targetRow) return
|
||||||
updateBudgetCheckState(targetRow.id, checkField, checkbox.checked)
|
updateBudgetCheckState(targetRow.id, checkField, checkbox.checked)
|
||||||
handleCellValueChanged()
|
handleCellValueChanged()
|
||||||
|
void nextTick(() => {
|
||||||
|
params.api?.redrawRows?.({
|
||||||
|
rowNodes: params.node ? [params.node] : undefined
|
||||||
|
})
|
||||||
params.api?.refreshCells?.({
|
params.api?.refreshCells?.({
|
||||||
rowNodes: params.node ? [params.node] : undefined,
|
rowNodes: params.node ? [params.node] : undefined,
|
||||||
force: true
|
force: true
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
})
|
||||||
|
|
||||||
const valueSpan = document.createElement('span')
|
const valueSpan = document.createElement('span')
|
||||||
valueSpan.textContent = valueText
|
valueSpan.textContent = valueText
|
||||||
|
valueSpan.addEventListener('pointerdown', event => event.stopPropagation())
|
||||||
|
valueSpan.addEventListener('mousedown', event => event.stopPropagation())
|
||||||
|
valueSpan.addEventListener('click', event => event.stopPropagation())
|
||||||
wrapper.append(checkbox, valueSpan)
|
wrapper.append(checkbox, valueSpan)
|
||||||
|
|
||||||
return wrapper
|
return wrapper
|
||||||
@ -548,11 +562,12 @@ const getCheckedBenchmarkBudgetSplitByLandArea = (
|
|||||||
if (!split) return null
|
if (!split) return null
|
||||||
const basic = row?.benchmarkBudgetBasicChecked === false ? 0 : split.basic
|
const basic = row?.benchmarkBudgetBasicChecked === false ? 0 : split.basic
|
||||||
const optional = row?.benchmarkBudgetOptionalChecked === false ? 0 : split.optional
|
const optional = row?.benchmarkBudgetOptionalChecked === false ? 0 : split.optional
|
||||||
|
const total = isBenchmarkBudgetFullyUnchecked(row) ? null : roundTo(addNumbers(basic, optional), 2)
|
||||||
return {
|
return {
|
||||||
...split,
|
...split,
|
||||||
basic,
|
basic,
|
||||||
optional,
|
optional,
|
||||||
total: roundTo(addNumbers(basic, optional), 2)
|
total
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -568,6 +583,7 @@ const getBudgetFee = (
|
|||||||
| 'workRatio'
|
| 'workRatio'
|
||||||
>
|
>
|
||||||
) => {
|
) => {
|
||||||
|
if (isBenchmarkBudgetFullyUnchecked(row)) return null
|
||||||
const benchmarkBudgetSplit = getCheckedBenchmarkBudgetSplitByLandArea(row)
|
const benchmarkBudgetSplit = getCheckedBenchmarkBudgetSplitByLandArea(row)
|
||||||
if (!benchmarkBudgetSplit) return null
|
if (!benchmarkBudgetSplit) return null
|
||||||
|
|
||||||
@ -594,6 +610,7 @@ const getBudgetFeeSplit = (
|
|||||||
| 'workRatio'
|
| 'workRatio'
|
||||||
>
|
>
|
||||||
) => {
|
) => {
|
||||||
|
if (isBenchmarkBudgetFullyUnchecked(row)) return null
|
||||||
const benchmarkBudgetSplit = getCheckedBenchmarkBudgetSplitByLandArea(row)
|
const benchmarkBudgetSplit = getCheckedBenchmarkBudgetSplitByLandArea(row)
|
||||||
if (!benchmarkBudgetSplit) return null
|
if (!benchmarkBudgetSplit) return null
|
||||||
return getScaleBudgetFeeSplit({
|
return getScaleBudgetFeeSplit({
|
||||||
@ -627,10 +644,74 @@ const formatEditableFlexibleNumber = (params: any) => {
|
|||||||
return formatThousandsFlexible(params.value, 3)
|
return formatThousandsFlexible(params.value, 3)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const refreshGridAfterColumnReset = async () => {
|
||||||
|
await nextTick()
|
||||||
|
gridApi.value?.refreshHeader()
|
||||||
|
gridApi.value?.refreshCells({ force: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
const restoreLandAreaColumnDefaults = async () => {
|
||||||
|
gridApi.value?.stopEditing()
|
||||||
|
const htData = await kvStore.getItem<{ detailRows: SourceRow[] }>(HT_DB_KEY.value)
|
||||||
|
const sourceRows = Array.isArray(htData?.detailRows) ? htData.detailRows : []
|
||||||
|
const sourceRowMap = buildContractScaleMap(sourceRows)
|
||||||
|
|
||||||
|
let changed = false
|
||||||
|
for (const row of detailRows.value) {
|
||||||
|
const sourceRow = getContractScaleRowByMajor(row, sourceRowMap)
|
||||||
|
const nextLandArea = row.hasArea ? (typeof sourceRow?.landArea === 'number' ? sourceRow.landArea : null) : null
|
||||||
|
if (isSameNullableNumber(row.landArea, nextLandArea)) continue
|
||||||
|
row.landArea = nextLandArea
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
if (!changed) return
|
||||||
|
syncComputedValuesToDetailRows()
|
||||||
|
await saveToIndexedDB({ skipComputedSync: true })
|
||||||
|
await refreshGridAfterColumnReset()
|
||||||
|
}
|
||||||
|
|
||||||
|
const restoreConsultCategoryFactorColumnDefaults = async () => {
|
||||||
|
gridApi.value?.stopEditing()
|
||||||
|
await loadFactorDefaults()
|
||||||
|
const nextConsultFactor = getDefaultConsultCategoryFactor()
|
||||||
|
let changed = false
|
||||||
|
for (const row of detailRows.value) {
|
||||||
|
if (isSameNullableNumber(row.consultCategoryFactor, nextConsultFactor)) continue
|
||||||
|
row.consultCategoryFactor = nextConsultFactor
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
if (!changed) return
|
||||||
|
syncComputedValuesToDetailRows()
|
||||||
|
await saveToIndexedDB({ skipComputedSync: true })
|
||||||
|
await refreshGridAfterColumnReset()
|
||||||
|
}
|
||||||
|
|
||||||
|
const restoreMajorFactorColumnDefaults = async () => {
|
||||||
|
gridApi.value?.stopEditing()
|
||||||
|
await loadFactorDefaults()
|
||||||
|
let changed = false
|
||||||
|
for (const row of detailRows.value) {
|
||||||
|
const nextMajorFactor = getDefaultMajorFactorById(resolveRowMajorDictId(row))
|
||||||
|
if (isSameNullableNumber(row.majorFactor, nextMajorFactor)) continue
|
||||||
|
row.majorFactor = nextMajorFactor
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
if (!changed) return
|
||||||
|
syncComputedValuesToDetailRows()
|
||||||
|
await saveToIndexedDB({ skipComputedSync: true })
|
||||||
|
await refreshGridAfterColumnReset()
|
||||||
|
}
|
||||||
|
|
||||||
const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
|
const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
|
||||||
{
|
{
|
||||||
headerName: '用地面积(亩)',
|
headerName: '用地面积(亩)',
|
||||||
field: 'landArea',
|
field: 'landArea',
|
||||||
|
headerTooltip: '点击右侧↻恢复本列默认用地面积',
|
||||||
|
headerComponent: AgGridResetHeader,
|
||||||
|
headerComponentParams: {
|
||||||
|
onReset: restoreLandAreaColumnDefaults,
|
||||||
|
resetTitle: '恢复本列默认用地面积'
|
||||||
|
},
|
||||||
headerClass: 'ag-right-aligned-header',
|
headerClass: 'ag-right-aligned-header',
|
||||||
minWidth: 90,
|
minWidth: 90,
|
||||||
|
|
||||||
@ -639,9 +720,10 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
|
|||||||
editable: params => !params.node?.group && !params.node?.rowPinned && Boolean(params.data?.hasArea),
|
editable: params => !params.node?.group && !params.node?.rowPinned && Boolean(params.data?.hasArea),
|
||||||
cellClass: params =>
|
cellClass: params =>
|
||||||
!params.node?.group && !params.node?.rowPinned && params.data?.hasArea
|
!params.node?.group && !params.node?.rowPinned && params.data?.hasArea
|
||||||
? 'ag-right-aligned-cell editable-cell-line'
|
? 'editable-cell-line'
|
||||||
: 'ag-right-aligned-cell',
|
: '',
|
||||||
cellClassRules: {
|
cellClassRules: {
|
||||||
|
'ag-right-aligned-cell': () => true,
|
||||||
'editable-cell-empty': params =>
|
'editable-cell-empty': params =>
|
||||||
!params.node?.group && !params.node?.rowPinned && Boolean(params.data?.hasArea) && (params.value == null || params.value === '')
|
!params.node?.group && !params.node?.rowPinned && Boolean(params.data?.hasArea) && (params.value == null || params.value === '')
|
||||||
},
|
},
|
||||||
@ -662,12 +744,17 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
|
|||||||
headerClass: 'ag-right-aligned-header',
|
headerClass: 'ag-right-aligned-header',
|
||||||
minWidth: 130,
|
minWidth: 130,
|
||||||
flex: 1,
|
flex: 1,
|
||||||
cellClass: 'ag-right-aligned-cell',
|
cellClassRules: {
|
||||||
|
'ag-right-aligned-cell': () => true
|
||||||
|
},
|
||||||
valueGetter: params =>
|
valueGetter: params =>
|
||||||
params.node?.rowPinned
|
params.node?.rowPinned
|
||||||
? null
|
? null
|
||||||
: getCheckedBenchmarkBudgetSplitByLandArea(params.data)?.basic ?? null,
|
: getCheckedBenchmarkBudgetSplitByLandArea(params.data)?.basic ?? null,
|
||||||
cellRenderer: createBudgetCellRendererWithCheck('benchmarkBudgetBasicChecked'),
|
cellRenderer: createBudgetCellRendererWithCheck('benchmarkBudgetBasicChecked'),
|
||||||
|
cellRendererParams: {
|
||||||
|
suppressMouseEventHandling: () => true
|
||||||
|
},
|
||||||
valueFormatter: formatReadonlyMoney
|
valueFormatter: formatReadonlyMoney
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -677,12 +764,17 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
|
|||||||
headerClass: 'ag-right-aligned-header',
|
headerClass: 'ag-right-aligned-header',
|
||||||
minWidth: 130,
|
minWidth: 130,
|
||||||
flex: 1,
|
flex: 1,
|
||||||
cellClass: 'ag-right-aligned-cell',
|
cellClassRules: {
|
||||||
|
'ag-right-aligned-cell': () => true
|
||||||
|
},
|
||||||
valueGetter: params =>
|
valueGetter: params =>
|
||||||
params.node?.rowPinned
|
params.node?.rowPinned
|
||||||
? null
|
? null
|
||||||
: getCheckedBenchmarkBudgetSplitByLandArea(params.data)?.optional ?? null,
|
: getCheckedBenchmarkBudgetSplitByLandArea(params.data)?.optional ?? null,
|
||||||
cellRenderer: createBudgetCellRendererWithCheck('benchmarkBudgetOptionalChecked'),
|
cellRenderer: createBudgetCellRendererWithCheck('benchmarkBudgetOptionalChecked'),
|
||||||
|
cellRendererParams: {
|
||||||
|
suppressMouseEventHandling: () => true
|
||||||
|
},
|
||||||
valueFormatter: formatReadonlyMoney
|
valueFormatter: formatReadonlyMoney
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -692,7 +784,9 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
|
|||||||
headerClass: 'ag-right-aligned-header',
|
headerClass: 'ag-right-aligned-header',
|
||||||
minWidth: 100,
|
minWidth: 100,
|
||||||
flex: 1,
|
flex: 1,
|
||||||
cellClass: 'ag-right-aligned-cell',
|
cellClassRules: {
|
||||||
|
'ag-right-aligned-cell': () => true
|
||||||
|
},
|
||||||
valueGetter: params =>
|
valueGetter: params =>
|
||||||
params.node?.rowPinned
|
params.node?.rowPinned
|
||||||
? null
|
? null
|
||||||
@ -709,15 +803,22 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
|
|||||||
headerName: '咨询分类系数',
|
headerName: '咨询分类系数',
|
||||||
field: 'consultCategoryFactor',
|
field: 'consultCategoryFactor',
|
||||||
colId: 'consultCategoryFactor',
|
colId: 'consultCategoryFactor',
|
||||||
|
headerTooltip: '点击右侧↻恢复本列默认咨询分类系数',
|
||||||
|
headerComponent: AgGridResetHeader,
|
||||||
|
headerComponentParams: {
|
||||||
|
onReset: restoreConsultCategoryFactorColumnDefaults,
|
||||||
|
resetTitle: '恢复本列默认咨询分类系数'
|
||||||
|
},
|
||||||
headerClass: 'ag-right-aligned-header',
|
headerClass: 'ag-right-aligned-header',
|
||||||
minWidth: 80,
|
minWidth: 80,
|
||||||
flex: 1,
|
flex: 1,
|
||||||
editable: params => !params.node?.group && !params.node?.rowPinned,
|
editable: params => !params.node?.group && !params.node?.rowPinned,
|
||||||
cellClass: params =>
|
cellClass: params =>
|
||||||
!params.node?.group && !params.node?.rowPinned
|
!params.node?.group && !params.node?.rowPinned
|
||||||
? 'ag-right-aligned-cell editable-cell-line'
|
? 'editable-cell-line'
|
||||||
: 'ag-right-aligned-cell',
|
: '',
|
||||||
cellClassRules: {
|
cellClassRules: {
|
||||||
|
'ag-right-aligned-cell': () => true,
|
||||||
'editable-cell-empty': params =>
|
'editable-cell-empty': params =>
|
||||||
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
|
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
|
||||||
},
|
},
|
||||||
@ -728,15 +829,22 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
|
|||||||
headerName: '专业系数',
|
headerName: '专业系数',
|
||||||
field: 'majorFactor',
|
field: 'majorFactor',
|
||||||
colId: 'majorFactor',
|
colId: 'majorFactor',
|
||||||
|
headerTooltip: '点击右侧↻恢复本列默认专业系数',
|
||||||
|
headerComponent: AgGridResetHeader,
|
||||||
|
headerComponentParams: {
|
||||||
|
onReset: restoreMajorFactorColumnDefaults,
|
||||||
|
resetTitle: '恢复本列默认专业系数'
|
||||||
|
},
|
||||||
headerClass: 'ag-right-aligned-header',
|
headerClass: 'ag-right-aligned-header',
|
||||||
minWidth: 80,
|
minWidth: 80,
|
||||||
flex: 1,
|
flex: 1,
|
||||||
editable: params => !params.node?.group && !params.node?.rowPinned,
|
editable: params => !params.node?.group && !params.node?.rowPinned,
|
||||||
cellClass: params =>
|
cellClass: params =>
|
||||||
!params.node?.group && !params.node?.rowPinned
|
!params.node?.group && !params.node?.rowPinned
|
||||||
? 'ag-right-aligned-cell editable-cell-line'
|
? 'editable-cell-line'
|
||||||
: 'ag-right-aligned-cell',
|
: '',
|
||||||
cellClassRules: {
|
cellClassRules: {
|
||||||
|
'ag-right-aligned-cell': () => true,
|
||||||
'editable-cell-empty': params =>
|
'editable-cell-empty': params =>
|
||||||
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
|
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
|
||||||
},
|
},
|
||||||
@ -753,9 +861,10 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
|
|||||||
editable: params => !params.node?.group && !params.node?.rowPinned,
|
editable: params => !params.node?.group && !params.node?.rowPinned,
|
||||||
cellClass: params =>
|
cellClass: params =>
|
||||||
!params.node?.group && !params.node?.rowPinned
|
!params.node?.group && !params.node?.rowPinned
|
||||||
? 'ag-right-aligned-cell editable-cell-line'
|
? 'editable-cell-line'
|
||||||
: 'ag-right-aligned-cell',
|
: '',
|
||||||
cellClassRules: {
|
cellClassRules: {
|
||||||
|
'ag-right-aligned-cell': () => true,
|
||||||
'editable-cell-empty': params =>
|
'editable-cell-empty': params =>
|
||||||
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
|
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
|
||||||
},
|
},
|
||||||
@ -772,9 +881,10 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
|
|||||||
editable: params => !params.node?.group && !params.node?.rowPinned,
|
editable: params => !params.node?.group && !params.node?.rowPinned,
|
||||||
cellClass: params =>
|
cellClass: params =>
|
||||||
!params.node?.group && !params.node?.rowPinned
|
!params.node?.group && !params.node?.rowPinned
|
||||||
? 'ag-right-aligned-cell editable-cell-line'
|
? 'editable-cell-line'
|
||||||
: 'ag-right-aligned-cell',
|
: '',
|
||||||
cellClassRules: {
|
cellClassRules: {
|
||||||
|
'ag-right-aligned-cell': () => true,
|
||||||
'editable-cell-empty': params =>
|
'editable-cell-empty': params =>
|
||||||
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
|
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
|
||||||
},
|
},
|
||||||
@ -788,8 +898,10 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
|
|||||||
headerClass: 'ag-right-aligned-header',
|
headerClass: 'ag-right-aligned-header',
|
||||||
minWidth: 120,
|
minWidth: 120,
|
||||||
flex: 1,
|
flex: 1,
|
||||||
cellClass: 'ag-right-aligned-cell',
|
|
||||||
aggFunc: decimalAggSum,
|
aggFunc: decimalAggSum,
|
||||||
|
cellClassRules: {
|
||||||
|
'ag-right-aligned-cell': () => true
|
||||||
|
},
|
||||||
valueGetter: params => (params.node?.rowPinned ? params.data?.budgetFee ?? null : getBudgetFee(params.data)),
|
valueGetter: params => (params.node?.rowPinned ? params.data?.budgetFee ?? null : getBudgetFee(params.data)),
|
||||||
valueFormatter: formatReadonlyMoney
|
valueFormatter: formatReadonlyMoney
|
||||||
}
|
}
|
||||||
@ -864,7 +976,9 @@ const totalBenchmarkBudget = computed(() => sumByNumber(detailRows.value, row =>
|
|||||||
const sumNullableBy = <T>(rows: T[], pick: (row: T) => unknown): number | null => {
|
const sumNullableBy = <T>(rows: T[], pick: (row: T) => unknown): number | null => {
|
||||||
let hasValid = false
|
let hasValid = false
|
||||||
const total = sumByNumber(rows, row => {
|
const total = sumByNumber(rows, row => {
|
||||||
const value = Number(pick(row))
|
const raw = pick(row)
|
||||||
|
if (raw == null || raw === '') return null
|
||||||
|
const value = Number(raw)
|
||||||
if (!Number.isFinite(value)) return null
|
if (!Number.isFinite(value)) return null
|
||||||
hasValid = true
|
hasValid = true
|
||||||
return value
|
return value
|
||||||
@ -874,9 +988,7 @@ const sumNullableBy = <T>(rows: T[], pick: (row: T) => unknown): number | null =
|
|||||||
|
|
||||||
const totalBudgetFeeBasic = computed(() => sumNullableBy(detailRows.value, row => getBudgetFeeSplit(row)?.basic))
|
const totalBudgetFeeBasic = computed(() => sumNullableBy(detailRows.value, row => getBudgetFeeSplit(row)?.basic))
|
||||||
const totalBudgetFeeOptional = computed(() => sumNullableBy(detailRows.value, row => getBudgetFeeSplit(row)?.optional))
|
const totalBudgetFeeOptional = computed(() => sumNullableBy(detailRows.value, row => getBudgetFeeSplit(row)?.optional))
|
||||||
const totalBudgetFee = computed(() =>
|
const totalBudgetFee = computed(() => sumNullableBy(detailRows.value, row => getBudgetFee(row)))
|
||||||
sumNullableBy(detailRows.value.filter(e => e.budgetFee !== null && e.budgetFee !== undefined), row => getBudgetFee(row))
|
|
||||||
)
|
|
||||||
const pinnedTopRowData = computed(() => [
|
const pinnedTopRowData = computed(() => [
|
||||||
{
|
{
|
||||||
id: 'pinned-total-row',
|
id: 'pinned-total-row',
|
||||||
@ -909,6 +1021,7 @@ const pinnedTopRowData = computed(() => [
|
|||||||
|
|
||||||
const syncComputedValuesToDetailRows = () => {
|
const syncComputedValuesToDetailRows = () => {
|
||||||
for (const row of detailRows.value) {
|
for (const row of detailRows.value) {
|
||||||
|
|
||||||
const benchmarkBudgetRawSplit = getBenchmarkBudgetSplitByLandArea(row)
|
const benchmarkBudgetRawSplit = getBenchmarkBudgetSplitByLandArea(row)
|
||||||
const benchmarkBudgetSplit = getCheckedBenchmarkBudgetSplitByLandArea(row)
|
const benchmarkBudgetSplit = getCheckedBenchmarkBudgetSplitByLandArea(row)
|
||||||
const budgetFeeSplit = benchmarkBudgetSplit
|
const budgetFeeSplit = benchmarkBudgetSplit
|
||||||
@ -935,14 +1048,14 @@ const syncComputedValuesToDetailRows = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const buildPersistDetailRows = () => {
|
const buildPersistDetailRows = () => detailRows.value.map(row => ({ ...row }))
|
||||||
syncComputedValuesToDetailRows()
|
|
||||||
return detailRows.value.map(row => ({ ...row }))
|
|
||||||
}
|
|
||||||
|
|
||||||
const saveToIndexedDB = async () => {
|
const saveToIndexedDB = async (options?: { skipComputedSync?: boolean }) => {
|
||||||
if (shouldSkipPersist()) return
|
if (shouldSkipPersist()) return
|
||||||
try {
|
try {
|
||||||
|
if (!options?.skipComputedSync) {
|
||||||
|
syncComputedValuesToDetailRows()
|
||||||
|
}
|
||||||
const payload = {
|
const payload = {
|
||||||
detailRows: JSON.parse(JSON.stringify(buildPersistDetailRows())),
|
detailRows: JSON.parse(JSON.stringify(buildPersistDetailRows())),
|
||||||
projectCount: getTargetProjectCount()
|
projectCount: getTargetProjectCount()
|
||||||
@ -966,6 +1079,96 @@ const saveToIndexedDB = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isSameNullableNumber = (left: number | null | undefined, right: number | null | undefined) => {
|
||||||
|
if (left == null && right == null) return true
|
||||||
|
if (left == null || right == null) return false
|
||||||
|
return roundTo(left, 6) === roundTo(right, 6)
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildContractScaleMap = (rows: SourceRow[] | undefined) => {
|
||||||
|
const map = new Map<string, SourceRow>()
|
||||||
|
for (const row of rows || []) {
|
||||||
|
const majorDictId = resolveRowMajorDictId(row)
|
||||||
|
if (!majorDictId) continue
|
||||||
|
const projectIndex = resolveRowProjectIndex(row)
|
||||||
|
map.set(makeProjectMajorKey(projectIndex, majorDictId), row)
|
||||||
|
}
|
||||||
|
return map
|
||||||
|
}
|
||||||
|
|
||||||
|
const getContractScaleRowByMajor = (row: DetailRow, map: Map<string, SourceRow>) => {
|
||||||
|
const majorDictId = resolveRowMajorDictId(row)
|
||||||
|
if (!majorDictId) return undefined
|
||||||
|
const projectIndex = resolveRowProjectIndex(row)
|
||||||
|
return map.get(makeProjectMajorKey(projectIndex, majorDictId))
|
||||||
|
|| (projectIndex > 1 ? map.get(makeProjectMajorKey(1, majorDictId)) : undefined)
|
||||||
|
}
|
||||||
|
|
||||||
|
const syncLinkedFieldsFromContractAndFactors = async () => {
|
||||||
|
if (detailRows.value.length === 0) return
|
||||||
|
await loadFactorDefaults()
|
||||||
|
const consultFactor = getDefaultConsultCategoryFactor()
|
||||||
|
|
||||||
|
let changed = false
|
||||||
|
detailRows.value = detailRows.value.map(row => {
|
||||||
|
const majorDictId = resolveRowMajorDictId(row)
|
||||||
|
const nextConsultFactor = consultFactor
|
||||||
|
const nextMajorFactor = getDefaultMajorFactorById(majorDictId)
|
||||||
|
if (
|
||||||
|
isSameNullableNumber(row.consultCategoryFactor, nextConsultFactor)
|
||||||
|
&& isSameNullableNumber(row.majorFactor, nextMajorFactor)
|
||||||
|
) {
|
||||||
|
return row
|
||||||
|
}
|
||||||
|
changed = true
|
||||||
|
return {
|
||||||
|
...row,
|
||||||
|
consultCategoryFactor: nextConsultFactor,
|
||||||
|
majorFactor: nextMajorFactor
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (!changed) return
|
||||||
|
syncComputedValuesToDetailRows()
|
||||||
|
await saveToIndexedDB({ skipComputedSync: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
const syncLinkedScaleValuesFromContract = async () => {
|
||||||
|
if (detailRows.value.length === 0) return
|
||||||
|
const htData = await kvStore.getItem<{ detailRows: SourceRow[] }>(HT_DB_KEY.value)
|
||||||
|
const sourceRows = Array.isArray(htData?.detailRows) ? htData.detailRows : []
|
||||||
|
const sourceRowMap = buildContractScaleMap(sourceRows)
|
||||||
|
|
||||||
|
let changed = false
|
||||||
|
detailRows.value = detailRows.value.map(row => {
|
||||||
|
const sourceRow = getContractScaleRowByMajor(row, sourceRowMap)
|
||||||
|
const nextLandArea = row.hasArea ? (typeof sourceRow?.landArea === 'number' ? sourceRow.landArea : null) : null
|
||||||
|
if (isSameNullableNumber(row.landArea, nextLandArea)) return row
|
||||||
|
changed = true
|
||||||
|
return {
|
||||||
|
...row,
|
||||||
|
landArea: nextLandArea
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (!changed) return
|
||||||
|
syncComputedValuesToDetailRows()
|
||||||
|
await saveToIndexedDB({ skipComputedSync: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
const linkedSourceSignature = computed(() => JSON.stringify({
|
||||||
|
consultFactor:
|
||||||
|
zxFwPricingStore.keyedStates[HT_CONSULT_FACTOR_KEY.value]
|
||||||
|
?? kvStore.entries[HT_CONSULT_FACTOR_KEY.value]
|
||||||
|
?? null,
|
||||||
|
majorFactor:
|
||||||
|
zxFwPricingStore.keyedStates[HT_MAJOR_FACTOR_KEY.value]
|
||||||
|
?? kvStore.entries[HT_MAJOR_FACTOR_KEY.value]
|
||||||
|
?? null
|
||||||
|
}))
|
||||||
|
|
||||||
|
const linkedContractScaleSignature = computed(() => JSON.stringify(
|
||||||
|
kvStore.entries[HT_DB_KEY.value] ?? null
|
||||||
|
))
|
||||||
|
|
||||||
const getProjectMajorKeyFromRow = (row: Partial<DetailRow> | undefined) => {
|
const getProjectMajorKeyFromRow = (row: Partial<DetailRow> | undefined) => {
|
||||||
if (!row) return ''
|
if (!row) return ''
|
||||||
const majorDictId = resolveRowMajorDictId(row)
|
const majorDictId = resolveRowMajorDictId(row)
|
||||||
@ -1004,7 +1207,7 @@ const applyProjectCountChange = async (nextValue: unknown) => {
|
|||||||
if (normalized < previousProjectCount) {
|
if (normalized < previousProjectCount) {
|
||||||
detailRows.value = mergeWithDictRows(previousRows as any, { projectCount: normalized })
|
detailRows.value = mergeWithDictRows(previousRows as any, { projectCount: normalized })
|
||||||
syncComputedValuesToDetailRows()
|
syncComputedValuesToDetailRows()
|
||||||
await saveToIndexedDB()
|
await saveToIndexedDB({ skipComputedSync: true })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1037,7 +1240,7 @@ const applyProjectCountChange = async (nextValue: unknown) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
syncComputedValuesToDetailRows()
|
syncComputedValuesToDetailRows()
|
||||||
await saveToIndexedDB()
|
await saveToIndexedDB({ skipComputedSync: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadFromIndexedDB = async () => {
|
const loadFromIndexedDB = async () => {
|
||||||
@ -1131,7 +1334,7 @@ const clearAllData = async () => {
|
|||||||
|
|
||||||
const handleCellValueChanged = () => {
|
const handleCellValueChanged = () => {
|
||||||
syncComputedValuesToDetailRows()
|
syncComputedValuesToDetailRows()
|
||||||
void saveToIndexedDB()
|
void saveToIndexedDB({ skipComputedSync: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleGridReady = (event: GridReadyEvent<DetailRow>) => {
|
const handleGridReady = (event: GridReadyEvent<DetailRow>) => {
|
||||||
@ -1140,10 +1343,12 @@ const handleGridReady = (event: GridReadyEvent<DetailRow>) => {
|
|||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await loadFromIndexedDB()
|
await loadFromIndexedDB()
|
||||||
|
await syncLinkedFieldsFromContractAndFactors()
|
||||||
})
|
})
|
||||||
|
|
||||||
onActivated(() => {
|
onActivated(async () => {
|
||||||
void loadFromIndexedDB()
|
await loadFromIndexedDB()
|
||||||
|
await syncLinkedFieldsFromContractAndFactors()
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
@ -1151,6 +1356,14 @@ onBeforeUnmount(() => {
|
|||||||
gridApi.value = null
|
gridApi.value = null
|
||||||
void saveToIndexedDB()
|
void saveToIndexedDB()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
watch(linkedSourceSignature, () => {
|
||||||
|
void syncLinkedFieldsFromContractAndFactors()
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(linkedContractScaleSignature, () => {
|
||||||
|
void syncLinkedScaleValuesFromContract()
|
||||||
|
})
|
||||||
const processCellForClipboard = (params: any) => {
|
const processCellForClipboard = (params: any) => {
|
||||||
if (Array.isArray(params.value)) {
|
if (Array.isArray(params.value)) {
|
||||||
return JSON.stringify(params.value); // 数组转字符串复制
|
return JSON.stringify(params.value); // 数组转字符串复制
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
import { computed, onActivated, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||||
import { AgGridVue } from 'ag-grid-vue3'
|
import { AgGridVue } from 'ag-grid-vue3'
|
||||||
import type { ColDef, GridApi, GridReadyEvent } from 'ag-grid-community'
|
import type { ColDef, GridApi, GridReadyEvent } from 'ag-grid-community'
|
||||||
import { taskList } from '@/sql'
|
import { taskList } from '@/sql'
|
||||||
@ -9,6 +9,7 @@ import { formatThousands, formatThousandsFlexible } from '@/lib/numberFormat'
|
|||||||
import { parseNumberOrNull } from '@/lib/number'
|
import { parseNumberOrNull } from '@/lib/number'
|
||||||
import { syncPricingTotalToZxFw } from '@/lib/zxFwPricingSync'
|
import { syncPricingTotalToZxFw } from '@/lib/zxFwPricingSync'
|
||||||
import { useZxFwPricingStore } from '@/pinia/zxFwPricing'
|
import { useZxFwPricingStore } from '@/pinia/zxFwPricing'
|
||||||
|
import { useKvStore } from '@/pinia/kv'
|
||||||
import { loadConsultCategoryFactorMap } from '@/lib/xmFactorDefaults'
|
import { loadConsultCategoryFactorMap } from '@/lib/xmFactorDefaults'
|
||||||
import MethodUnavailableNotice from '@/components/shared/MethodUnavailableNotice.vue'
|
import MethodUnavailableNotice from '@/components/shared/MethodUnavailableNotice.vue'
|
||||||
|
|
||||||
@ -42,6 +43,7 @@ const props = defineProps<{
|
|||||||
serviceId: string | number
|
serviceId: string | number
|
||||||
}>()
|
}>()
|
||||||
const zxFwPricingStore = useZxFwPricingStore()
|
const zxFwPricingStore = useZxFwPricingStore()
|
||||||
|
const kvStore = useKvStore()
|
||||||
const DB_KEY = computed(() => `gzlF-${props.contractId}-${props.serviceId}`)
|
const DB_KEY = computed(() => `gzlF-${props.contractId}-${props.serviceId}`)
|
||||||
const HT_CONSULT_FACTOR_KEY = computed(() => `ht-consult-category-factor-v1-${props.contractId}`)
|
const HT_CONSULT_FACTOR_KEY = computed(() => `ht-consult-category-factor-v1-${props.contractId}`)
|
||||||
const PRICING_CLEAR_SKIP_PREFIX = 'pricing-clear-skip:'
|
const PRICING_CLEAR_SKIP_PREFIX = 'pricing-clear-skip:'
|
||||||
@ -193,7 +195,7 @@ const mergeWithDictRows = (rowsFromDb: DetailRow[] | undefined): DetailRow[] =>
|
|||||||
budgetAdoptedUnitPrice:
|
budgetAdoptedUnitPrice:
|
||||||
typeof fromDb.budgetAdoptedUnitPrice === 'number' ? fromDb.budgetAdoptedUnitPrice : null,
|
typeof fromDb.budgetAdoptedUnitPrice === 'number' ? fromDb.budgetAdoptedUnitPrice : null,
|
||||||
consultCategoryFactor:
|
consultCategoryFactor:
|
||||||
typeof fromDb.consultCategoryFactor === 'number' ? fromDb.consultCategoryFactor : null,
|
typeof fromDb.consultCategoryFactor === 'number' ? fromDb.consultCategoryFactor : getDefaultConsultCategoryFactor(),
|
||||||
serviceFee: typeof fromDb.serviceFee === 'number' ? fromDb.serviceFee : null,
|
serviceFee: typeof fromDb.serviceFee === 'number' ? fromDb.serviceFee : null,
|
||||||
remark: typeof fromDb.remark === 'string' ? fromDb.remark : hasRemark ? '' : row.remark
|
remark: typeof fromDb.remark === 'string' ? fromDb.remark : hasRemark ? '' : row.remark
|
||||||
}
|
}
|
||||||
@ -304,8 +306,9 @@ const columnDefs: ColDef<DetailRow>[] = [
|
|||||||
minWidth: 170,
|
minWidth: 170,
|
||||||
flex: 1,
|
flex: 1,
|
||||||
editable: params => !params.node?.group && !params.node?.rowPinned && !isNoTaskRow(params.data),
|
editable: params => !params.node?.group && !params.node?.rowPinned && !isNoTaskRow(params.data),
|
||||||
cellClass: params => (!params.node?.group && !params.node?.rowPinned ? 'ag-right-aligned-cell editable-cell-line' : 'ag-right-aligned-cell'),
|
cellClass: params => (!params.node?.group && !params.node?.rowPinned ? 'editable-cell-line' : ''),
|
||||||
cellClassRules: {
|
cellClassRules: {
|
||||||
|
'ag-right-aligned-cell': () => true,
|
||||||
'editable-cell-empty': params =>
|
'editable-cell-empty': params =>
|
||||||
!params.node?.group &&
|
!params.node?.group &&
|
||||||
!params.node?.rowPinned &&
|
!params.node?.rowPinned &&
|
||||||
@ -365,8 +368,10 @@ const columnDefs: ColDef<DetailRow>[] = [
|
|||||||
headerClass: 'ag-right-aligned-header',
|
headerClass: 'ag-right-aligned-header',
|
||||||
minWidth: 150,
|
minWidth: 150,
|
||||||
flex: 1,
|
flex: 1,
|
||||||
cellClass: 'ag-right-aligned-cell',
|
|
||||||
editable: false,
|
editable: false,
|
||||||
|
cellClassRules: {
|
||||||
|
'ag-right-aligned-cell': () => true
|
||||||
|
},
|
||||||
valueGetter: params => (params.node?.rowPinned ? params.data?.serviceFee ?? null : calcServiceFee(params.data)),
|
valueGetter: params => (params.node?.rowPinned ? params.data?.serviceFee ?? null : calcServiceFee(params.data)),
|
||||||
aggFunc: decimalAggSum,
|
aggFunc: decimalAggSum,
|
||||||
valueFormatter: params => {
|
valueFormatter: params => {
|
||||||
@ -414,7 +419,7 @@ const sumNullableBy = <T>(rows: T[], pick: (row: T) => unknown): number | null =
|
|||||||
return hasValid ? total : null
|
return hasValid ? total : null
|
||||||
}
|
}
|
||||||
|
|
||||||
const totalServiceFee = computed(() => sumNullableBy(detailRows.value.filter(e => e.basicFee !== null && e.basicFee !== undefined), row => calcServiceFee(row)))
|
const totalServiceFee = computed(() => sumNullableBy(detailRows.value, row => calcServiceFee(row)))
|
||||||
const pinnedTopRowData = computed(() => [
|
const pinnedTopRowData = computed(() => [
|
||||||
{
|
{
|
||||||
id: 'pinned-total-row',
|
id: 'pinned-total-row',
|
||||||
@ -468,6 +473,37 @@ const saveToIndexedDB = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isSameNullableNumber = (left: number | null | undefined, right: number | null | undefined) => {
|
||||||
|
if (left == null && right == null) return true
|
||||||
|
if (left == null || right == null) return false
|
||||||
|
return roundTo(left, 6) === roundTo(right, 6)
|
||||||
|
}
|
||||||
|
|
||||||
|
const syncLinkedConsultFactorFromHt = async () => {
|
||||||
|
if (!isWorkloadMethodApplicable.value || detailRows.value.length === 0) return
|
||||||
|
consultCategoryFactorMap.value = await loadConsultCategoryFactorMap(HT_CONSULT_FACTOR_KEY.value)
|
||||||
|
factorDefaultsLoaded = true
|
||||||
|
const nextDefaultFactor = getDefaultConsultCategoryFactor()
|
||||||
|
let changed = false
|
||||||
|
detailRows.value = detailRows.value.map(row => {
|
||||||
|
if (isSameNullableNumber(row.consultCategoryFactor, nextDefaultFactor)) return row
|
||||||
|
changed = true
|
||||||
|
return {
|
||||||
|
...row,
|
||||||
|
consultCategoryFactor: nextDefaultFactor
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (!changed) return
|
||||||
|
await saveToIndexedDB()
|
||||||
|
}
|
||||||
|
|
||||||
|
const linkedConsultFactorSignature = computed(() => JSON.stringify({
|
||||||
|
consultFactor:
|
||||||
|
zxFwPricingStore.keyedStates[HT_CONSULT_FACTOR_KEY.value]
|
||||||
|
?? kvStore.entries[HT_CONSULT_FACTOR_KEY.value]
|
||||||
|
?? null
|
||||||
|
}))
|
||||||
|
|
||||||
const loadFromIndexedDB = async () => {
|
const loadFromIndexedDB = async () => {
|
||||||
try {
|
try {
|
||||||
if (!isWorkloadMethodApplicable.value) {
|
if (!isWorkloadMethodApplicable.value) {
|
||||||
@ -505,6 +541,12 @@ const handleGridReady = (event: GridReadyEvent<DetailRow>) => {
|
|||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await loadFromIndexedDB()
|
await loadFromIndexedDB()
|
||||||
|
await syncLinkedConsultFactorFromHt()
|
||||||
|
})
|
||||||
|
|
||||||
|
onActivated(async () => {
|
||||||
|
await loadFromIndexedDB()
|
||||||
|
await syncLinkedConsultFactorFromHt()
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
@ -512,6 +554,10 @@ onBeforeUnmount(() => {
|
|||||||
gridApi.value = null
|
gridApi.value = null
|
||||||
void saveToIndexedDB()
|
void saveToIndexedDB()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
watch(linkedConsultFactorSignature, () => {
|
||||||
|
void syncLinkedConsultFactorFromHt()
|
||||||
|
})
|
||||||
const processCellForClipboard = (params: any) => {
|
const processCellForClipboard = (params: any) => {
|
||||||
if (Array.isArray(params.value)) {
|
if (Array.isArray(params.value)) {
|
||||||
return JSON.stringify(params.value); // 数组转字符串复制
|
return JSON.stringify(params.value); // 数组转字符串复制
|
||||||
|
|||||||
@ -304,6 +304,7 @@ const editableNumberCol = <K extends keyof DetailRow>(
|
|||||||
editable: params => !params.node?.group && !params.node?.rowPinned,
|
editable: params => !params.node?.group && !params.node?.rowPinned,
|
||||||
cellClass: params => (!params.node?.group && !params.node?.rowPinned ? 'editable-cell-line' : ''),
|
cellClass: params => (!params.node?.group && !params.node?.rowPinned ? 'editable-cell-line' : ''),
|
||||||
cellClassRules: {
|
cellClassRules: {
|
||||||
|
'ag-right-aligned-cell':()=>true,
|
||||||
'editable-cell-empty': params =>
|
'editable-cell-empty': params =>
|
||||||
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
|
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
|
||||||
},
|
},
|
||||||
@ -325,9 +326,10 @@ const editableMoneyCol = <K extends keyof DetailRow>(
|
|||||||
editable: params => !params.node?.group && !params.node?.rowPinned,
|
editable: params => !params.node?.group && !params.node?.rowPinned,
|
||||||
cellClass: params =>
|
cellClass: params =>
|
||||||
!params.node?.group && !params.node?.rowPinned
|
!params.node?.group && !params.node?.rowPinned
|
||||||
? 'ag-right-aligned-cell editable-cell-line'
|
? 'editable-cell-line'
|
||||||
: 'ag-right-aligned-cell',
|
: '',
|
||||||
cellClassRules: {
|
cellClassRules: {
|
||||||
|
'ag-right-aligned-cell': () => true,
|
||||||
'editable-cell-empty': params =>
|
'editable-cell-empty': params =>
|
||||||
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
|
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
|
||||||
},
|
},
|
||||||
@ -403,8 +405,10 @@ const columnDefs: (ColDef<DetailRow> | ColGroupDef<DetailRow>)[] = [
|
|||||||
headerClass: 'ag-right-aligned-header',
|
headerClass: 'ag-right-aligned-header',
|
||||||
minWidth: 120,
|
minWidth: 120,
|
||||||
flex: 1,
|
flex: 1,
|
||||||
cellClass: 'ag-right-aligned-cell',
|
|
||||||
editable: false,
|
editable: false,
|
||||||
|
cellClassRules: {
|
||||||
|
'ag-right-aligned-cell': () => true
|
||||||
|
},
|
||||||
aggFunc: decimalAggSum,
|
aggFunc: decimalAggSum,
|
||||||
valueGetter: params => (params.node?.rowPinned ? params.data?.serviceBudget ?? null : calcServiceBudget(params.data)),
|
valueGetter: params => (params.node?.rowPinned ? params.data?.serviceBudget ?? null : calcServiceBudget(params.data)),
|
||||||
valueFormatter: params => {
|
valueFormatter: params => {
|
||||||
@ -446,7 +450,7 @@ const sumNullableBy = <T>(rows: T[], pick: (row: T) => unknown): number | null =
|
|||||||
})
|
})
|
||||||
return hasValid ? total : null
|
return hasValid ? total : null
|
||||||
}
|
}
|
||||||
const totalServiceBudget = computed(() => sumNullableBy(detailRows.value.filter(e => e.serviceBudget !== null && e.serviceBudget !== undefined), row => calcServiceBudget(row)))
|
const totalServiceBudget = computed(() => sumNullableBy(detailRows.value, row => calcServiceBudget(row)))
|
||||||
const pinnedTopRowData = computed(() => [
|
const pinnedTopRowData = computed(() => [
|
||||||
{
|
{
|
||||||
id: 'pinned-total-row',
|
id: 'pinned-total-row',
|
||||||
|
|||||||
@ -309,11 +309,12 @@ const columnDefs: ColDef<FeeRow>[] = [
|
|||||||
minWidth: 100,
|
minWidth: 100,
|
||||||
flex: 1,
|
flex: 1,
|
||||||
headerClass: 'ag-right-aligned-header',
|
headerClass: 'ag-right-aligned-header',
|
||||||
cellClass: 'ag-right-aligned-cell editable-cell-line',
|
cellClass: 'editable-cell-line',
|
||||||
editable: params => !isSubtotalRow(params.data),
|
editable: params => !isSubtotalRow(params.data),
|
||||||
valueParser: params => parseNumberOrNull(params.newValue, { precision: 3 }),
|
valueParser: params => parseNumberOrNull(params.newValue, { precision: 3 }),
|
||||||
valueFormatter: formatEditableQuantity,
|
valueFormatter: formatEditableQuantity,
|
||||||
cellClassRules: {
|
cellClassRules: {
|
||||||
|
'ag-right-aligned-cell': () => true,
|
||||||
'editable-cell-empty': params => params.value == null || params.value === ''
|
'editable-cell-empty': params => params.value == null || params.value === ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -323,11 +324,12 @@ const columnDefs: ColDef<FeeRow>[] = [
|
|||||||
minWidth: 120,
|
minWidth: 120,
|
||||||
flex: 1.1,
|
flex: 1.1,
|
||||||
headerClass: 'ag-right-aligned-header',
|
headerClass: 'ag-right-aligned-header',
|
||||||
cellClass: 'ag-right-aligned-cell editable-cell-line',
|
cellClass: 'editable-cell-line',
|
||||||
editable: params => !isSubtotalRow(params.data),
|
editable: params => !isSubtotalRow(params.data),
|
||||||
valueParser: params => parseNumberOrNull(params.newValue, { precision: 2 }),
|
valueParser: params => parseNumberOrNull(params.newValue, { precision: 2 }),
|
||||||
valueFormatter: formatEditableUnitPrice,
|
valueFormatter: formatEditableUnitPrice,
|
||||||
cellClassRules: {
|
cellClassRules: {
|
||||||
|
'ag-right-aligned-cell': () => true,
|
||||||
'editable-cell-empty': params => params.value == null || params.value === ''
|
'editable-cell-empty': params => params.value == null || params.value === ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -337,8 +339,10 @@ const columnDefs: ColDef<FeeRow>[] = [
|
|||||||
minWidth: 130,
|
minWidth: 130,
|
||||||
flex: 1.2,
|
flex: 1.2,
|
||||||
headerClass: 'ag-right-aligned-header',
|
headerClass: 'ag-right-aligned-header',
|
||||||
cellClass: 'ag-right-aligned-cell',
|
|
||||||
editable: false,
|
editable: false,
|
||||||
|
cellClassRules: {
|
||||||
|
'ag-right-aligned-cell': () => true
|
||||||
|
},
|
||||||
valueFormatter: formatReadonlyBudgetFee
|
valueFormatter: formatReadonlyBudgetFee
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@ -510,10 +510,10 @@ const columnDefs: ColDef<FeeMethodRow>[] = [
|
|||||||
flex: 1.2,
|
flex: 1.2,
|
||||||
editable: false,
|
editable: false,
|
||||||
headerClass: 'ag-right-aligned-header',
|
headerClass: 'ag-right-aligned-header',
|
||||||
cellClass: 'ag-right-aligned-cell',
|
|
||||||
valueParser: params => numericParser(params.newValue),
|
valueParser: params => numericParser(params.newValue),
|
||||||
valueFormatter: formatEditableNumber,
|
valueFormatter: formatEditableNumber,
|
||||||
cellClassRules: {
|
cellClassRules: {
|
||||||
|
'ag-right-aligned-cell': () => true,
|
||||||
'editable-cell-empty': params => params.value == null || params.value === ''
|
'editable-cell-empty': params => params.value == null || params.value === ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -524,10 +524,10 @@ const columnDefs: ColDef<FeeMethodRow>[] = [
|
|||||||
flex: 1.2,
|
flex: 1.2,
|
||||||
editable: false,
|
editable: false,
|
||||||
headerClass: 'ag-right-aligned-header',
|
headerClass: 'ag-right-aligned-header',
|
||||||
cellClass: 'ag-right-aligned-cell',
|
|
||||||
valueParser: params => numericParser(params.newValue),
|
valueParser: params => numericParser(params.newValue),
|
||||||
valueFormatter: formatEditableNumber,
|
valueFormatter: formatEditableNumber,
|
||||||
cellClassRules: {
|
cellClassRules: {
|
||||||
|
'ag-right-aligned-cell': () => true,
|
||||||
'editable-cell-empty': params => params.value == null || params.value === ''
|
'editable-cell-empty': params => params.value == null || params.value === ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -538,10 +538,10 @@ const columnDefs: ColDef<FeeMethodRow>[] = [
|
|||||||
flex: 1.2,
|
flex: 1.2,
|
||||||
editable: false,
|
editable: false,
|
||||||
headerClass: 'ag-right-aligned-header',
|
headerClass: 'ag-right-aligned-header',
|
||||||
cellClass: 'ag-right-aligned-cell',
|
|
||||||
valueParser: params => numericParser(params.newValue),
|
valueParser: params => numericParser(params.newValue),
|
||||||
valueFormatter: formatEditableNumber,
|
valueFormatter: formatEditableNumber,
|
||||||
cellClassRules: {
|
cellClassRules: {
|
||||||
|
'ag-right-aligned-cell': () => true,
|
||||||
'editable-cell-empty': params => params.value == null || params.value === ''
|
'editable-cell-empty': params => params.value == null || params.value === ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -552,7 +552,9 @@ const columnDefs: ColDef<FeeMethodRow>[] = [
|
|||||||
flex: 1.2,
|
flex: 1.2,
|
||||||
editable: false,
|
editable: false,
|
||||||
headerClass: 'ag-right-aligned-header',
|
headerClass: 'ag-right-aligned-header',
|
||||||
cellClass: 'ag-right-aligned-cell',
|
cellClassRules: {
|
||||||
|
'ag-right-aligned-cell': () => true
|
||||||
|
},
|
||||||
valueGetter: params => getRowSubtotal(params.data),
|
valueGetter: params => getRowSubtotal(params.data),
|
||||||
valueFormatter: formatEditableNumber
|
valueFormatter: formatEditableNumber
|
||||||
},
|
},
|
||||||
|
|||||||
@ -160,8 +160,10 @@ const columnDefs: ColDef<FactorRow>[] = [
|
|||||||
minWidth: 86,
|
minWidth: 86,
|
||||||
maxWidth: 100,
|
maxWidth: 100,
|
||||||
headerClass: 'ag-right-aligned-header',
|
headerClass: 'ag-right-aligned-header',
|
||||||
cellClass: 'ag-right-aligned-cell',
|
|
||||||
flex: 0.9,
|
flex: 0.9,
|
||||||
|
cellClassRules: {
|
||||||
|
'ag-right-aligned-cell': () => true
|
||||||
|
},
|
||||||
valueFormatter: params => formatReadonlyFactor(params.value)
|
valueFormatter: params => formatReadonlyFactor(params.value)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -172,10 +174,11 @@ const columnDefs: ColDef<FactorRow>[] = [
|
|||||||
headerClass: 'ag-right-aligned-header',
|
headerClass: 'ag-right-aligned-header',
|
||||||
cellClass: params => {
|
cellClass: params => {
|
||||||
const disabled = props.disableBudgetEditWhenStandardNull && params.data?.standardFactor == null
|
const disabled = props.disableBudgetEditWhenStandardNull && params.data?.standardFactor == null
|
||||||
return disabled ? 'ag-right-aligned-cell' : 'ag-right-aligned-cell editable-cell-line'
|
return disabled ? '' : 'editable-cell-line'
|
||||||
},
|
},
|
||||||
flex: 0.9,
|
flex: 0.9,
|
||||||
cellClassRules: {
|
cellClassRules: {
|
||||||
|
'ag-right-aligned-cell': () => true,
|
||||||
'editable-cell-empty': params => {
|
'editable-cell-empty': params => {
|
||||||
const disabled = props.disableBudgetEditWhenStandardNull && params.data?.standardFactor == null
|
const disabled = props.disableBudgetEditWhenStandardNull && params.data?.standardFactor == null
|
||||||
return !disabled && (params.value == null || params.value === '')
|
return !disabled && (params.value == null || params.value === '')
|
||||||
|
|||||||
@ -338,11 +338,12 @@ const columnDefs: ColDef<DetailRow>[] = [
|
|||||||
},
|
},
|
||||||
cellClass: params =>
|
cellClass: params =>
|
||||||
roughCalcEnabled.value && params.node?.rowPinned
|
roughCalcEnabled.value && params.node?.rowPinned
|
||||||
? 'ag-right-aligned-cell editable-cell-line'
|
? 'editable-cell-line'
|
||||||
: !roughCalcEnabled.value && !params.node?.group && !params.node?.rowPinned && params.data?.hasCost
|
: !roughCalcEnabled.value && !params.node?.group && !params.node?.rowPinned && params.data?.hasCost
|
||||||
? 'ag-right-aligned-cell editable-cell-line'
|
? 'editable-cell-line'
|
||||||
: 'ag-right-aligned-cell',
|
: '',
|
||||||
cellClassRules: {
|
cellClassRules: {
|
||||||
|
'ag-right-aligned-cell': () => true,
|
||||||
'editable-cell-empty': params =>
|
'editable-cell-empty': params =>
|
||||||
roughCalcEnabled.value && params.node?.rowPinned
|
roughCalcEnabled.value && params.node?.rowPinned
|
||||||
? params.value == null || params.value === ''
|
? params.value == null || params.value === ''
|
||||||
@ -379,9 +380,10 @@ const columnDefs: ColDef<DetailRow>[] = [
|
|||||||
editable: params => !roughCalcEnabled.value && !params.node?.group && !params.node?.rowPinned && Boolean(params.data?.hasArea),
|
editable: params => !roughCalcEnabled.value && !params.node?.group && !params.node?.rowPinned && Boolean(params.data?.hasArea),
|
||||||
cellClass: params =>
|
cellClass: params =>
|
||||||
!params.node?.group && !params.node?.rowPinned && params.data?.hasArea
|
!params.node?.group && !params.node?.rowPinned && params.data?.hasArea
|
||||||
? 'ag-right-aligned-cell editable-cell-line'
|
? 'editable-cell-line'
|
||||||
: 'ag-right-aligned-cell',
|
: '',
|
||||||
cellClassRules: {
|
cellClassRules: {
|
||||||
|
'ag-right-aligned-cell': () => true,
|
||||||
'editable-cell-empty': params =>
|
'editable-cell-empty': params =>
|
||||||
!params.node?.group && !params.node?.rowPinned && Boolean(params.data?.hasArea) && (params.value == null || params.value === '')
|
!params.node?.group && !params.node?.rowPinned && Boolean(params.data?.hasArea) && (params.value == null || params.value === '')
|
||||||
},
|
},
|
||||||
|
|||||||
@ -199,7 +199,7 @@ const confirmQuickCalc = async () => {
|
|||||||
tabStore.enterWorkspace({
|
tabStore.enterWorkspace({
|
||||||
id: `contract-${QUICK_CONTRACT_ID}`,
|
id: `contract-${QUICK_CONTRACT_ID}`,
|
||||||
title: contractName,
|
title: contractName,
|
||||||
componentName: 'QuickCalcView',
|
componentName: 'QuickCalcWorkbenchView',
|
||||||
props: {
|
props: {
|
||||||
contractId: QUICK_CONTRACT_ID,
|
contractId: QUICK_CONTRACT_ID,
|
||||||
contractName,
|
contractName,
|
||||||
|
|||||||
1076
src/components/views/QuickCalcWorkbenchView.vue
Normal file
1076
src/components/views/QuickCalcWorkbenchView.vue
Normal file
File diff suppressed because it is too large
Load Diff
@ -126,6 +126,8 @@ interface ZxFwStorageLike {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface ScaleMethodRowLike extends ScaleRowLike {
|
interface ScaleMethodRowLike extends ScaleRowLike {
|
||||||
|
benchmarkBudgetBasicChecked?: unknown
|
||||||
|
benchmarkBudgetOptionalChecked?: unknown
|
||||||
basicFormula?: unknown
|
basicFormula?: unknown
|
||||||
optionalFormula?: unknown
|
optionalFormula?: unknown
|
||||||
budgetFee?: unknown
|
budgetFee?: unknown
|
||||||
@ -482,6 +484,7 @@ const userGuideSteps: UserGuideStep[] = [
|
|||||||
const componentMap: Record<string, any> = {
|
const componentMap: Record<string, any> = {
|
||||||
ProjectCalcView: markRaw(defineAsyncComponent(() => import('@/components/xm/xmCard.vue'))),
|
ProjectCalcView: markRaw(defineAsyncComponent(() => import('@/components/xm/xmCard.vue'))),
|
||||||
QuickCalcView: markRaw(defineAsyncComponent(() => import('@/components/ht/htCard.vue'))),
|
QuickCalcView: markRaw(defineAsyncComponent(() => import('@/components/ht/htCard.vue'))),
|
||||||
|
QuickCalcWorkbenchView: markRaw(defineAsyncComponent(() => import('@/components/views/QuickCalcWorkbenchView.vue'))),
|
||||||
ZxFwView: markRaw(defineAsyncComponent(() => import('@/components/views/ZxFwView.vue'))),
|
ZxFwView: markRaw(defineAsyncComponent(() => import('@/components/views/ZxFwView.vue'))),
|
||||||
HtFeeMethodTypeLineView: markRaw(defineAsyncComponent(() => import('@/components/views/HtFeeMethodTypeLineView.vue'))),
|
HtFeeMethodTypeLineView: markRaw(defineAsyncComponent(() => import('@/components/views/HtFeeMethodTypeLineView.vue'))),
|
||||||
}
|
}
|
||||||
@ -1105,25 +1108,30 @@ const resolveTaskRowServiceId = (row: WorkContentRowLike): number | null =>
|
|||||||
const resolveScaleMethodFee = (row: ScaleMethodRowLike, mode: 'cost' | 'area') => {
|
const resolveScaleMethodFee = (row: ScaleMethodRowLike, mode: 'cost' | 'area') => {
|
||||||
const scaleValue = mode === 'cost' ? toFiniteNumber(row.amount) : toFiniteNumber(row.landArea)
|
const scaleValue = mode === 'cost' ? toFiniteNumber(row.amount) : toFiniteNumber(row.landArea)
|
||||||
const benchmarkSplit = getBenchmarkBudgetSplitByScale(scaleValue, mode)
|
const benchmarkSplit = getBenchmarkBudgetSplitByScale(scaleValue, mode)
|
||||||
|
const basicChecked = row.benchmarkBudgetBasicChecked !== false
|
||||||
|
const optionalChecked = row.benchmarkBudgetOptionalChecked !== false
|
||||||
|
const allUnchecked = !basicChecked && !optionalChecked
|
||||||
|
const benchmarkBudgetBasic = benchmarkSplit ? (basicChecked ? benchmarkSplit.basic : 0) : null
|
||||||
|
const benchmarkBudgetOptional = benchmarkSplit ? (optionalChecked ? benchmarkSplit.optional : 0) : null
|
||||||
const computedSplit = benchmarkSplit
|
const computedSplit = benchmarkSplit
|
||||||
? getScaleBudgetFeeSplit({
|
? getScaleBudgetFeeSplit({
|
||||||
benchmarkBudgetBasic: benchmarkSplit.basic,
|
benchmarkBudgetBasic,
|
||||||
benchmarkBudgetOptional: benchmarkSplit.optional,
|
benchmarkBudgetOptional,
|
||||||
majorFactor: row.majorFactor,
|
majorFactor: row.majorFactor,
|
||||||
consultCategoryFactor: row.consultCategoryFactor,
|
consultCategoryFactor: row.consultCategoryFactor,
|
||||||
workStageFactor: row.workStageFactor,
|
workStageFactor: row.workStageFactor,
|
||||||
workRatio: row.workRatio
|
workRatio: row.workRatio
|
||||||
})
|
})
|
||||||
: null
|
: null
|
||||||
const basicFee = toFiniteNumber(row.budgetFee) ?? computedSplit?.total ?? null
|
const basicFee = allUnchecked ? null : (toFiniteNumber(row.budgetFee) ?? computedSplit?.total ?? null)
|
||||||
const basicFeeBasic = toFiniteNumber(row.budgetFeeBasic) ?? computedSplit?.basic ?? null
|
const basicFeeBasic = allUnchecked ? null : (toFiniteNumber(row.budgetFeeBasic) ?? computedSplit?.basic ?? null)
|
||||||
const basicFeeOptional = toFiniteNumber(row.budgetFeeOptional) ?? computedSplit?.optional ?? null
|
const basicFeeOptional = allUnchecked ? null : (toFiniteNumber(row.budgetFeeOptional) ?? computedSplit?.optional ?? null)
|
||||||
const basicFormula = typeof row.basicFormula === 'string' && row.basicFormula.trim()
|
const basicFormula = typeof row.basicFormula === 'string' && row.basicFormula.trim()
|
||||||
? row.basicFormula
|
? row.basicFormula
|
||||||
: (benchmarkSplit?.basicFormula ?? '')
|
: (basicChecked ? (benchmarkSplit?.basicFormula ?? '') : '')
|
||||||
const optionalFormula = typeof row.optionalFormula === 'string' && row.optionalFormula.trim()
|
const optionalFormula = typeof row.optionalFormula === 'string' && row.optionalFormula.trim()
|
||||||
? row.optionalFormula
|
? row.optionalFormula
|
||||||
: (benchmarkSplit?.optionalFormula ?? '')
|
: (optionalChecked ? (benchmarkSplit?.optionalFormula ?? '') : '')
|
||||||
return {
|
return {
|
||||||
basicFee,
|
basicFee,
|
||||||
basicFeeBasic,
|
basicFeeBasic,
|
||||||
@ -1251,13 +1259,7 @@ const buildMethod1 = (rows: ScaleMethodRowLike[] | undefined): ExportMethod1 | n
|
|||||||
const basicFeeBasic = feeResolved.basicFeeBasic
|
const basicFeeBasic = feeResolved.basicFeeBasic
|
||||||
const basicFeeOptional = feeResolved.basicFeeOptional
|
const basicFeeOptional = feeResolved.basicFeeOptional
|
||||||
const remark = typeof row.remark === 'string' ? row.remark : ''
|
const remark = typeof row.remark === 'string' ? row.remark : ''
|
||||||
const hasValue =
|
if (basicFee == null) return null
|
||||||
cost != null ||
|
|
||||||
basicFee != null ||
|
|
||||||
basicFeeBasic != null ||
|
|
||||||
basicFeeOptional != null ||
|
|
||||||
isNonEmptyString(remark)
|
|
||||||
if (!hasValue) return null
|
|
||||||
return {
|
return {
|
||||||
proNum,
|
proNum,
|
||||||
major,
|
major,
|
||||||
@ -1306,13 +1308,7 @@ const buildMethod2 = (rows: ScaleMethodRowLike[] | undefined): ExportMethod2 | n
|
|||||||
const basicFeeBasic = feeResolved.basicFeeBasic
|
const basicFeeBasic = feeResolved.basicFeeBasic
|
||||||
const basicFeeOptional = feeResolved.basicFeeOptional
|
const basicFeeOptional = feeResolved.basicFeeOptional
|
||||||
const remark = typeof row.remark === 'string' ? row.remark : ''
|
const remark = typeof row.remark === 'string' ? row.remark : ''
|
||||||
const hasValue =
|
if (basicFee == null) return null
|
||||||
area != null ||
|
|
||||||
basicFee != null ||
|
|
||||||
basicFeeBasic != null ||
|
|
||||||
basicFeeOptional != null ||
|
|
||||||
isNonEmptyString(remark)
|
|
||||||
if (!hasValue) return null
|
|
||||||
return {
|
return {
|
||||||
proNum,
|
proNum,
|
||||||
major,
|
major,
|
||||||
@ -2149,7 +2145,7 @@ watch(
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex shrink-0 self-center items-center gap-1">
|
<div class="flex shrink-0 self-center items-center gap-1">
|
||||||
<div ref="dataMenuRef" class="relative shrink-0">
|
<div v-if="readWorkspaceMode() !== 'quick'" ref="dataMenuRef" class="relative shrink-0">
|
||||||
<Button variant="outline" size="sm"
|
<Button variant="outline" size="sm"
|
||||||
class="app-toolbar-btn shrink-0 cursor-pointer"
|
class="app-toolbar-btn shrink-0 cursor-pointer"
|
||||||
:disabled="isResetting"
|
:disabled="isResetting"
|
||||||
@ -2179,7 +2175,7 @@ watch(
|
|||||||
<input ref="importFileRef" type="file" accept=".zw" class="hidden" @change="importData" />
|
<input ref="importFileRef" type="file" accept=".zw" class="hidden" @change="importData" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button variant="outline" size="sm" class="app-toolbar-btn shrink-0 cursor-pointer"
|
<Button v-if="readWorkspaceMode() !== 'quick'" variant="outline" size="sm" class="app-toolbar-btn shrink-0 cursor-pointer"
|
||||||
:disabled="isResetting"
|
:disabled="isResetting"
|
||||||
@click="openUserGuide(0)">
|
@click="openUserGuide(0)">
|
||||||
<CircleHelp class="h-4 w-4 mr-1" />
|
<CircleHelp class="h-4 w-4 mr-1" />
|
||||||
|
|||||||
79
src/lib/agGridResetHeader.ts
Normal file
79
src/lib/agGridResetHeader.ts
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import type { IHeaderComp, IHeaderParams } from 'ag-grid-community'
|
||||||
|
|
||||||
|
export type ResetHeaderParams = IHeaderParams & {
|
||||||
|
onReset?: () => void | Promise<void>
|
||||||
|
resetTitle?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AgGridResetHeader implements IHeaderComp {
|
||||||
|
private params!: ResetHeaderParams
|
||||||
|
private eGui!: HTMLDivElement
|
||||||
|
private eLabel!: HTMLSpanElement
|
||||||
|
private eButton!: HTMLButtonElement
|
||||||
|
private onButtonClick = (event: MouseEvent) => {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
void this.params.onReset?.()
|
||||||
|
}
|
||||||
|
|
||||||
|
init(params: ResetHeaderParams) {
|
||||||
|
this.params = params
|
||||||
|
|
||||||
|
const eGui = document.createElement('div')
|
||||||
|
eGui.style.display = 'flex'
|
||||||
|
eGui.style.alignItems = 'center'
|
||||||
|
eGui.style.justifyContent = 'space-between'
|
||||||
|
eGui.style.gap = '6px'
|
||||||
|
eGui.style.width = '100%'
|
||||||
|
|
||||||
|
const eLabel = document.createElement('span')
|
||||||
|
eLabel.style.flex = '1'
|
||||||
|
eLabel.style.minWidth = '0'
|
||||||
|
eLabel.style.whiteSpace = 'normal'
|
||||||
|
eLabel.style.lineHeight = '1.2'
|
||||||
|
|
||||||
|
const eButton = document.createElement('button')
|
||||||
|
eButton.type = 'button'
|
||||||
|
eButton.textContent = '↻'
|
||||||
|
eButton.title = params.resetTitle || '恢复默认值'
|
||||||
|
eButton.setAttribute('aria-label', params.resetTitle || '恢复默认值')
|
||||||
|
eButton.style.display = 'inline-flex'
|
||||||
|
eButton.style.alignItems = 'center'
|
||||||
|
eButton.style.justifyContent = 'center'
|
||||||
|
eButton.style.width = '18px'
|
||||||
|
eButton.style.height = '18px'
|
||||||
|
eButton.style.border = '1px solid #d1d5db'
|
||||||
|
eButton.style.borderRadius = '999px'
|
||||||
|
eButton.style.background = '#fff'
|
||||||
|
eButton.style.color = '#4b5563'
|
||||||
|
eButton.style.cursor = 'pointer'
|
||||||
|
eButton.style.fontSize = '12px'
|
||||||
|
eButton.style.lineHeight = '1'
|
||||||
|
eButton.style.flex = '0 0 auto'
|
||||||
|
eButton.addEventListener('click', this.onButtonClick)
|
||||||
|
|
||||||
|
eGui.append(eLabel, eButton)
|
||||||
|
|
||||||
|
this.eGui = eGui
|
||||||
|
this.eLabel = eLabel
|
||||||
|
this.eButton = eButton
|
||||||
|
this.refresh(params)
|
||||||
|
}
|
||||||
|
|
||||||
|
getGui() {
|
||||||
|
return this.eGui
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh(params: ResetHeaderParams) {
|
||||||
|
this.params = params
|
||||||
|
this.eLabel.textContent = params.displayName || params.column?.getColDef().headerName || ''
|
||||||
|
this.eButton.title = params.resetTitle || '恢复默认值'
|
||||||
|
this.eButton.setAttribute('aria-label', params.resetTitle || '恢复默认值')
|
||||||
|
this.eButton.style.visibility = params.onReset ? 'visible' : 'hidden'
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
this.eButton?.removeEventListener('click', this.onButtonClick)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -37,6 +37,8 @@ export const gridOptions: GridOptions = {
|
|||||||
suppressContextMenu: false,
|
suppressContextMenu: false,
|
||||||
groupDefaultExpanded: -1,
|
groupDefaultExpanded: -1,
|
||||||
suppressFieldDotNotation: true,
|
suppressFieldDotNotation: true,
|
||||||
|
enterNavigatesVertically: true,
|
||||||
|
enterNavigatesVerticallyAfterEdit: true,
|
||||||
// rowData 更新后通过稳定 ID 维持展开状态和编辑上下文。
|
// rowData 更新后通过稳定 ID 维持展开状态和编辑上下文。
|
||||||
getRowId: params => {
|
getRowId: params => {
|
||||||
const id = params.data?.id
|
const id = params.data?.id
|
||||||
|
|||||||
@ -76,7 +76,6 @@ export const getScaleBudgetFeeSplit = (params: {
|
|||||||
const roundedBenchmarkBudget = roundTo(addNumbers(benchmarkBudgetBasic, benchmarkBudgetOptional), 2)
|
const roundedBenchmarkBudget = roundTo(addNumbers(benchmarkBudgetBasic, benchmarkBudgetOptional), 2)
|
||||||
const basic = roundTo(toDecimal(benchmarkBudgetBasic).mul(multiplier), 2)
|
const basic = roundTo(toDecimal(benchmarkBudgetBasic).mul(multiplier), 2)
|
||||||
const optional = roundTo(toDecimal(benchmarkBudgetOptional).mul(multiplier), 2)
|
const optional = roundTo(toDecimal(benchmarkBudgetOptional).mul(multiplier), 2)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
basic,
|
basic,
|
||||||
optional,
|
optional,
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { getMajorDictById, getMajorIdAliasMap, getServiceDictById } from '@/sql'
|
import { getMajorDictById, getMajorIdAliasMap, getServiceDictById } from '@/sql'
|
||||||
import { toFiniteNumberOrNull } from '@/lib/number'
|
import { toFiniteNumberOrNull } from '@/lib/number'
|
||||||
import { useKvStore } from '@/pinia/kv'
|
import { useKvStore } from '@/pinia/kv'
|
||||||
|
import { useZxFwPricingStore } from '@/pinia/zxFwPricing'
|
||||||
|
|
||||||
const CONSULT_CATEGORY_FACTOR_KEY = 'xm-consult-category-factor-v1'
|
const CONSULT_CATEGORY_FACTOR_KEY = 'xm-consult-category-factor-v1'
|
||||||
const MAJOR_FACTOR_KEY = 'xm-major-factor-v1'
|
const MAJOR_FACTOR_KEY = 'xm-major-factor-v1'
|
||||||
@ -49,13 +50,23 @@ const getKvStoreSafely = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getZxFwPricingStoreSafely = () => {
|
||||||
|
try {
|
||||||
|
return useZxFwPricingStore()
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const loadFactorMap = async (
|
const loadFactorMap = async (
|
||||||
storageKey: string,
|
storageKey: string,
|
||||||
dict: FactorDict,
|
dict: FactorDict,
|
||||||
aliases?: Map<string, string>
|
aliases?: Map<string, string>
|
||||||
): Promise<Map<string, number | null>> => {
|
): Promise<Map<string, number | null>> => {
|
||||||
|
const zxFwPricingStore = getZxFwPricingStoreSafely()
|
||||||
const kvStore = getKvStoreSafely()
|
const kvStore = getKvStoreSafely()
|
||||||
const data = kvStore ? await kvStore.getItem<XmFactorState>(storageKey) : null
|
const piniaData = zxFwPricingStore ? await zxFwPricingStore.loadKeyState<XmFactorState>(storageKey) : null
|
||||||
|
const data = piniaData ?? (kvStore ? await kvStore.getItem<XmFactorState>(storageKey) : null)
|
||||||
const map = buildStandardFactorMap(dict)
|
const map = buildStandardFactorMap(dict)
|
||||||
for (const row of data?.detailRows || []) {
|
for (const row of data?.detailRows || []) {
|
||||||
if (!row?.id) continue
|
if (!row?.id) continue
|
||||||
|
|||||||
164
src/sql.ts
164
src/sql.ts
@ -77,7 +77,7 @@ export const majorList = {
|
|||||||
0: { code: 'E1', name: '交通运输工程通用专业', hideInIndustrySelector: true, maxCoe: null, minCoe: null, defCoe: null, desc: '', isRoad: true, isRailway: true, isWaterway: true, order: 1, hasCost: false, hasArea: false },
|
0: { code: 'E1', name: '交通运输工程通用专业', hideInIndustrySelector: true, maxCoe: null, minCoe: null, defCoe: null, desc: '', isRoad: true, isRailway: true, isWaterway: true, order: 1, hasCost: false, hasArea: false },
|
||||||
1: { code: 'E1-1', name: '征地(用海)补偿', maxCoe: null, minCoe: null, defCoe: 1, desc: '适用于交通建设项目征地(用海)补偿的施工图预算、招标工程量清单及清单预算(或最高投标限价)、清理概算(仅限铁路工程)、合同(工程)结算和造价鉴定、计算工程量、工程变更费用咨询、工程成本测(核)算', isRoad: true, isRailway: true, isWaterway: true, order: 2, hasCost: true, hasArea: true },
|
1: { code: 'E1-1', name: '征地(用海)补偿', maxCoe: null, minCoe: null, defCoe: 1, desc: '适用于交通建设项目征地(用海)补偿的施工图预算、招标工程量清单及清单预算(或最高投标限价)、清理概算(仅限铁路工程)、合同(工程)结算和造价鉴定、计算工程量、工程变更费用咨询、工程成本测(核)算', isRoad: true, isRailway: true, isWaterway: true, order: 2, hasCost: true, hasArea: true },
|
||||||
2: { code: 'E1-2', name: '拆迁补偿', maxCoe: null, minCoe: null, defCoe: 2.5, desc: '适用于交通建设项目拆迁补偿的施工图预算、招标工程量清单及清单预算(或最高投标限价)、清理概算(仅限铁路工程)、合同(工程)结算和造价鉴定、计算工程量、工程变更费用咨询、工程成本测(核)算', isRoad: true, isRailway: true, isWaterway: true, order: 3, hasCost: true, hasArea: true },
|
2: { code: 'E1-2', name: '拆迁补偿', maxCoe: null, minCoe: null, defCoe: 2.5, desc: '适用于交通建设项目拆迁补偿的施工图预算、招标工程量清单及清单预算(或最高投标限价)、清理概算(仅限铁路工程)、合同(工程)结算和造价鉴定、计算工程量、工程变更费用咨询、工程成本测(核)算', isRoad: true, isRailway: true, isWaterway: true, order: 3, hasCost: true, hasArea: true },
|
||||||
3: { code: 'E1-3', name: '迁改工程', maxCoe: null, minCoe: null, defCoe: 2, desc: '适用于交通建设项目迁改工程的施工图预算、招标工程量清单及清单预算(或最高投标限价)、清理概算(仅限铁路工程)、合同(工程)结算和造价鉴定、计算工程量、工程变更费用咨询、工程成本测(核)算', isRoad: true, isRailway: true, isWaterway: true, order: 4, hasCost: true, hasArea: false },
|
3: { code: 'E1-3', name: '迁改工程', quickLabel: '迁改工程等费用', maxCoe: null, minCoe: null, defCoe: 2, desc: '适用于交通建设项目迁改工程的施工图预算、招标工程量清单及清单预算(或最高投标限价)、清理概算(仅限铁路工程)、合同(工程)结算和造价鉴定、计算工程量、工程变更费用咨询、工程成本测(核)算', isRoad: true, isRailway: true, isWaterway: true, order: 4, hasCost: true, hasArea: false },
|
||||||
4: { code: 'E1-4', name: '工程建设其他费', maxCoe: null, minCoe: null, defCoe: 1, desc: '适用于交通建设项目的工程建设其他费的施工图预算、招标工程量清单及清单预算(或最高投标限价)、清理概算(仅限铁路工程)和造价鉴定、计算工程量、工程变更费用咨询、工程成本测(核)算', isRoad: true, isRailway: true, isWaterway: true, order: 5, hasCost: true, hasArea: false },
|
4: { code: 'E1-4', name: '工程建设其他费', maxCoe: null, minCoe: null, defCoe: 1, desc: '适用于交通建设项目的工程建设其他费的施工图预算、招标工程量清单及清单预算(或最高投标限价)、清理概算(仅限铁路工程)和造价鉴定、计算工程量、工程变更费用咨询、工程成本测(核)算', isRoad: true, isRailway: true, isWaterway: true, order: 5, hasCost: true, hasArea: false },
|
||||||
5: { code: 'E1-5', name: '预备费', maxCoe: null, minCoe: null, defCoe: 1, desc: '', isRoad: true, isRailway: true, isWaterway: true, order: 6, hasCost: true, hasArea: false },
|
5: { code: 'E1-5', name: '预备费', maxCoe: null, minCoe: null, defCoe: 1, desc: '', isRoad: true, isRailway: true, isWaterway: true, order: 6, hasCost: true, hasArea: false },
|
||||||
6: { code: 'E1-6', name: '建设期贷款利息', maxCoe: null, minCoe: null, defCoe: 1, desc: '', isRoad: true, isRailway: true, isWaterway: true, order: 7, hasCost: true, hasArea: false },
|
6: { code: 'E1-6', name: '建设期贷款利息', maxCoe: null, minCoe: null, defCoe: 1, desc: '', isRoad: true, isRailway: true, isWaterway: true, order: 7, hasCost: true, hasArea: false },
|
||||||
@ -116,9 +116,9 @@ export const majorList = {
|
|||||||
export const serviceList = {
|
export const serviceList = {
|
||||||
0: { code: 'D1', name: '全过程造价咨询', maxCoe: null, minCoe: null, defCoe: 1, desc: '', isRoad: true, isRailway: true, isWaterway: true, mutiple: false, order: 1, scale: true, onlyCostScale: true, amount: false, workDay: true },
|
0: { code: 'D1', name: '全过程造价咨询', maxCoe: null, minCoe: null, defCoe: 1, desc: '', isRoad: true, isRailway: true, isWaterway: true, mutiple: false, order: 1, scale: true, onlyCostScale: true, amount: false, workDay: true },
|
||||||
1: { code: 'D2', name: '分阶段造价咨询', maxCoe: null, minCoe: null, defCoe: null, desc: '', isRoad: true, isRailway: true, isWaterway: true, mutiple: false, order: 2, scale: null, onlyCostScale: null, amount: null, workDay: null },
|
1: { code: 'D2', name: '分阶段造价咨询', maxCoe: null, minCoe: null, defCoe: null, desc: '', isRoad: true, isRailway: true, isWaterway: true, mutiple: false, order: 2, scale: null, onlyCostScale: null, amount: null, workDay: null },
|
||||||
2: { code: 'D2-1', name: '前期阶段造价咨询', maxCoe: null, minCoe: null, defCoe: 0.5, desc: '', isRoad: true, isRailway: true, isWaterway: true, mutiple: false, order: 3, scale: true, onlyCostScale: true, amount: false, workDay: true },
|
2: { code: 'D2-1', name: '前期阶段造价咨询', quickLabel: '项目前期阶段造价咨询', maxCoe: null, minCoe: null, defCoe: 0.5, desc: '', isRoad: true, isRailway: true, isWaterway: true, mutiple: false, order: 3, scale: true, onlyCostScale: true, amount: false, workDay: true },
|
||||||
3: { code: 'D2-2-1', name: '实施阶段造价咨询(公路、水运)', maxCoe: null, minCoe: null, defCoe: 0.55, desc: '本系数适用于公路和水运工程。', isRoad: true, isRailway: false, isWaterway: true, mutiple: false, order: 4, scale: true, onlyCostScale: true, amount: false, workDay: true },
|
3: { code: 'D2-2-1', name: '实施阶段造价咨询(公路、水运)', quickLabel: '项目实施阶段造价咨询', maxCoe: null, minCoe: null, defCoe: 0.55, desc: '本系数适用于公路和水运工程。', isRoad: true, isRailway: false, isWaterway: true, mutiple: false, order: 4, scale: true, onlyCostScale: true, amount: false, workDay: true },
|
||||||
4: { code: 'D2-2-2', name: '实施阶段造价咨询(铁路)', maxCoe: null, minCoe: null, defCoe: 0.6, desc: '本系数适用于铁路工程。', isRoad: false, isRailway: true, isWaterway: false, mutiple: false, order: 5, scale: true, onlyCostScale: true, amount: false, workDay: true },
|
4: { code: 'D2-2-2', name: '实施阶段造价咨询(铁路)', quickLabel: '项目实施阶段造价咨询', maxCoe: null, minCoe: null, defCoe: 0.6, desc: '本系数适用于铁路工程。', isRoad: false, isRailway: true, isWaterway: false, mutiple: false, order: 5, scale: true, onlyCostScale: true, amount: false, workDay: true },
|
||||||
5: { code: 'D3', name: '基本造价咨询', maxCoe: null, minCoe: null, defCoe: null, desc: '', isRoad: true, isRailway: true, isWaterway: true, mutiple: false, order: 6, scale: null, onlyCostScale: null, amount: null, workDay: null },
|
5: { code: 'D3', name: '基本造价咨询', maxCoe: null, minCoe: null, defCoe: null, desc: '', isRoad: true, isRailway: true, isWaterway: true, mutiple: false, order: 6, scale: null, onlyCostScale: null, amount: null, workDay: null },
|
||||||
6: { code: 'D3-1', name: '投资估算', maxCoe: null, minCoe: null, defCoe: 0.1, desc: '委托同一咨询人同时负责D3-1和D3-2时,D3-1和D3-2的合计调整系数为0.25。', isRoad: true, isRailway: true, isWaterway: true, mutiple: false, order: 7, scale: true, onlyCostScale: true, amount: false, workDay: true },
|
6: { code: 'D3-1', name: '投资估算', maxCoe: null, minCoe: null, defCoe: 0.1, desc: '委托同一咨询人同时负责D3-1和D3-2时,D3-1和D3-2的合计调整系数为0.25。', isRoad: true, isRailway: true, isWaterway: true, mutiple: false, order: 7, scale: true, onlyCostScale: true, amount: false, workDay: true },
|
||||||
7: { code: 'D3-2', name: '设计概算', maxCoe: null, minCoe: null, defCoe: 0.2, desc: '', isRoad: true, isRailway: true, isWaterway: true, mutiple: false, order: 8, scale: true, onlyCostScale: true, amount: false, workDay: true },
|
7: { code: 'D3-2', name: '设计概算', maxCoe: null, minCoe: null, defCoe: 0.2, desc: '', isRoad: true, isRailway: true, isWaterway: true, mutiple: false, order: 8, scale: true, onlyCostScale: true, amount: false, workDay: true },
|
||||||
@ -674,10 +674,159 @@ export const getServiceDictItemById = (id: string | number): DictItem | undefine
|
|||||||
return dict[key]
|
return dict[key]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type QuickCalcOption = {
|
||||||
|
label: string
|
||||||
|
code: string | Record<string, string>
|
||||||
|
}
|
||||||
|
|
||||||
// 判断数值是否命中分段区间:(staLine, endLine]。
|
export type QuickCalcGroup = {
|
||||||
|
key: string
|
||||||
|
label: string
|
||||||
|
hint: string
|
||||||
|
items: QuickCalcOption[]
|
||||||
|
rows: string[][]
|
||||||
|
industryId?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const getQuickDictLabel = (item: DictItem | undefined, fallback = '') =>
|
||||||
|
String(item?.quickLabel || item?.name || fallback)
|
||||||
|
|
||||||
|
const createQuickOptionByServiceCode = (
|
||||||
|
code: string,
|
||||||
|
override?: Partial<QuickCalcOption> & { label?: string }
|
||||||
|
): QuickCalcOption => {
|
||||||
|
const entry = getServiceDictEntries().find(item => String(item.item?.code || '') === code)
|
||||||
|
return {
|
||||||
|
label: override?.label || getQuickDictLabel(entry?.item, code),
|
||||||
|
code: override?.code || code
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const createQuickOptionByMajorCode = (
|
||||||
|
code: string,
|
||||||
|
override?: Partial<QuickCalcOption> & { label?: string }
|
||||||
|
): QuickCalcOption => {
|
||||||
|
const entry = getMajorDictEntries().find(item => String(item.item?.code || '') === code)
|
||||||
|
return {
|
||||||
|
label: override?.label || getQuickDictLabel(entry?.item, code),
|
||||||
|
code: override?.code || code
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getQuickCalcGroups = (): QuickCalcGroup[] => [
|
||||||
|
{
|
||||||
|
key: 'consult',
|
||||||
|
label: '咨询类别(常用)',
|
||||||
|
hint: '先选择咨询类别,再补规模和预算参数。',
|
||||||
|
items: [
|
||||||
|
createQuickOptionByServiceCode('D1'),
|
||||||
|
createQuickOptionByServiceCode('D2-1'),
|
||||||
|
createQuickOptionByServiceCode('D2-2-1', { code: { '0': 'D2-2-1', '1': 'D2-2-2', '2': 'D2-2-1' } }),
|
||||||
|
createQuickOptionByServiceCode('D3-1'),
|
||||||
|
createQuickOptionByServiceCode('D3-2'),
|
||||||
|
createQuickOptionByServiceCode('D3-3'),
|
||||||
|
createQuickOptionByServiceCode('D3-4'),
|
||||||
|
createQuickOptionByServiceCode('D3-5'),
|
||||||
|
createQuickOptionByServiceCode('D3-6-1', { code: { '0': 'D3-6-1', '1': 'D3-6-2', '2': 'D3-6-1' } }),
|
||||||
|
createQuickOptionByServiceCode('D3-7'),
|
||||||
|
createQuickOptionByServiceCode('D4-6'),
|
||||||
|
createQuickOptionByServiceCode('D4-7'),
|
||||||
|
createQuickOptionByServiceCode('D4-8'),
|
||||||
|
createQuickOptionByServiceCode('D4-9'),
|
||||||
|
createQuickOptionByServiceCode('D4-10'),
|
||||||
|
createQuickOptionByServiceCode('D4-11'),
|
||||||
|
createQuickOptionByServiceCode('D4-12')
|
||||||
|
],
|
||||||
|
rows: [
|
||||||
|
['全过程造价咨询', '项目前期阶段造价咨询', '项目实施阶段造价咨询'],
|
||||||
|
['投资估算', '设计概算', '施工图预算', '招标工程量清单及清单预算(或最高投标限价)', '清理概算(仅限铁路)', '合同(工程)结算', '竣工决算'],
|
||||||
|
['造价鉴定', '工程成本测算', '工程成本核算', '计算工程量', '工程变更费用咨询', '调整估算', '调整概算']
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'general',
|
||||||
|
label: '通用专业',
|
||||||
|
hint: '跨行业共用的补偿与其他费用专业。',
|
||||||
|
items: [
|
||||||
|
createQuickOptionByMajorCode('E1-1'),
|
||||||
|
createQuickOptionByMajorCode('E1-2'),
|
||||||
|
createQuickOptionByMajorCode('E1-3'),
|
||||||
|
createQuickOptionByMajorCode('E1-4')
|
||||||
|
],
|
||||||
|
rows: [['征地(用海)补偿', '拆迁补偿', '迁改工程等费用', '工程建设其他费']]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'road',
|
||||||
|
label: '公路工程专业',
|
||||||
|
hint: '首页行业为公路工程时默认展示。',
|
||||||
|
industryId: '0',
|
||||||
|
items: [
|
||||||
|
createQuickOptionByMajorCode('E2'),
|
||||||
|
createQuickOptionByMajorCode('E2-1'),
|
||||||
|
createQuickOptionByMajorCode('E2-2'),
|
||||||
|
createQuickOptionByMajorCode('E2-3'),
|
||||||
|
createQuickOptionByMajorCode('E2-4'),
|
||||||
|
createQuickOptionByMajorCode('E2-5'),
|
||||||
|
createQuickOptionByMajorCode('E2-6'),
|
||||||
|
createQuickOptionByMajorCode('E2-7'),
|
||||||
|
createQuickOptionByMajorCode('E2-8'),
|
||||||
|
createQuickOptionByMajorCode('E2-9'),
|
||||||
|
createQuickOptionByMajorCode('E2-10')
|
||||||
|
],
|
||||||
|
rows: [
|
||||||
|
['建设工程项目'],
|
||||||
|
['临时工程', '路基工程', '路面工程', '桥涵工程', '隧道工程', '交叉工程'],
|
||||||
|
['机电工程', '交通安全设施工程', '绿化及环境保护工程', '房建工程']
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'railway',
|
||||||
|
label: '铁路工程专业',
|
||||||
|
hint: '首页行业为铁路工程时默认展示。',
|
||||||
|
industryId: '1',
|
||||||
|
items: [
|
||||||
|
createQuickOptionByMajorCode('E3'),
|
||||||
|
createQuickOptionByMajorCode('E3-1'),
|
||||||
|
createQuickOptionByMajorCode('E3-2'),
|
||||||
|
createQuickOptionByMajorCode('E3-3'),
|
||||||
|
createQuickOptionByMajorCode('E3-4'),
|
||||||
|
createQuickOptionByMajorCode('E3-5'),
|
||||||
|
createQuickOptionByMajorCode('E3-6'),
|
||||||
|
createQuickOptionByMajorCode('E3-7'),
|
||||||
|
createQuickOptionByMajorCode('E3-8'),
|
||||||
|
createQuickOptionByMajorCode('E3-9')
|
||||||
|
],
|
||||||
|
rows: [
|
||||||
|
['建设工程项目'],
|
||||||
|
['大型临时设施和过渡工程', '路基工程', '桥涵工程', '隧道及明洞工程', '轨道工程'],
|
||||||
|
['通信、信号、信息及灾害监测工程', '电力及电力牵引供电工程', '房建工程(房屋建筑及附属工程)', '装饰装修工程']
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'waterway',
|
||||||
|
label: '水运工程专业',
|
||||||
|
hint: '首页行业为水运工程时默认展示。',
|
||||||
|
industryId: '2',
|
||||||
|
items: [
|
||||||
|
createQuickOptionByMajorCode('E4'),
|
||||||
|
createQuickOptionByMajorCode('E4-1'),
|
||||||
|
createQuickOptionByMajorCode('E4-2'),
|
||||||
|
createQuickOptionByMajorCode('E4-3'),
|
||||||
|
createQuickOptionByMajorCode('E4-4'),
|
||||||
|
createQuickOptionByMajorCode('E4-5', { label: '房建工程(房屋建筑及附属工程)' })
|
||||||
|
],
|
||||||
|
rows: [
|
||||||
|
['建设工程项目'],
|
||||||
|
['临时工程', '土建工程'],
|
||||||
|
['机电与金属结构工程', '设备工程', '房建工程(房屋建筑及附属工程)']
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
// 判断数值是否命中分段区间:默认 (staLine, endLine],但首段 0 需要包含 0。
|
||||||
const inRange = (sv: number, staLine: number, endLine: number | null) =>
|
const inRange = (sv: number, staLine: number, endLine: number | null) =>
|
||||||
staLine < sv && (endLine == null || sv <= endLine)
|
(staLine === 0 ? staLine <= sv : staLine < sv) && (endLine == null || sv <= endLine)
|
||||||
|
|
||||||
// 按分段参数计算基础费用。
|
// 按分段参数计算基础费用。
|
||||||
const calcScaleFee = (params: {
|
const calcScaleFee = (params: {
|
||||||
@ -730,8 +879,9 @@ export function getBasicFeeFromScale(
|
|||||||
scaleValue: unknown,
|
scaleValue: unknown,
|
||||||
scaleType: 'cost' | 'area'
|
scaleType: 'cost' | 'area'
|
||||||
): BasicFeeFromScaleResult | null {
|
): BasicFeeFromScaleResult | null {
|
||||||
|
if (scaleValue == null || scaleValue === '') return null
|
||||||
const sv = Number(scaleValue)
|
const sv = Number(scaleValue)
|
||||||
if (!Number.isFinite(sv) || sv <= 0) return null
|
if (!Number.isFinite(sv) || sv < 0) return null
|
||||||
|
|
||||||
if (scaleType === 'cost') {
|
if (scaleType === 'cost') {
|
||||||
const targetRange = costScaleCal.find(f => inRange(sv, f.staLine, f.endLine))
|
const targetRange = costScaleCal.find(f => inRange(sv, f.staLine, f.endLine))
|
||||||
|
|||||||
@ -365,6 +365,15 @@ input[inputmode='numeric'] {
|
|||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Keep right-aligned editable placeholders right-aligned after refresh/edit cycles. */
|
||||||
|
.xmMx .ag-cell.ag-right-aligned-cell.editable-cell-empty .ag-cell-wrapper {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xmMx .ag-cell.ag-right-aligned-cell.editable-cell-empty .ag-cell-value {
|
||||||
|
text-align: right !important;
|
||||||
|
}
|
||||||
|
|
||||||
/* Web adaptive typography baseline: tablet / laptop / 1080p / 2K / 4K */
|
/* Web adaptive typography baseline: tablet / laptop / 1080p / 2K / 4K */
|
||||||
@media (max-width: 1024px) {
|
@media (max-width: 1024px) {
|
||||||
html {
|
html {
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
{"root":["./src/main.ts","./src/sql.ts","./src/components/ui/button/index.ts","./src/components/ui/card/index.ts","./src/components/ui/scroll-area/index.ts","./src/components/ui/tooltip/index.ts","./src/lib/decimal.ts","./src/lib/diyaggridoptions.ts","./src/lib/number.ts","./src/lib/numberformat.ts","./src/lib/pricinghourlycalc.ts","./src/lib/pricingmethodtotals.ts","./src/lib/pricingpersistcontrol.ts","./src/lib/pricingscalecalc.ts","./src/lib/pricingscalefee.ts","./src/lib/pricingworkloadcalc.ts","./src/lib/projectworkspace.ts","./src/lib/utils.ts","./src/lib/workspace.ts","./src/lib/xmfactordefaults.ts","./src/lib/zwarchive.ts","./src/lib/zxfwpricingsync.ts","./src/pinia/kv.ts","./src/pinia/tab.ts","./src/pinia/zxfwpricing.ts","./src/pinia/zxfwpricinghtfee.ts","./src/pinia/zxfwpricingkeys.ts","./src/pinia/plugin/indexdb.ts","./src/pinia/plugin/types.d.ts","./src/types/pricing.ts","./src/app.vue","./src/components/ht/ht.vue","./src/components/ht/htadditionalworkfee.vue","./src/components/ht/htbaseinfo.vue","./src/components/ht/htconsultcategoryfactor.vue","./src/components/ht/htcontractsummary.vue","./src/components/ht/htfeeratemethodform.vue","./src/components/ht/htmajorfactor.vue","./src/components/ht/htreservefee.vue","./src/components/ht/htcard.vue","./src/components/ht/htinfo.vue","./src/components/ht/zxfw.vue","./src/components/pricing/hourlypricingpane.vue","./src/components/pricing/investmentscalepricingpane.vue","./src/components/pricing/landscalepricingpane.vue","./src/components/pricing/workloadpricingpane.vue","./src/components/shared/hourlyfeegrid.vue","./src/components/shared/htfeegrid.vue","./src/components/shared/htfeemethodgrid.vue","./src/components/shared/methodunavailablenotice.vue","./src/components/shared/servicecheckboxselector.vue","./src/components/shared/workcontentgrid.vue","./src/components/shared/xmfactorgrid.vue","./src/components/shared/xmcommonaggrid.vue","./src/components/ui/button/button.vue","./src/components/ui/card/card.vue","./src/components/ui/card/cardaction.vue","./src/components/ui/card/cardcontent.vue","./src/components/ui/card/carddescription.vue","./src/components/ui/card/cardfooter.vue","./src/components/ui/card/cardheader.vue","./src/components/ui/card/cardtitle.vue","./src/components/ui/scroll-area/scrollarea.vue","./src/components/ui/scroll-area/scrollbar.vue","./src/components/ui/tooltip/tooltipcontent.vue","./src/components/views/homeentryview.vue","./src/components/views/htfeemethodtypelineview.vue","./src/components/views/zxfwview.vue","./src/components/xm/xmconsultcategoryfactor.vue","./src/components/xm/xmmajorfactor.vue","./src/components/xm/info.vue","./src/components/xm/xmcard.vue","./src/components/xm/xminfo.vue","./src/layout/tab.vue","./src/layout/typeline.vue"],"version":"5.9.3"}
|
{"root":["./src/main.ts","./src/sql.ts","./src/components/ui/button/index.ts","./src/components/ui/card/index.ts","./src/components/ui/scroll-area/index.ts","./src/components/ui/tooltip/index.ts","./src/lib/aggridresetheader.ts","./src/lib/decimal.ts","./src/lib/diyaggridoptions.ts","./src/lib/number.ts","./src/lib/numberformat.ts","./src/lib/pricinghourlycalc.ts","./src/lib/pricingmethodtotals.ts","./src/lib/pricingpersistcontrol.ts","./src/lib/pricingscalecalc.ts","./src/lib/pricingscalefee.ts","./src/lib/pricingworkloadcalc.ts","./src/lib/projectworkspace.ts","./src/lib/utils.ts","./src/lib/workspace.ts","./src/lib/xmfactordefaults.ts","./src/lib/zwarchive.ts","./src/lib/zxfwpricingsync.ts","./src/pinia/kv.ts","./src/pinia/tab.ts","./src/pinia/zxfwpricing.ts","./src/pinia/zxfwpricinghtfee.ts","./src/pinia/zxfwpricingkeys.ts","./src/pinia/plugin/indexdb.ts","./src/pinia/plugin/types.d.ts","./src/types/pricing.ts","./src/app.vue","./src/components/ht/ht.vue","./src/components/ht/htadditionalworkfee.vue","./src/components/ht/htbaseinfo.vue","./src/components/ht/htconsultcategoryfactor.vue","./src/components/ht/htcontractsummary.vue","./src/components/ht/htfeeratemethodform.vue","./src/components/ht/htmajorfactor.vue","./src/components/ht/htreservefee.vue","./src/components/ht/htcard.vue","./src/components/ht/htinfo.vue","./src/components/ht/zxfw.vue","./src/components/pricing/hourlypricingpane.vue","./src/components/pricing/investmentscalepricingpane.vue","./src/components/pricing/landscalepricingpane.vue","./src/components/pricing/workloadpricingpane.vue","./src/components/shared/hourlyfeegrid.vue","./src/components/shared/htfeegrid.vue","./src/components/shared/htfeemethodgrid.vue","./src/components/shared/methodunavailablenotice.vue","./src/components/shared/servicecheckboxselector.vue","./src/components/shared/workcontentgrid.vue","./src/components/shared/xmfactorgrid.vue","./src/components/shared/xmcommonaggrid.vue","./src/components/ui/button/button.vue","./src/components/ui/card/card.vue","./src/components/ui/card/cardaction.vue","./src/components/ui/card/cardcontent.vue","./src/components/ui/card/carddescription.vue","./src/components/ui/card/cardfooter.vue","./src/components/ui/card/cardheader.vue","./src/components/ui/card/cardtitle.vue","./src/components/ui/scroll-area/scrollarea.vue","./src/components/ui/scroll-area/scrollbar.vue","./src/components/ui/tooltip/tooltipcontent.vue","./src/components/views/homeentryview.vue","./src/components/views/htfeemethodtypelineview.vue","./src/components/views/quickcalcworkbenchview.vue","./src/components/views/zxfwview.vue","./src/components/xm/xmconsultcategoryfactor.vue","./src/components/xm/xmmajorfactor.vue","./src/components/xm/info.vue","./src/components/xm/xmcard.vue","./src/components/xm/xminfo.vue","./src/layout/tab.vue","./src/layout/typeline.vue"],"version":"5.9.3"}
|
||||||
Loading…
x
Reference in New Issue
Block a user