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,
|
||||
flex: 1.2,
|
||||
headerClass: 'ag-right-aligned-header',
|
||||
cellClass: 'ag-right-aligned-cell',
|
||||
cellClassRules: {
|
||||
'ag-right-aligned-cell': () => true
|
||||
},
|
||||
colSpan: params => {
|
||||
if (!params.data) return 1
|
||||
if (params.data.rowType === 'total') return 4
|
||||
@ -340,7 +342,9 @@ const columnDefs: ColDef<SummaryRow>[] = [
|
||||
minWidth: 120,
|
||||
flex: 1.2,
|
||||
headerClass: 'ag-right-aligned-header',
|
||||
cellClass: 'ag-right-aligned-cell',
|
||||
cellClassRules: {
|
||||
'ag-right-aligned-cell': () => true
|
||||
},
|
||||
valueFormatter: params => {
|
||||
if (params.data?.rowType === 'total') return ''
|
||||
return params.value == null ? '' : formatThousandsFlexible(params.value, 3)
|
||||
@ -352,7 +356,9 @@ const columnDefs: ColDef<SummaryRow>[] = [
|
||||
minWidth: 110,
|
||||
flex: 1.2,
|
||||
headerClass: 'ag-right-aligned-header',
|
||||
cellClass: 'ag-right-aligned-cell',
|
||||
cellClassRules: {
|
||||
'ag-right-aligned-cell': () => true
|
||||
},
|
||||
valueFormatter: params => {
|
||||
if (params.data?.rowType === 'total') return ''
|
||||
return params.value == null ? '' : formatThousandsFlexible(params.value, 3)
|
||||
@ -364,7 +370,9 @@ const columnDefs: ColDef<SummaryRow>[] = [
|
||||
minWidth: 110,
|
||||
flex: 1.2,
|
||||
headerClass: 'ag-right-aligned-header',
|
||||
cellClass: 'ag-right-aligned-cell',
|
||||
cellClassRules: {
|
||||
'ag-right-aligned-cell': () => true
|
||||
},
|
||||
valueFormatter: params => {
|
||||
if (params.data?.rowType === 'total') return ''
|
||||
return params.value == null ? '' : formatThousandsFlexible(params.value, 3)
|
||||
@ -376,7 +384,9 @@ const columnDefs: ColDef<SummaryRow>[] = [
|
||||
minWidth: 120,
|
||||
flex: 1.2,
|
||||
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))
|
||||
},
|
||||
{
|
||||
@ -385,7 +395,9 @@ const columnDefs: ColDef<SummaryRow>[] = [
|
||||
minWidth: 120,
|
||||
flex: 1.2,
|
||||
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))
|
||||
}
|
||||
]
|
||||
|
||||
@ -335,11 +335,11 @@ const summaryView = markRaw(
|
||||
|
||||
// 4. 给分类数组添加严格类型标注
|
||||
const xmCategories: XmCategoryItem[] = [
|
||||
{ key: 'base-info', label: '基础信息', component: htBaseInfoView },
|
||||
{ key: 'base-info', label: '基础信息', component: htBaseInfoView },
|
||||
{ key: 'info', label: '规模信息', component: htView },
|
||||
{ key: 'contract', label: '咨询服务', component: zxfwView },
|
||||
{ key: 'consult-category-factor', label: '咨询分类系数', component: consultCategoryFactorView },
|
||||
{ key: 'major-factor', label: '工程专业系数', component: majorFactorView },
|
||||
{ key: 'contract', label: '咨询服务', component: zxfwView },
|
||||
{ key: 'additional-work-fee', label: '附加工作费', component: additionalWorkFeeView },
|
||||
{ key: 'reserve-fee', label: '预备费', component: reserveFeeView },
|
||||
{ key: 'all', label: '汇总', component: summaryView },
|
||||
|
||||
@ -401,8 +401,10 @@ const createMethodColumn = (
|
||||
headerClass: 'ag-right-aligned-header',
|
||||
minWidth,
|
||||
flex: 1.5,
|
||||
cellClass: 'ag-right-aligned-cell',
|
||||
editable: false,
|
||||
cellClassRules: {
|
||||
'ag-right-aligned-cell': () => true
|
||||
},
|
||||
valueGetter: params => {
|
||||
if (!params.data) return null
|
||||
if (isFixedRow(params.data)) return getMethodTotal(field)
|
||||
@ -656,8 +658,10 @@ const columnDefs: ColDef<DetailRow>[] = [
|
||||
headerClass: 'ag-right-aligned-header',
|
||||
flex: 2,
|
||||
minWidth: 100,
|
||||
cellClass: 'ag-right-aligned-cell',
|
||||
editable: false,
|
||||
cellClassRules: {
|
||||
'ag-right-aligned-cell': () => true
|
||||
},
|
||||
valueGetter: params => {
|
||||
if (!params.data) return null
|
||||
return params.data.subtotal
|
||||
@ -665,13 +669,16 @@ const columnDefs: ColDef<DetailRow>[] = [
|
||||
valueFormatter: params => (params.value == null ? '' : formatThousands(params.value, 2))
|
||||
},
|
||||
{
|
||||
headerName: '确认金额',
|
||||
headerName: '确认金额 ✎',
|
||||
field: 'finalFee',
|
||||
headerClass: 'ag-right-aligned-header',
|
||||
headerTooltip: '该列支持手动修改,修改后会自动汇总到固定小计行',
|
||||
flex: 2,
|
||||
minWidth: 110,
|
||||
cellClass: 'ag-right-aligned-cell',
|
||||
editable: params => !isFixedRow(params.data),
|
||||
cellClassRules: {
|
||||
'ag-right-aligned-cell': () => true
|
||||
},
|
||||
valueGetter: params => {
|
||||
if (!params.data) return null
|
||||
return params.data.finalFee
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<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 type { ColDef, ColGroupDef, GridApi, GridReadyEvent } from 'ag-grid-community'
|
||||
import { getMajorDictEntries, getServiceDictItemById, industryTypeList, isMajorIdInIndustryScope } from '@/sql'
|
||||
@ -12,6 +12,7 @@ import { useKvStore } from '@/pinia/kv'
|
||||
import { loadConsultCategoryFactorMap, loadMajorFactorMap } from '@/lib/xmFactorDefaults'
|
||||
import { parseNumberOrNull } from '@/lib/number'
|
||||
import { getBenchmarkBudgetSplitByScale, getScaleBudgetFeeSplit } from '@/lib/pricingScaleFee'
|
||||
import { AgGridResetHeader } from '@/lib/agGridResetHeader'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
AlertDialogAction,
|
||||
@ -616,23 +617,22 @@ const formatReadonlyMoney = (params: any) => {
|
||||
}
|
||||
|
||||
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) => {
|
||||
detailRows.value = detailRows.value.map(row => {
|
||||
if (row.id !== rowId) return row
|
||||
for (const row of detailRows.value) {
|
||||
if (row.id !== rowId) continue
|
||||
if (checkField === 'benchmarkBudgetBasicChecked') {
|
||||
return {
|
||||
...row,
|
||||
benchmarkBudgetBasicChecked: checked,
|
||||
benchmarkBudgetBasic: checked ? row.benchmarkBudgetBasic : 0
|
||||
}
|
||||
row.benchmarkBudgetBasicChecked = checked
|
||||
row.benchmarkBudgetBasic = checked ? row.benchmarkBudgetBasic : 0
|
||||
return
|
||||
}
|
||||
return {
|
||||
...row,
|
||||
benchmarkBudgetOptionalChecked: checked,
|
||||
benchmarkBudgetOptional: checked ? row.benchmarkBudgetOptional : 0
|
||||
}
|
||||
})
|
||||
row.benchmarkBudgetOptionalChecked = checked
|
||||
row.benchmarkBudgetOptional = checked ? row.benchmarkBudgetOptional : 0
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const createBudgetCellRendererWithCheck = (checkField: BudgetCheckField) => (params: any) => {
|
||||
@ -648,28 +648,42 @@ const createBudgetCellRendererWithCheck = (checkField: BudgetCheckField) => (par
|
||||
wrapper.style.justifyContent = 'space-between'
|
||||
wrapper.style.gap = '6px'
|
||||
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')
|
||||
checkbox.type = 'checkbox'
|
||||
checkbox.className = 'cursor-pointer'
|
||||
|
||||
checkbox.checked = params.data[checkField] !== false
|
||||
checkbox.addEventListener('pointerdown', event => event.stopPropagation())
|
||||
checkbox.addEventListener('mousedown', 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
|
||||
if (!targetRow) return
|
||||
|
||||
updateBudgetCheckState(targetRow.id, checkField, checkbox.checked)
|
||||
handleCellValueChanged()
|
||||
params.api?.refreshCells?.({
|
||||
rowNodes: params.node ? [params.node] : undefined,
|
||||
force: true
|
||||
void nextTick(() => {
|
||||
params.api?.redrawRows?.({
|
||||
rowNodes: params.node ? [params.node] : undefined
|
||||
})
|
||||
params.api?.refreshCells?.({
|
||||
rowNodes: params.node ? [params.node] : undefined,
|
||||
force: true
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
const valueSpan = document.createElement('span')
|
||||
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)
|
||||
|
||||
return wrapper
|
||||
@ -685,11 +699,12 @@ const getCheckedBenchmarkBudgetSplitByAmount = (
|
||||
if (!split) return null
|
||||
const basic = row?.benchmarkBudgetBasicChecked === false ? 0 : split.basic
|
||||
const optional = row?.benchmarkBudgetOptionalChecked === false ? 0 : split.optional
|
||||
const total = isBenchmarkBudgetFullyUnchecked(row) ? null : roundTo(addNumbers(basic, optional), 2)
|
||||
return {
|
||||
...split,
|
||||
basic,
|
||||
optional,
|
||||
total: roundTo(addNumbers(basic, optional), 2)
|
||||
total
|
||||
}
|
||||
}
|
||||
|
||||
@ -705,6 +720,7 @@ const getBudgetFee = (
|
||||
| 'workRatio'
|
||||
>
|
||||
) => {
|
||||
if (isBenchmarkBudgetFullyUnchecked(row)) return null
|
||||
const benchmarkBudgetSplit = getCheckedBenchmarkBudgetSplitByAmount(row)
|
||||
if (!benchmarkBudgetSplit) return null
|
||||
|
||||
@ -731,6 +747,7 @@ const getBudgetFeeSplit = (
|
||||
| 'workRatio'
|
||||
>
|
||||
) => {
|
||||
if (isBenchmarkBudgetFullyUnchecked(row)) return null
|
||||
const benchmarkBudgetSplit = getCheckedBenchmarkBudgetSplitByAmount(row)
|
||||
if (!benchmarkBudgetSplit) return null
|
||||
return getScaleBudgetFeeSplit({
|
||||
@ -753,10 +770,79 @@ const getMergeColSpanBeforeTotal = (params: any) => {
|
||||
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>> = [
|
||||
{
|
||||
headerName: '造价金额(万元)',
|
||||
field: 'amount',
|
||||
headerTooltip: '点击右侧↻恢复本列默认造价金额',
|
||||
headerComponent: AgGridResetHeader,
|
||||
headerComponentParams: {
|
||||
onReset: restoreAmountColumnDefaults,
|
||||
resetTitle: '恢复本列默认造价金额'
|
||||
},
|
||||
headerClass: 'ag-right-aligned-header',
|
||||
minWidth: 90,
|
||||
flex: 2,
|
||||
@ -766,9 +852,10 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
|
||||
},
|
||||
cellClass: params =>
|
||||
!params.node?.group && !params.node?.rowPinned && params.data?.hasCost
|
||||
? 'ag-right-aligned-cell editable-cell-line'
|
||||
: 'ag-right-aligned-cell',
|
||||
? ' editable-cell-line'
|
||||
: '',
|
||||
cellClassRules: {
|
||||
'ag-right-aligned-cell':()=>true,
|
||||
'editable-cell-empty': params =>
|
||||
!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',
|
||||
minWidth: 130,
|
||||
flex: 1,
|
||||
cellClass: 'ag-right-aligned-cell',
|
||||
cellClassRules: {
|
||||
'ag-right-aligned-cell': () => true
|
||||
},
|
||||
valueGetter: params =>
|
||||
params.node?.rowPinned
|
||||
? null
|
||||
: getCheckedBenchmarkBudgetSplitByAmount(params.data)?.basic ?? null,
|
||||
cellRenderer: createBudgetCellRendererWithCheck('benchmarkBudgetBasicChecked'),
|
||||
cellRendererParams: {
|
||||
suppressMouseEventHandling: () => true
|
||||
},
|
||||
valueFormatter: formatReadonlyMoney
|
||||
},
|
||||
{
|
||||
@ -801,12 +893,17 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
|
||||
headerClass: 'ag-right-aligned-header',
|
||||
minWidth: 130,
|
||||
flex: 1,
|
||||
cellClass: 'ag-right-aligned-cell',
|
||||
cellClassRules: {
|
||||
'ag-right-aligned-cell': () => true
|
||||
},
|
||||
valueGetter: params =>
|
||||
params.node?.rowPinned
|
||||
? null
|
||||
: getCheckedBenchmarkBudgetSplitByAmount(params.data)?.optional ?? null,
|
||||
cellRenderer: createBudgetCellRendererWithCheck('benchmarkBudgetOptionalChecked'),
|
||||
cellRendererParams: {
|
||||
suppressMouseEventHandling: () => true
|
||||
},
|
||||
valueFormatter: formatReadonlyMoney
|
||||
},
|
||||
{
|
||||
@ -816,7 +913,9 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
|
||||
headerClass: 'ag-right-aligned-header',
|
||||
minWidth: 100,
|
||||
flex: 1,
|
||||
cellClass: 'ag-right-aligned-cell',
|
||||
cellClassRules: {
|
||||
'ag-right-aligned-cell': () => true
|
||||
},
|
||||
valueGetter: params =>
|
||||
params.node?.rowPinned
|
||||
? null
|
||||
@ -833,15 +932,22 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
|
||||
headerName: '咨询分类系数',
|
||||
field: 'consultCategoryFactor',
|
||||
colId: 'consultCategoryFactor',
|
||||
headerTooltip: '点击右侧↻恢复本列默认咨询分类系数',
|
||||
headerComponent: AgGridResetHeader,
|
||||
headerComponentParams: {
|
||||
onReset: restoreConsultCategoryFactorColumnDefaults,
|
||||
resetTitle: '恢复本列默认咨询分类系数'
|
||||
},
|
||||
headerClass: 'ag-right-aligned-header',
|
||||
minWidth: 80,
|
||||
flex: 1,
|
||||
editable: params => !params.node?.group && !params.node?.rowPinned,
|
||||
cellClass: params =>
|
||||
!params.node?.group && !params.node?.rowPinned
|
||||
? 'ag-right-aligned-cell editable-cell-line'
|
||||
: 'ag-right-aligned-cell',
|
||||
? 'editable-cell-line'
|
||||
: '',
|
||||
cellClassRules: {
|
||||
'ag-right-aligned-cell': () => true,
|
||||
'editable-cell-empty': params =>
|
||||
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
|
||||
},
|
||||
@ -852,15 +958,22 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
|
||||
headerName: '专业系数',
|
||||
field: 'majorFactor',
|
||||
colId: 'majorFactor',
|
||||
headerTooltip: '点击右侧↻恢复本列默认专业系数',
|
||||
headerComponent: AgGridResetHeader,
|
||||
headerComponentParams: {
|
||||
onReset: restoreMajorFactorColumnDefaults,
|
||||
resetTitle: '恢复本列默认专业系数'
|
||||
},
|
||||
headerClass: 'ag-right-aligned-header',
|
||||
minWidth: 80,
|
||||
flex: 1,
|
||||
editable: params => !params.node?.group && !params.node?.rowPinned,
|
||||
cellClass: params =>
|
||||
!params.node?.group && !params.node?.rowPinned
|
||||
? 'ag-right-aligned-cell editable-cell-line'
|
||||
: 'ag-right-aligned-cell',
|
||||
? 'editable-cell-line'
|
||||
: '',
|
||||
cellClassRules: {
|
||||
'ag-right-aligned-cell': () => true,
|
||||
'editable-cell-empty': params =>
|
||||
!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,
|
||||
cellClass: params =>
|
||||
!params.node?.group && !params.node?.rowPinned
|
||||
? 'ag-right-aligned-cell editable-cell-line'
|
||||
: 'ag-right-aligned-cell',
|
||||
? 'editable-cell-line'
|
||||
: '',
|
||||
cellClassRules: {
|
||||
'ag-right-aligned-cell': () => true,
|
||||
'editable-cell-empty': params =>
|
||||
!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,
|
||||
cellClass: params =>
|
||||
!params.node?.group && !params.node?.rowPinned
|
||||
? 'ag-right-aligned-cell editable-cell-line'
|
||||
: 'ag-right-aligned-cell',
|
||||
? 'editable-cell-line'
|
||||
: '',
|
||||
cellClassRules: {
|
||||
'ag-right-aligned-cell': () => true,
|
||||
'editable-cell-empty': params =>
|
||||
!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',
|
||||
minWidth: 120,
|
||||
flex: 1,
|
||||
cellClass: 'ag-right-aligned-cell',
|
||||
aggFunc: decimalAggSum,
|
||||
cellClassRules: {
|
||||
'ag-right-aligned-cell': () => true
|
||||
},
|
||||
valueGetter: params => (params.node?.rowPinned ? params.data?.budgetFee ?? null : getBudgetFee(params.data)),
|
||||
valueFormatter: formatReadonlyMoney
|
||||
}
|
||||
@ -986,7 +1103,9 @@ const autoGroupColumnDef: ColDef = {
|
||||
const sumNullableBy = <T>(rows: T[], pick: (row: T) => unknown): number | null => {
|
||||
let hasValid = false
|
||||
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
|
||||
hasValid = true
|
||||
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 totalBudgetFeeOptional = computed(() => sumNullableBy(detailRows.value, row => getBudgetFeeSplit(row)?.optional))
|
||||
const totalBudgetFee = computed(() =>
|
||||
sumNullableBy(detailRows.value.filter(e => e.budgetFee !== null && e.budgetFee !== undefined), row => getBudgetFee(row))
|
||||
)
|
||||
const totalBudgetFee = computed(() => sumNullableBy(detailRows.value, row => getBudgetFee(row)))
|
||||
const pinnedTopRowData = computed(() => {
|
||||
return [
|
||||
{
|
||||
@ -1058,14 +1175,14 @@ const syncComputedValuesToDetailRows = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const buildPersistDetailRows = () => {
|
||||
syncComputedValuesToDetailRows()
|
||||
return detailRows.value.map(row => ({ ...row }))
|
||||
}
|
||||
const buildPersistDetailRows = () => detailRows.value.map(row => ({ ...row }))
|
||||
|
||||
const saveToIndexedDB = async () => {
|
||||
const saveToIndexedDB = async (options?: { skipComputedSync?: boolean }) => {
|
||||
if (shouldSkipPersist()) return
|
||||
try {
|
||||
if (!options?.skipComputedSync) {
|
||||
syncComputedValuesToDetailRows()
|
||||
}
|
||||
const payload = {
|
||||
detailRows: JSON.parse(JSON.stringify(buildPersistDetailRows())),
|
||||
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) => {
|
||||
if (!row) return ''
|
||||
const majorDictId = resolveRowMajorDictId(row)
|
||||
@ -1134,7 +1346,7 @@ const applyProjectCountChange = async (nextValue: unknown) => {
|
||||
? buildOnlyCostScaleRows(previousRows as any, { projectCount: normalized })
|
||||
: mergeWithDictRows(previousRows as any, { projectCount: normalized })
|
||||
syncComputedValuesToDetailRows()
|
||||
await saveToIndexedDB()
|
||||
await saveToIndexedDB({ skipComputedSync: true })
|
||||
return
|
||||
}
|
||||
|
||||
@ -1167,7 +1379,7 @@ const applyProjectCountChange = async (nextValue: unknown) => {
|
||||
}
|
||||
})
|
||||
syncComputedValuesToDetailRows()
|
||||
await saveToIndexedDB()
|
||||
await saveToIndexedDB({ skipComputedSync: true })
|
||||
}
|
||||
|
||||
const loadFromIndexedDB = async () => {
|
||||
@ -1282,7 +1494,7 @@ const clearAllData = async () => {
|
||||
|
||||
const handleCellValueChanged = () => {
|
||||
syncComputedValuesToDetailRows()
|
||||
void saveToIndexedDB()
|
||||
void saveToIndexedDB({ skipComputedSync: true })
|
||||
}
|
||||
|
||||
const handleGridReady = (event: GridReadyEvent<DetailRow>) => {
|
||||
@ -1291,10 +1503,12 @@ const handleGridReady = (event: GridReadyEvent<DetailRow>) => {
|
||||
|
||||
onMounted(async () => {
|
||||
await loadFromIndexedDB()
|
||||
await syncLinkedFieldsFromContractAndFactors()
|
||||
})
|
||||
|
||||
onActivated(() => {
|
||||
void loadFromIndexedDB()
|
||||
onActivated(async () => {
|
||||
await loadFromIndexedDB()
|
||||
await syncLinkedFieldsFromContractAndFactors()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
@ -1302,6 +1516,14 @@ onBeforeUnmount(() => {
|
||||
gridApi.value = null
|
||||
void saveToIndexedDB()
|
||||
})
|
||||
|
||||
watch(linkedSourceSignature, () => {
|
||||
void syncLinkedFieldsFromContractAndFactors()
|
||||
})
|
||||
|
||||
watch(linkedContractScaleSignature, () => {
|
||||
void syncLinkedScaleValuesFromContract()
|
||||
})
|
||||
const processCellForClipboard = (params: any) => {
|
||||
if (Array.isArray(params.value)) {
|
||||
return JSON.stringify(params.value); // 数组转字符串复制
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<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 type { ColDef, ColGroupDef, GridApi, GridReadyEvent } from 'ag-grid-community'
|
||||
import { getMajorDictEntries, getServiceDictItemById, industryTypeList, isMajorIdInIndustryScope } from '@/sql'
|
||||
@ -12,6 +12,7 @@ import { useKvStore } from '@/pinia/kv'
|
||||
import { loadConsultCategoryFactorMap, loadMajorFactorMap } from '@/lib/xmFactorDefaults'
|
||||
import { parseNumberOrNull } from '@/lib/number'
|
||||
import { getBenchmarkBudgetSplitByScale, getScaleBudgetFeeSplit } from '@/lib/pricingScaleFee'
|
||||
import { AgGridResetHeader } from '@/lib/agGridResetHeader'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
AlertDialogAction,
|
||||
@ -481,23 +482,22 @@ const formatReadonlyMoney = (params: any) => {
|
||||
}
|
||||
|
||||
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) => {
|
||||
detailRows.value = detailRows.value.map(row => {
|
||||
if (row.id !== rowId) return row
|
||||
for (const row of detailRows.value) {
|
||||
if (row.id !== rowId) continue
|
||||
if (checkField === 'benchmarkBudgetBasicChecked') {
|
||||
return {
|
||||
...row,
|
||||
benchmarkBudgetBasicChecked: checked,
|
||||
benchmarkBudgetBasic: checked ? row.benchmarkBudgetBasic : 0
|
||||
}
|
||||
row.benchmarkBudgetBasicChecked = checked
|
||||
row.benchmarkBudgetBasic = checked ? row.benchmarkBudgetBasic : 0
|
||||
return
|
||||
}
|
||||
return {
|
||||
...row,
|
||||
benchmarkBudgetOptionalChecked: checked,
|
||||
benchmarkBudgetOptional: checked ? row.benchmarkBudgetOptional : 0
|
||||
}
|
||||
})
|
||||
row.benchmarkBudgetOptionalChecked = checked
|
||||
row.benchmarkBudgetOptional = checked ? row.benchmarkBudgetOptional : 0
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const createBudgetCellRendererWithCheck = (checkField: BudgetCheckField) => (params: any) => {
|
||||
@ -513,26 +513,40 @@ const createBudgetCellRendererWithCheck = (checkField: BudgetCheckField) => (par
|
||||
wrapper.style.justifyContent = 'space-between'
|
||||
wrapper.style.gap = '6px'
|
||||
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')
|
||||
checkbox.type = 'checkbox'
|
||||
checkbox.className = 'cursor-pointer'
|
||||
checkbox.checked = params.data[checkField] !== false
|
||||
checkbox.addEventListener('pointerdown', event => event.stopPropagation())
|
||||
checkbox.addEventListener('mousedown', 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
|
||||
if (!targetRow) return
|
||||
updateBudgetCheckState(targetRow.id, checkField, checkbox.checked)
|
||||
handleCellValueChanged()
|
||||
params.api?.refreshCells?.({
|
||||
rowNodes: params.node ? [params.node] : undefined,
|
||||
force: true
|
||||
void nextTick(() => {
|
||||
params.api?.redrawRows?.({
|
||||
rowNodes: params.node ? [params.node] : undefined
|
||||
})
|
||||
params.api?.refreshCells?.({
|
||||
rowNodes: params.node ? [params.node] : undefined,
|
||||
force: true
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
const valueSpan = document.createElement('span')
|
||||
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)
|
||||
|
||||
return wrapper
|
||||
@ -548,11 +562,12 @@ const getCheckedBenchmarkBudgetSplitByLandArea = (
|
||||
if (!split) return null
|
||||
const basic = row?.benchmarkBudgetBasicChecked === false ? 0 : split.basic
|
||||
const optional = row?.benchmarkBudgetOptionalChecked === false ? 0 : split.optional
|
||||
const total = isBenchmarkBudgetFullyUnchecked(row) ? null : roundTo(addNumbers(basic, optional), 2)
|
||||
return {
|
||||
...split,
|
||||
basic,
|
||||
optional,
|
||||
total: roundTo(addNumbers(basic, optional), 2)
|
||||
total
|
||||
}
|
||||
}
|
||||
|
||||
@ -568,6 +583,7 @@ const getBudgetFee = (
|
||||
| 'workRatio'
|
||||
>
|
||||
) => {
|
||||
if (isBenchmarkBudgetFullyUnchecked(row)) return null
|
||||
const benchmarkBudgetSplit = getCheckedBenchmarkBudgetSplitByLandArea(row)
|
||||
if (!benchmarkBudgetSplit) return null
|
||||
|
||||
@ -594,6 +610,7 @@ const getBudgetFeeSplit = (
|
||||
| 'workRatio'
|
||||
>
|
||||
) => {
|
||||
if (isBenchmarkBudgetFullyUnchecked(row)) return null
|
||||
const benchmarkBudgetSplit = getCheckedBenchmarkBudgetSplitByLandArea(row)
|
||||
if (!benchmarkBudgetSplit) return null
|
||||
return getScaleBudgetFeeSplit({
|
||||
@ -627,10 +644,74 @@ const formatEditableFlexibleNumber = (params: any) => {
|
||||
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>> = [
|
||||
{
|
||||
headerName: '用地面积(亩)',
|
||||
field: 'landArea',
|
||||
headerTooltip: '点击右侧↻恢复本列默认用地面积',
|
||||
headerComponent: AgGridResetHeader,
|
||||
headerComponentParams: {
|
||||
onReset: restoreLandAreaColumnDefaults,
|
||||
resetTitle: '恢复本列默认用地面积'
|
||||
},
|
||||
headerClass: 'ag-right-aligned-header',
|
||||
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),
|
||||
cellClass: params =>
|
||||
!params.node?.group && !params.node?.rowPinned && params.data?.hasArea
|
||||
? 'ag-right-aligned-cell editable-cell-line'
|
||||
: 'ag-right-aligned-cell',
|
||||
? 'editable-cell-line'
|
||||
: '',
|
||||
cellClassRules: {
|
||||
'ag-right-aligned-cell': () => true,
|
||||
'editable-cell-empty': params =>
|
||||
!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',
|
||||
minWidth: 130,
|
||||
flex: 1,
|
||||
cellClass: 'ag-right-aligned-cell',
|
||||
cellClassRules: {
|
||||
'ag-right-aligned-cell': () => true
|
||||
},
|
||||
valueGetter: params =>
|
||||
params.node?.rowPinned
|
||||
? null
|
||||
: getCheckedBenchmarkBudgetSplitByLandArea(params.data)?.basic ?? null,
|
||||
cellRenderer: createBudgetCellRendererWithCheck('benchmarkBudgetBasicChecked'),
|
||||
cellRendererParams: {
|
||||
suppressMouseEventHandling: () => true
|
||||
},
|
||||
valueFormatter: formatReadonlyMoney
|
||||
},
|
||||
{
|
||||
@ -677,12 +764,17 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
|
||||
headerClass: 'ag-right-aligned-header',
|
||||
minWidth: 130,
|
||||
flex: 1,
|
||||
cellClass: 'ag-right-aligned-cell',
|
||||
cellClassRules: {
|
||||
'ag-right-aligned-cell': () => true
|
||||
},
|
||||
valueGetter: params =>
|
||||
params.node?.rowPinned
|
||||
? null
|
||||
: getCheckedBenchmarkBudgetSplitByLandArea(params.data)?.optional ?? null,
|
||||
cellRenderer: createBudgetCellRendererWithCheck('benchmarkBudgetOptionalChecked'),
|
||||
cellRendererParams: {
|
||||
suppressMouseEventHandling: () => true
|
||||
},
|
||||
valueFormatter: formatReadonlyMoney
|
||||
},
|
||||
{
|
||||
@ -692,7 +784,9 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
|
||||
headerClass: 'ag-right-aligned-header',
|
||||
minWidth: 100,
|
||||
flex: 1,
|
||||
cellClass: 'ag-right-aligned-cell',
|
||||
cellClassRules: {
|
||||
'ag-right-aligned-cell': () => true
|
||||
},
|
||||
valueGetter: params =>
|
||||
params.node?.rowPinned
|
||||
? null
|
||||
@ -709,15 +803,22 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
|
||||
headerName: '咨询分类系数',
|
||||
field: 'consultCategoryFactor',
|
||||
colId: 'consultCategoryFactor',
|
||||
headerTooltip: '点击右侧↻恢复本列默认咨询分类系数',
|
||||
headerComponent: AgGridResetHeader,
|
||||
headerComponentParams: {
|
||||
onReset: restoreConsultCategoryFactorColumnDefaults,
|
||||
resetTitle: '恢复本列默认咨询分类系数'
|
||||
},
|
||||
headerClass: 'ag-right-aligned-header',
|
||||
minWidth: 80,
|
||||
flex: 1,
|
||||
editable: params => !params.node?.group && !params.node?.rowPinned,
|
||||
cellClass: params =>
|
||||
!params.node?.group && !params.node?.rowPinned
|
||||
? 'ag-right-aligned-cell editable-cell-line'
|
||||
: 'ag-right-aligned-cell',
|
||||
? 'editable-cell-line'
|
||||
: '',
|
||||
cellClassRules: {
|
||||
'ag-right-aligned-cell': () => true,
|
||||
'editable-cell-empty': params =>
|
||||
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
|
||||
},
|
||||
@ -728,15 +829,22 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
|
||||
headerName: '专业系数',
|
||||
field: 'majorFactor',
|
||||
colId: 'majorFactor',
|
||||
headerTooltip: '点击右侧↻恢复本列默认专业系数',
|
||||
headerComponent: AgGridResetHeader,
|
||||
headerComponentParams: {
|
||||
onReset: restoreMajorFactorColumnDefaults,
|
||||
resetTitle: '恢复本列默认专业系数'
|
||||
},
|
||||
headerClass: 'ag-right-aligned-header',
|
||||
minWidth: 80,
|
||||
flex: 1,
|
||||
editable: params => !params.node?.group && !params.node?.rowPinned,
|
||||
cellClass: params =>
|
||||
!params.node?.group && !params.node?.rowPinned
|
||||
? 'ag-right-aligned-cell editable-cell-line'
|
||||
: 'ag-right-aligned-cell',
|
||||
? 'editable-cell-line'
|
||||
: '',
|
||||
cellClassRules: {
|
||||
'ag-right-aligned-cell': () => true,
|
||||
'editable-cell-empty': params =>
|
||||
!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,
|
||||
cellClass: params =>
|
||||
!params.node?.group && !params.node?.rowPinned
|
||||
? 'ag-right-aligned-cell editable-cell-line'
|
||||
: 'ag-right-aligned-cell',
|
||||
? 'editable-cell-line'
|
||||
: '',
|
||||
cellClassRules: {
|
||||
'ag-right-aligned-cell': () => true,
|
||||
'editable-cell-empty': params =>
|
||||
!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,
|
||||
cellClass: params =>
|
||||
!params.node?.group && !params.node?.rowPinned
|
||||
? 'ag-right-aligned-cell editable-cell-line'
|
||||
: 'ag-right-aligned-cell',
|
||||
? 'editable-cell-line'
|
||||
: '',
|
||||
cellClassRules: {
|
||||
'ag-right-aligned-cell': () => true,
|
||||
'editable-cell-empty': params =>
|
||||
!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',
|
||||
minWidth: 120,
|
||||
flex: 1,
|
||||
cellClass: 'ag-right-aligned-cell',
|
||||
aggFunc: decimalAggSum,
|
||||
cellClassRules: {
|
||||
'ag-right-aligned-cell': () => true
|
||||
},
|
||||
valueGetter: params => (params.node?.rowPinned ? params.data?.budgetFee ?? null : getBudgetFee(params.data)),
|
||||
valueFormatter: formatReadonlyMoney
|
||||
}
|
||||
@ -864,7 +976,9 @@ const totalBenchmarkBudget = computed(() => sumByNumber(detailRows.value, row =>
|
||||
const sumNullableBy = <T>(rows: T[], pick: (row: T) => unknown): number | null => {
|
||||
let hasValid = false
|
||||
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
|
||||
hasValid = true
|
||||
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 totalBudgetFeeOptional = computed(() => sumNullableBy(detailRows.value, row => getBudgetFeeSplit(row)?.optional))
|
||||
const totalBudgetFee = computed(() =>
|
||||
sumNullableBy(detailRows.value.filter(e => e.budgetFee !== null && e.budgetFee !== undefined), row => getBudgetFee(row))
|
||||
)
|
||||
const totalBudgetFee = computed(() => sumNullableBy(detailRows.value, row => getBudgetFee(row)))
|
||||
const pinnedTopRowData = computed(() => [
|
||||
{
|
||||
id: 'pinned-total-row',
|
||||
@ -909,6 +1021,7 @@ const pinnedTopRowData = computed(() => [
|
||||
|
||||
const syncComputedValuesToDetailRows = () => {
|
||||
for (const row of detailRows.value) {
|
||||
|
||||
const benchmarkBudgetRawSplit = getBenchmarkBudgetSplitByLandArea(row)
|
||||
const benchmarkBudgetSplit = getCheckedBenchmarkBudgetSplitByLandArea(row)
|
||||
const budgetFeeSplit = benchmarkBudgetSplit
|
||||
@ -935,14 +1048,14 @@ const syncComputedValuesToDetailRows = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const buildPersistDetailRows = () => {
|
||||
syncComputedValuesToDetailRows()
|
||||
return detailRows.value.map(row => ({ ...row }))
|
||||
}
|
||||
const buildPersistDetailRows = () => detailRows.value.map(row => ({ ...row }))
|
||||
|
||||
const saveToIndexedDB = async () => {
|
||||
const saveToIndexedDB = async (options?: { skipComputedSync?: boolean }) => {
|
||||
if (shouldSkipPersist()) return
|
||||
try {
|
||||
if (!options?.skipComputedSync) {
|
||||
syncComputedValuesToDetailRows()
|
||||
}
|
||||
const payload = {
|
||||
detailRows: JSON.parse(JSON.stringify(buildPersistDetailRows())),
|
||||
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) => {
|
||||
if (!row) return ''
|
||||
const majorDictId = resolveRowMajorDictId(row)
|
||||
@ -1004,7 +1207,7 @@ const applyProjectCountChange = async (nextValue: unknown) => {
|
||||
if (normalized < previousProjectCount) {
|
||||
detailRows.value = mergeWithDictRows(previousRows as any, { projectCount: normalized })
|
||||
syncComputedValuesToDetailRows()
|
||||
await saveToIndexedDB()
|
||||
await saveToIndexedDB({ skipComputedSync: true })
|
||||
return
|
||||
}
|
||||
|
||||
@ -1037,7 +1240,7 @@ const applyProjectCountChange = async (nextValue: unknown) => {
|
||||
}
|
||||
})
|
||||
syncComputedValuesToDetailRows()
|
||||
await saveToIndexedDB()
|
||||
await saveToIndexedDB({ skipComputedSync: true })
|
||||
}
|
||||
|
||||
const loadFromIndexedDB = async () => {
|
||||
@ -1131,7 +1334,7 @@ const clearAllData = async () => {
|
||||
|
||||
const handleCellValueChanged = () => {
|
||||
syncComputedValuesToDetailRows()
|
||||
void saveToIndexedDB()
|
||||
void saveToIndexedDB({ skipComputedSync: true })
|
||||
}
|
||||
|
||||
const handleGridReady = (event: GridReadyEvent<DetailRow>) => {
|
||||
@ -1140,10 +1343,12 @@ const handleGridReady = (event: GridReadyEvent<DetailRow>) => {
|
||||
|
||||
onMounted(async () => {
|
||||
await loadFromIndexedDB()
|
||||
await syncLinkedFieldsFromContractAndFactors()
|
||||
})
|
||||
|
||||
onActivated(() => {
|
||||
void loadFromIndexedDB()
|
||||
onActivated(async () => {
|
||||
await loadFromIndexedDB()
|
||||
await syncLinkedFieldsFromContractAndFactors()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
@ -1151,6 +1356,14 @@ onBeforeUnmount(() => {
|
||||
gridApi.value = null
|
||||
void saveToIndexedDB()
|
||||
})
|
||||
|
||||
watch(linkedSourceSignature, () => {
|
||||
void syncLinkedFieldsFromContractAndFactors()
|
||||
})
|
||||
|
||||
watch(linkedContractScaleSignature, () => {
|
||||
void syncLinkedScaleValuesFromContract()
|
||||
})
|
||||
const processCellForClipboard = (params: any) => {
|
||||
if (Array.isArray(params.value)) {
|
||||
return JSON.stringify(params.value); // 数组转字符串复制
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<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 type { ColDef, GridApi, GridReadyEvent } from 'ag-grid-community'
|
||||
import { taskList } from '@/sql'
|
||||
@ -9,6 +9,7 @@ import { formatThousands, formatThousandsFlexible } from '@/lib/numberFormat'
|
||||
import { parseNumberOrNull } from '@/lib/number'
|
||||
import { syncPricingTotalToZxFw } from '@/lib/zxFwPricingSync'
|
||||
import { useZxFwPricingStore } from '@/pinia/zxFwPricing'
|
||||
import { useKvStore } from '@/pinia/kv'
|
||||
import { loadConsultCategoryFactorMap } from '@/lib/xmFactorDefaults'
|
||||
import MethodUnavailableNotice from '@/components/shared/MethodUnavailableNotice.vue'
|
||||
|
||||
@ -42,6 +43,7 @@ const props = defineProps<{
|
||||
serviceId: string | number
|
||||
}>()
|
||||
const zxFwPricingStore = useZxFwPricingStore()
|
||||
const kvStore = useKvStore()
|
||||
const DB_KEY = computed(() => `gzlF-${props.contractId}-${props.serviceId}`)
|
||||
const HT_CONSULT_FACTOR_KEY = computed(() => `ht-consult-category-factor-v1-${props.contractId}`)
|
||||
const PRICING_CLEAR_SKIP_PREFIX = 'pricing-clear-skip:'
|
||||
@ -193,7 +195,7 @@ const mergeWithDictRows = (rowsFromDb: DetailRow[] | undefined): DetailRow[] =>
|
||||
budgetAdoptedUnitPrice:
|
||||
typeof fromDb.budgetAdoptedUnitPrice === 'number' ? fromDb.budgetAdoptedUnitPrice : null,
|
||||
consultCategoryFactor:
|
||||
typeof fromDb.consultCategoryFactor === 'number' ? fromDb.consultCategoryFactor : null,
|
||||
typeof fromDb.consultCategoryFactor === 'number' ? fromDb.consultCategoryFactor : getDefaultConsultCategoryFactor(),
|
||||
serviceFee: typeof fromDb.serviceFee === 'number' ? fromDb.serviceFee : null,
|
||||
remark: typeof fromDb.remark === 'string' ? fromDb.remark : hasRemark ? '' : row.remark
|
||||
}
|
||||
@ -304,8 +306,9 @@ const columnDefs: ColDef<DetailRow>[] = [
|
||||
minWidth: 170,
|
||||
flex: 1,
|
||||
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: {
|
||||
'ag-right-aligned-cell': () => true,
|
||||
'editable-cell-empty': params =>
|
||||
!params.node?.group &&
|
||||
!params.node?.rowPinned &&
|
||||
@ -365,8 +368,10 @@ const columnDefs: ColDef<DetailRow>[] = [
|
||||
headerClass: 'ag-right-aligned-header',
|
||||
minWidth: 150,
|
||||
flex: 1,
|
||||
cellClass: 'ag-right-aligned-cell',
|
||||
editable: false,
|
||||
cellClassRules: {
|
||||
'ag-right-aligned-cell': () => true
|
||||
},
|
||||
valueGetter: params => (params.node?.rowPinned ? params.data?.serviceFee ?? null : calcServiceFee(params.data)),
|
||||
aggFunc: decimalAggSum,
|
||||
valueFormatter: params => {
|
||||
@ -414,7 +419,7 @@ const sumNullableBy = <T>(rows: T[], pick: (row: T) => unknown): number | 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(() => [
|
||||
{
|
||||
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 () => {
|
||||
try {
|
||||
if (!isWorkloadMethodApplicable.value) {
|
||||
@ -505,6 +541,12 @@ const handleGridReady = (event: GridReadyEvent<DetailRow>) => {
|
||||
|
||||
onMounted(async () => {
|
||||
await loadFromIndexedDB()
|
||||
await syncLinkedConsultFactorFromHt()
|
||||
})
|
||||
|
||||
onActivated(async () => {
|
||||
await loadFromIndexedDB()
|
||||
await syncLinkedConsultFactorFromHt()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
@ -512,6 +554,10 @@ onBeforeUnmount(() => {
|
||||
gridApi.value = null
|
||||
void saveToIndexedDB()
|
||||
})
|
||||
|
||||
watch(linkedConsultFactorSignature, () => {
|
||||
void syncLinkedConsultFactorFromHt()
|
||||
})
|
||||
const processCellForClipboard = (params: any) => {
|
||||
if (Array.isArray(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,
|
||||
cellClass: params => (!params.node?.group && !params.node?.rowPinned ? 'editable-cell-line' : ''),
|
||||
cellClassRules: {
|
||||
'ag-right-aligned-cell':()=>true,
|
||||
'editable-cell-empty': params =>
|
||||
!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,
|
||||
cellClass: params =>
|
||||
!params.node?.group && !params.node?.rowPinned
|
||||
? 'ag-right-aligned-cell editable-cell-line'
|
||||
: 'ag-right-aligned-cell',
|
||||
? 'editable-cell-line'
|
||||
: '',
|
||||
cellClassRules: {
|
||||
'ag-right-aligned-cell': () => true,
|
||||
'editable-cell-empty': params =>
|
||||
!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',
|
||||
minWidth: 120,
|
||||
flex: 1,
|
||||
cellClass: 'ag-right-aligned-cell',
|
||||
editable: false,
|
||||
cellClassRules: {
|
||||
'ag-right-aligned-cell': () => true
|
||||
},
|
||||
aggFunc: decimalAggSum,
|
||||
valueGetter: params => (params.node?.rowPinned ? params.data?.serviceBudget ?? null : calcServiceBudget(params.data)),
|
||||
valueFormatter: params => {
|
||||
@ -446,7 +450,7 @@ const sumNullableBy = <T>(rows: T[], pick: (row: T) => unknown): number | 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(() => [
|
||||
{
|
||||
id: 'pinned-total-row',
|
||||
|
||||
@ -309,11 +309,12 @@ const columnDefs: ColDef<FeeRow>[] = [
|
||||
minWidth: 100,
|
||||
flex: 1,
|
||||
headerClass: 'ag-right-aligned-header',
|
||||
cellClass: 'ag-right-aligned-cell editable-cell-line',
|
||||
cellClass: 'editable-cell-line',
|
||||
editable: params => !isSubtotalRow(params.data),
|
||||
valueParser: params => parseNumberOrNull(params.newValue, { precision: 3 }),
|
||||
valueFormatter: formatEditableQuantity,
|
||||
cellClassRules: {
|
||||
'ag-right-aligned-cell': () => true,
|
||||
'editable-cell-empty': params => params.value == null || params.value === ''
|
||||
}
|
||||
},
|
||||
@ -323,11 +324,12 @@ const columnDefs: ColDef<FeeRow>[] = [
|
||||
minWidth: 120,
|
||||
flex: 1.1,
|
||||
headerClass: 'ag-right-aligned-header',
|
||||
cellClass: 'ag-right-aligned-cell editable-cell-line',
|
||||
cellClass: 'editable-cell-line',
|
||||
editable: params => !isSubtotalRow(params.data),
|
||||
valueParser: params => parseNumberOrNull(params.newValue, { precision: 2 }),
|
||||
valueFormatter: formatEditableUnitPrice,
|
||||
cellClassRules: {
|
||||
'ag-right-aligned-cell': () => true,
|
||||
'editable-cell-empty': params => params.value == null || params.value === ''
|
||||
}
|
||||
},
|
||||
@ -337,8 +339,10 @@ const columnDefs: ColDef<FeeRow>[] = [
|
||||
minWidth: 130,
|
||||
flex: 1.2,
|
||||
headerClass: 'ag-right-aligned-header',
|
||||
cellClass: 'ag-right-aligned-cell',
|
||||
editable: false,
|
||||
cellClassRules: {
|
||||
'ag-right-aligned-cell': () => true
|
||||
},
|
||||
valueFormatter: formatReadonlyBudgetFee
|
||||
},
|
||||
{
|
||||
|
||||
@ -510,10 +510,10 @@ const columnDefs: ColDef<FeeMethodRow>[] = [
|
||||
flex: 1.2,
|
||||
editable: false,
|
||||
headerClass: 'ag-right-aligned-header',
|
||||
cellClass: 'ag-right-aligned-cell',
|
||||
valueParser: params => numericParser(params.newValue),
|
||||
valueFormatter: formatEditableNumber,
|
||||
cellClassRules: {
|
||||
'ag-right-aligned-cell': () => true,
|
||||
'editable-cell-empty': params => params.value == null || params.value === ''
|
||||
}
|
||||
},
|
||||
@ -524,10 +524,10 @@ const columnDefs: ColDef<FeeMethodRow>[] = [
|
||||
flex: 1.2,
|
||||
editable: false,
|
||||
headerClass: 'ag-right-aligned-header',
|
||||
cellClass: 'ag-right-aligned-cell',
|
||||
valueParser: params => numericParser(params.newValue),
|
||||
valueFormatter: formatEditableNumber,
|
||||
cellClassRules: {
|
||||
'ag-right-aligned-cell': () => true,
|
||||
'editable-cell-empty': params => params.value == null || params.value === ''
|
||||
}
|
||||
},
|
||||
@ -538,10 +538,10 @@ const columnDefs: ColDef<FeeMethodRow>[] = [
|
||||
flex: 1.2,
|
||||
editable: false,
|
||||
headerClass: 'ag-right-aligned-header',
|
||||
cellClass: 'ag-right-aligned-cell',
|
||||
valueParser: params => numericParser(params.newValue),
|
||||
valueFormatter: formatEditableNumber,
|
||||
cellClassRules: {
|
||||
'ag-right-aligned-cell': () => true,
|
||||
'editable-cell-empty': params => params.value == null || params.value === ''
|
||||
}
|
||||
},
|
||||
@ -552,7 +552,9 @@ const columnDefs: ColDef<FeeMethodRow>[] = [
|
||||
flex: 1.2,
|
||||
editable: false,
|
||||
headerClass: 'ag-right-aligned-header',
|
||||
cellClass: 'ag-right-aligned-cell',
|
||||
cellClassRules: {
|
||||
'ag-right-aligned-cell': () => true
|
||||
},
|
||||
valueGetter: params => getRowSubtotal(params.data),
|
||||
valueFormatter: formatEditableNumber
|
||||
},
|
||||
|
||||
@ -160,8 +160,10 @@ const columnDefs: ColDef<FactorRow>[] = [
|
||||
minWidth: 86,
|
||||
maxWidth: 100,
|
||||
headerClass: 'ag-right-aligned-header',
|
||||
cellClass: 'ag-right-aligned-cell',
|
||||
flex: 0.9,
|
||||
cellClassRules: {
|
||||
'ag-right-aligned-cell': () => true
|
||||
},
|
||||
valueFormatter: params => formatReadonlyFactor(params.value)
|
||||
},
|
||||
{
|
||||
@ -172,10 +174,11 @@ const columnDefs: ColDef<FactorRow>[] = [
|
||||
headerClass: 'ag-right-aligned-header',
|
||||
cellClass: params => {
|
||||
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,
|
||||
cellClassRules: {
|
||||
'ag-right-aligned-cell': () => true,
|
||||
'editable-cell-empty': params => {
|
||||
const disabled = props.disableBudgetEditWhenStandardNull && params.data?.standardFactor == null
|
||||
return !disabled && (params.value == null || params.value === '')
|
||||
|
||||
@ -338,11 +338,12 @@ const columnDefs: ColDef<DetailRow>[] = [
|
||||
},
|
||||
cellClass: params =>
|
||||
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
|
||||
? 'ag-right-aligned-cell editable-cell-line'
|
||||
: 'ag-right-aligned-cell',
|
||||
? 'editable-cell-line'
|
||||
: '',
|
||||
cellClassRules: {
|
||||
'ag-right-aligned-cell': () => true,
|
||||
'editable-cell-empty': params =>
|
||||
roughCalcEnabled.value && params.node?.rowPinned
|
||||
? 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),
|
||||
cellClass: params =>
|
||||
!params.node?.group && !params.node?.rowPinned && params.data?.hasArea
|
||||
? 'ag-right-aligned-cell editable-cell-line'
|
||||
: 'ag-right-aligned-cell',
|
||||
? 'editable-cell-line'
|
||||
: '',
|
||||
cellClassRules: {
|
||||
'ag-right-aligned-cell': () => true,
|
||||
'editable-cell-empty': params =>
|
||||
!params.node?.group && !params.node?.rowPinned && Boolean(params.data?.hasArea) && (params.value == null || params.value === '')
|
||||
},
|
||||
|
||||
@ -199,7 +199,7 @@ const confirmQuickCalc = async () => {
|
||||
tabStore.enterWorkspace({
|
||||
id: `contract-${QUICK_CONTRACT_ID}`,
|
||||
title: contractName,
|
||||
componentName: 'QuickCalcView',
|
||||
componentName: 'QuickCalcWorkbenchView',
|
||||
props: {
|
||||
contractId: QUICK_CONTRACT_ID,
|
||||
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 {
|
||||
benchmarkBudgetBasicChecked?: unknown
|
||||
benchmarkBudgetOptionalChecked?: unknown
|
||||
basicFormula?: unknown
|
||||
optionalFormula?: unknown
|
||||
budgetFee?: unknown
|
||||
@ -482,6 +484,7 @@ const userGuideSteps: UserGuideStep[] = [
|
||||
const componentMap: Record<string, any> = {
|
||||
ProjectCalcView: markRaw(defineAsyncComponent(() => import('@/components/xm/xmCard.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'))),
|
||||
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 scaleValue = mode === 'cost' ? toFiniteNumber(row.amount) : toFiniteNumber(row.landArea)
|
||||
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
|
||||
? getScaleBudgetFeeSplit({
|
||||
benchmarkBudgetBasic: benchmarkSplit.basic,
|
||||
benchmarkBudgetOptional: benchmarkSplit.optional,
|
||||
benchmarkBudgetBasic,
|
||||
benchmarkBudgetOptional,
|
||||
majorFactor: row.majorFactor,
|
||||
consultCategoryFactor: row.consultCategoryFactor,
|
||||
workStageFactor: row.workStageFactor,
|
||||
workRatio: row.workRatio
|
||||
})
|
||||
: null
|
||||
const basicFee = toFiniteNumber(row.budgetFee) ?? computedSplit?.total ?? null
|
||||
const basicFeeBasic = toFiniteNumber(row.budgetFeeBasic) ?? computedSplit?.basic ?? null
|
||||
const basicFeeOptional = toFiniteNumber(row.budgetFeeOptional) ?? computedSplit?.optional ?? null
|
||||
const basicFee = allUnchecked ? null : (toFiniteNumber(row.budgetFee) ?? computedSplit?.total ?? null)
|
||||
const basicFeeBasic = allUnchecked ? null : (toFiniteNumber(row.budgetFeeBasic) ?? computedSplit?.basic ?? null)
|
||||
const basicFeeOptional = allUnchecked ? null : (toFiniteNumber(row.budgetFeeOptional) ?? computedSplit?.optional ?? null)
|
||||
const basicFormula = typeof row.basicFormula === 'string' && row.basicFormula.trim()
|
||||
? row.basicFormula
|
||||
: (benchmarkSplit?.basicFormula ?? '')
|
||||
: (basicChecked ? (benchmarkSplit?.basicFormula ?? '') : '')
|
||||
const optionalFormula = typeof row.optionalFormula === 'string' && row.optionalFormula.trim()
|
||||
? row.optionalFormula
|
||||
: (benchmarkSplit?.optionalFormula ?? '')
|
||||
: (optionalChecked ? (benchmarkSplit?.optionalFormula ?? '') : '')
|
||||
return {
|
||||
basicFee,
|
||||
basicFeeBasic,
|
||||
@ -1251,13 +1259,7 @@ const buildMethod1 = (rows: ScaleMethodRowLike[] | undefined): ExportMethod1 | n
|
||||
const basicFeeBasic = feeResolved.basicFeeBasic
|
||||
const basicFeeOptional = feeResolved.basicFeeOptional
|
||||
const remark = typeof row.remark === 'string' ? row.remark : ''
|
||||
const hasValue =
|
||||
cost != null ||
|
||||
basicFee != null ||
|
||||
basicFeeBasic != null ||
|
||||
basicFeeOptional != null ||
|
||||
isNonEmptyString(remark)
|
||||
if (!hasValue) return null
|
||||
if (basicFee == null) return null
|
||||
return {
|
||||
proNum,
|
||||
major,
|
||||
@ -1306,13 +1308,7 @@ const buildMethod2 = (rows: ScaleMethodRowLike[] | undefined): ExportMethod2 | n
|
||||
const basicFeeBasic = feeResolved.basicFeeBasic
|
||||
const basicFeeOptional = feeResolved.basicFeeOptional
|
||||
const remark = typeof row.remark === 'string' ? row.remark : ''
|
||||
const hasValue =
|
||||
area != null ||
|
||||
basicFee != null ||
|
||||
basicFeeBasic != null ||
|
||||
basicFeeOptional != null ||
|
||||
isNonEmptyString(remark)
|
||||
if (!hasValue) return null
|
||||
if (basicFee == null) return null
|
||||
return {
|
||||
proNum,
|
||||
major,
|
||||
@ -2149,7 +2145,7 @@ watch(
|
||||
</button>
|
||||
</div>
|
||||
<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"
|
||||
class="app-toolbar-btn shrink-0 cursor-pointer"
|
||||
:disabled="isResetting"
|
||||
@ -2179,7 +2175,7 @@ watch(
|
||||
<input ref="importFileRef" type="file" accept=".zw" class="hidden" @change="importData" />
|
||||
</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"
|
||||
@click="openUserGuide(0)">
|
||||
<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,
|
||||
groupDefaultExpanded: -1,
|
||||
suppressFieldDotNotation: true,
|
||||
enterNavigatesVertically: true,
|
||||
enterNavigatesVerticallyAfterEdit: true,
|
||||
// rowData 更新后通过稳定 ID 维持展开状态和编辑上下文。
|
||||
getRowId: params => {
|
||||
const id = params.data?.id
|
||||
|
||||
@ -76,7 +76,6 @@ export const getScaleBudgetFeeSplit = (params: {
|
||||
const roundedBenchmarkBudget = roundTo(addNumbers(benchmarkBudgetBasic, benchmarkBudgetOptional), 2)
|
||||
const basic = roundTo(toDecimal(benchmarkBudgetBasic).mul(multiplier), 2)
|
||||
const optional = roundTo(toDecimal(benchmarkBudgetOptional).mul(multiplier), 2)
|
||||
|
||||
return {
|
||||
basic,
|
||||
optional,
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { getMajorDictById, getMajorIdAliasMap, getServiceDictById } from '@/sql'
|
||||
import { toFiniteNumberOrNull } from '@/lib/number'
|
||||
import { useKvStore } from '@/pinia/kv'
|
||||
import { useZxFwPricingStore } from '@/pinia/zxFwPricing'
|
||||
|
||||
const CONSULT_CATEGORY_FACTOR_KEY = 'xm-consult-category-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 (
|
||||
storageKey: string,
|
||||
dict: FactorDict,
|
||||
aliases?: Map<string, string>
|
||||
): Promise<Map<string, number | null>> => {
|
||||
const zxFwPricingStore = getZxFwPricingStoreSafely()
|
||||
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)
|
||||
for (const row of data?.detailRows || []) {
|
||||
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 },
|
||||
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 },
|
||||
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 },
|
||||
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 },
|
||||
@ -116,9 +116,9 @@ export const majorList = {
|
||||
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 },
|
||||
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 },
|
||||
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 },
|
||||
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 },
|
||||
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: '实施阶段造价咨询(公路、水运)', 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: '实施阶段造价咨询(铁路)', 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 },
|
||||
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 },
|
||||
@ -674,10 +674,159 @@ export const getServiceDictItemById = (id: string | number): DictItem | undefine
|
||||
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) =>
|
||||
staLine < sv && (endLine == null || sv <= endLine)
|
||||
(staLine === 0 ? staLine <= sv : staLine < sv) && (endLine == null || sv <= endLine)
|
||||
|
||||
// 按分段参数计算基础费用。
|
||||
const calcScaleFee = (params: {
|
||||
@ -730,8 +879,9 @@ export function getBasicFeeFromScale(
|
||||
scaleValue: unknown,
|
||||
scaleType: 'cost' | 'area'
|
||||
): BasicFeeFromScaleResult | null {
|
||||
if (scaleValue == null || scaleValue === '') return null
|
||||
const sv = Number(scaleValue)
|
||||
if (!Number.isFinite(sv) || sv <= 0) return null
|
||||
if (!Number.isFinite(sv) || sv < 0) return null
|
||||
|
||||
if (scaleType === 'cost') {
|
||||
const targetRange = costScaleCal.find(f => inRange(sv, f.staLine, f.endLine))
|
||||
|
||||
@ -365,6 +365,15 @@ input[inputmode='numeric'] {
|
||||
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 */
|
||||
@media (max-width: 1024px) {
|
||||
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