fix someone
This commit is contained in:
parent
c482faacbf
commit
ad4e9cdee0
278
src/components/common/HtFeeGrid.vue
Normal file
278
src/components/common/HtFeeGrid.vue
Normal file
@ -0,0 +1,278 @@
|
||||
<script setup lang="ts">
|
||||
import { onActivated, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { AgGridVue } from 'ag-grid-vue3'
|
||||
import type { ColDef, GridApi, GridOptions, GridReadyEvent } from 'ag-grid-community'
|
||||
import { AG_GRID_LOCALE_CN } from '@ag-grid-community/locale'
|
||||
import localforage from 'localforage'
|
||||
import { myTheme, gridOptions } from '@/lib/diyAgGridOptions'
|
||||
import { parseNumberOrNull } from '@/lib/number'
|
||||
import { formatThousands, formatThousandsFlexible } from '@/lib/numberFormat'
|
||||
import { roundTo, toDecimal } from '@/lib/decimal'
|
||||
|
||||
interface FeeRow {
|
||||
id: string
|
||||
feeItem: string
|
||||
unit: string
|
||||
quantity: number | null
|
||||
unitPrice: number | null
|
||||
budgetFee: number | null
|
||||
remark: string
|
||||
}
|
||||
|
||||
interface FeeGridState {
|
||||
detailRows: FeeRow[]
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
title: string
|
||||
storageKey: string
|
||||
}>()
|
||||
|
||||
const createRowId = () => `fee-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`
|
||||
|
||||
const createDefaultRow = (): FeeRow => ({
|
||||
id: createRowId(),
|
||||
feeItem: '',
|
||||
unit: '',
|
||||
quantity: null,
|
||||
unitPrice: null,
|
||||
budgetFee: null,
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const detailRows = ref<FeeRow[]>([createDefaultRow()])
|
||||
const gridApi = ref<GridApi<FeeRow> | null>(null)
|
||||
|
||||
const formatEditableText = (params: any) => {
|
||||
if (params.value == null || params.value === '') return '点击输入'
|
||||
return String(params.value)
|
||||
}
|
||||
|
||||
const formatEditableQuantity = (params: any) => {
|
||||
if (params.value == null || params.value === '') return '点击输入'
|
||||
return formatThousandsFlexible(params.value, 4)
|
||||
}
|
||||
|
||||
const formatEditableUnitPrice = (params: any) => {
|
||||
if (params.value == null || params.value === '') return '点击输入'
|
||||
return formatThousands(params.value, 2)
|
||||
}
|
||||
|
||||
const formatReadonlyBudgetFee = (params: any) => {
|
||||
if (params.value == null || params.value === '') return ''
|
||||
return formatThousands(params.value, 2)
|
||||
}
|
||||
|
||||
const syncComputedValuesToRows = () => {
|
||||
for (const row of detailRows.value) {
|
||||
if (row.quantity == null || row.unitPrice == null) {
|
||||
row.budgetFee = null
|
||||
continue
|
||||
}
|
||||
row.budgetFee = roundTo(toDecimal(row.quantity).mul(row.unitPrice), 2)
|
||||
}
|
||||
}
|
||||
|
||||
const mergeWithStoredRows = (rowsFromDb: unknown): FeeRow[] => {
|
||||
if (!Array.isArray(rowsFromDb) || rowsFromDb.length === 0) {
|
||||
return [createDefaultRow()]
|
||||
}
|
||||
|
||||
const rows: FeeRow[] = rowsFromDb.map(item => {
|
||||
const row = item as Partial<FeeRow>
|
||||
return {
|
||||
id: typeof row.id === 'string' && row.id ? row.id : createRowId(),
|
||||
feeItem: typeof row.feeItem === 'string' ? row.feeItem : '',
|
||||
unit: typeof row.unit === 'string' ? row.unit : '',
|
||||
quantity: typeof row.quantity === 'number' ? row.quantity : null,
|
||||
unitPrice: typeof row.unitPrice === 'number' ? row.unitPrice : null,
|
||||
budgetFee: typeof row.budgetFee === 'number' ? row.budgetFee : null,
|
||||
remark: typeof row.remark === 'string' ? row.remark : ''
|
||||
}
|
||||
})
|
||||
|
||||
return rows.length > 0 ? rows : [createDefaultRow()]
|
||||
}
|
||||
|
||||
const buildPersistDetailRows = () => {
|
||||
syncComputedValuesToRows()
|
||||
return detailRows.value.map(row => ({ ...row }))
|
||||
}
|
||||
|
||||
const saveToIndexedDB = async () => {
|
||||
try {
|
||||
const payload: FeeGridState = {
|
||||
detailRows: JSON.parse(JSON.stringify(buildPersistDetailRows()))
|
||||
}
|
||||
await localforage.setItem(props.storageKey, payload)
|
||||
} catch (error) {
|
||||
console.error('saveToIndexedDB failed:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const loadFromIndexedDB = async () => {
|
||||
try {
|
||||
const data = await localforage.getItem<FeeGridState>(props.storageKey)
|
||||
detailRows.value = mergeWithStoredRows(data?.detailRows)
|
||||
syncComputedValuesToRows()
|
||||
} catch (error) {
|
||||
console.error('loadFromIndexedDB failed:', error)
|
||||
detailRows.value = [createDefaultRow()]
|
||||
syncComputedValuesToRows()
|
||||
}
|
||||
}
|
||||
|
||||
const columnDefs: ColDef<FeeRow>[] = [
|
||||
{
|
||||
headerName: '费用项',
|
||||
field: 'feeItem',
|
||||
minWidth: 140,
|
||||
flex: 1.4,
|
||||
editable: true,
|
||||
valueFormatter: formatEditableText,
|
||||
cellClass: 'editable-cell-line',
|
||||
cellClassRules: {
|
||||
'editable-cell-empty': params => params.value == null || params.value === ''
|
||||
}
|
||||
},
|
||||
{
|
||||
headerName: '单位',
|
||||
field: 'unit',
|
||||
minWidth: 90,
|
||||
flex: 0.9,
|
||||
editable: true,
|
||||
valueFormatter: formatEditableText,
|
||||
cellClass: 'editable-cell-line',
|
||||
cellClassRules: {
|
||||
'editable-cell-empty': params => params.value == null || params.value === ''
|
||||
}
|
||||
},
|
||||
{
|
||||
headerName: '数量',
|
||||
field: 'quantity',
|
||||
minWidth: 100,
|
||||
flex: 1,
|
||||
headerClass: 'ag-right-aligned-header',
|
||||
cellClass: 'ag-right-aligned-cell editable-cell-line',
|
||||
editable: true,
|
||||
valueParser: params => parseNumberOrNull(params.newValue, { precision: 4 }),
|
||||
valueFormatter: formatEditableQuantity,
|
||||
cellClassRules: {
|
||||
'editable-cell-empty': params => params.value == null || params.value === ''
|
||||
}
|
||||
},
|
||||
{
|
||||
headerName: '单价(元)',
|
||||
field: 'unitPrice',
|
||||
minWidth: 120,
|
||||
flex: 1.1,
|
||||
headerClass: 'ag-right-aligned-header',
|
||||
cellClass: 'ag-right-aligned-cell editable-cell-line',
|
||||
editable: true,
|
||||
valueParser: params => parseNumberOrNull(params.newValue, { precision: 2 }),
|
||||
valueFormatter: formatEditableUnitPrice,
|
||||
cellClassRules: {
|
||||
'editable-cell-empty': params => params.value == null || params.value === ''
|
||||
}
|
||||
},
|
||||
{
|
||||
headerName: '预算费用(元)',
|
||||
field: 'budgetFee',
|
||||
minWidth: 130,
|
||||
flex: 1.2,
|
||||
headerClass: 'ag-right-aligned-header',
|
||||
cellClass: 'ag-right-aligned-cell',
|
||||
editable: false,
|
||||
valueFormatter: formatReadonlyBudgetFee
|
||||
},
|
||||
{
|
||||
headerName: '说明',
|
||||
field: 'remark',
|
||||
minWidth: 170,
|
||||
flex: 2,
|
||||
editable: true,
|
||||
cellEditor: 'agLargeTextCellEditor',
|
||||
wrapText: true,
|
||||
autoHeight: true,
|
||||
cellStyle: { whiteSpace: 'normal', lineHeight: '1.4' },
|
||||
valueFormatter: formatEditableText,
|
||||
cellClass: 'editable-cell-line remark-wrap-cell',
|
||||
cellClassRules: {
|
||||
'editable-cell-empty': params => params.value == null || params.value === ''
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
const detailGridOptions: GridOptions<FeeRow> = {
|
||||
...gridOptions,
|
||||
treeData: false
|
||||
}
|
||||
|
||||
let gridPersistTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
const handleGridReady = (event: GridReadyEvent<FeeRow>) => {
|
||||
gridApi.value = event.api
|
||||
}
|
||||
|
||||
const handleCellValueChanged = () => {
|
||||
syncComputedValuesToRows()
|
||||
gridApi.value?.refreshCells({ columns: ['budgetFee'], force: true })
|
||||
if (gridPersistTimer) clearTimeout(gridPersistTimer)
|
||||
gridPersistTimer = setTimeout(() => {
|
||||
void saveToIndexedDB()
|
||||
}, 300)
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await loadFromIndexedDB()
|
||||
})
|
||||
|
||||
onActivated(() => {
|
||||
void loadFromIndexedDB()
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.storageKey,
|
||||
() => {
|
||||
void loadFromIndexedDB()
|
||||
}
|
||||
)
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (gridPersistTimer) clearTimeout(gridPersistTimer)
|
||||
gridApi.value = null
|
||||
void saveToIndexedDB()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-full min-h-0 flex flex-col">
|
||||
<div class="rounded-lg border bg-card xmMx flex min-h-0 flex-1 flex-col">
|
||||
<div class="flex items-center justify-between border-b px-4 py-3">
|
||||
<h3 class="text-sm font-semibold text-foreground">{{ title }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="ag-theme-quartz h-full min-h-0 w-full flex-1">
|
||||
<AgGridVue
|
||||
:style="{ height: '100%' }"
|
||||
:rowData="detailRows"
|
||||
:columnDefs="columnDefs"
|
||||
:gridOptions="detailGridOptions"
|
||||
:theme="myTheme"
|
||||
:treeData="false"
|
||||
:localeText="AG_GRID_LOCALE_CN"
|
||||
:tooltipShowDelay="500"
|
||||
:headerHeight="50"
|
||||
:suppressColumnVirtualisation="true"
|
||||
:suppressRowVirtualisation="true"
|
||||
:cellSelection="{ handle: { mode: 'range' } }"
|
||||
:enableClipboard="true"
|
||||
:undoRedoCellEditing="true"
|
||||
:undoRedoCellEditingLimit="20"
|
||||
@grid-ready="handleGridReady"
|
||||
@cell-value-changed="handleCellValueChanged"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -57,7 +57,7 @@ const CONTRACT_KEY_PREFIX = 'ht-info-v3-'
|
||||
const SERVICE_KEY_PREFIX = 'zxFW-'
|
||||
const CONTRACT_CONSULT_FACTOR_KEY_PREFIX = 'ht-consult-category-factor-v1-'
|
||||
const CONTRACT_MAJOR_FACTOR_KEY_PREFIX = 'ht-major-factor-v1-'
|
||||
const PRICING_KEY_PREFIXES = ['tzGMF-', 'ydGMF-', 'gzlF-', 'hourlyPricing-']
|
||||
const PRICING_KEY_PREFIXES = ['tzGMF-', 'ydGMF-', 'gzlF-', 'hourlyPricing-', 'htExtraFee-']
|
||||
const PROJECT_INFO_KEY = 'xm-base-info-v1'
|
||||
|
||||
|
||||
|
||||
14
src/components/views/HtAdditionalWorkFee.vue
Normal file
14
src/components/views/HtAdditionalWorkFee.vue
Normal file
@ -0,0 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import HtFeeGrid from '@/components/common/HtFeeGrid.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
contractId: string
|
||||
}>()
|
||||
|
||||
const STORAGE_KEY = computed(() => `htExtraFee-${props.contractId}-additional-work`)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<HtFeeGrid title="附加工作费" :storageKey="STORAGE_KEY" />
|
||||
</template>
|
||||
14
src/components/views/HtReserveFee.vue
Normal file
14
src/components/views/HtReserveFee.vue
Normal file
@ -0,0 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import HtFeeGrid from '@/components/common/HtFeeGrid.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
contractId: string
|
||||
}>()
|
||||
|
||||
const STORAGE_KEY = computed(() => `htExtraFee-${props.contractId}-reserve`)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<HtFeeGrid title="预备费" :storageKey="STORAGE_KEY" />
|
||||
</template>
|
||||
@ -89,11 +89,43 @@ const majorFactorView = markRaw(
|
||||
})
|
||||
);
|
||||
|
||||
const additionalWorkFeeView = markRaw(
|
||||
defineComponent({
|
||||
name: 'HtAdditionalWorkFeeWithProps',
|
||||
setup() {
|
||||
const AsyncHtAdditionalWorkFee = defineAsyncComponent({
|
||||
loader: () => import('@/components/views/HtAdditionalWorkFee.vue'),
|
||||
onError: (err) => {
|
||||
console.error('加载 HtAdditionalWorkFee 组件失败:', err);
|
||||
}
|
||||
});
|
||||
return () => h(AsyncHtAdditionalWorkFee, { contractId: props.contractId });
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
const reserveFeeView = markRaw(
|
||||
defineComponent({
|
||||
name: 'HtReserveFeeWithProps',
|
||||
setup() {
|
||||
const AsyncHtReserveFee = defineAsyncComponent({
|
||||
loader: () => import('@/components/views/HtReserveFee.vue'),
|
||||
onError: (err) => {
|
||||
console.error('加载 HtReserveFee 组件失败:', err);
|
||||
}
|
||||
});
|
||||
return () => h(AsyncHtReserveFee, { contractId: props.contractId });
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// 4. 给分类数组添加严格类型标注
|
||||
const xmCategories: XmCategoryItem[] = [
|
||||
{ key: 'info', label: '规模信息', component: htView },
|
||||
{ key: 'consult-category-factor', label: '咨询分类系数', component: consultCategoryFactorView },
|
||||
{ key: 'major-factor', label: '工程专业系数', component: majorFactorView },
|
||||
{ key: 'additional-work-fee', label: '附加工作费', component: additionalWorkFeeView },
|
||||
{ key: 'reserve-fee', label: '预备费', component: reserveFeeView },
|
||||
{ key: 'contract', label: '咨询服务', component: zxfwView },
|
||||
|
||||
];
|
||||
|
||||
@ -43,6 +43,13 @@ const DB_KEY = 'xm-base-info-v1'
|
||||
const DEFAULT_PROJECT_NAME = 'xxx造价咨询服务'
|
||||
const INDUSTRY_HINT_TEXT = '变更需要重置后重新选择'
|
||||
const PROJECT_INIT_CHANGED_EVENT = 'xm-project-init-changed'
|
||||
const getTodayDateString = () => {
|
||||
const now = new Date()
|
||||
const year = String(now.getFullYear())
|
||||
const month = String(now.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(now.getDate()).padStart(2, '0')
|
||||
return `${year}-${month}-${day}`
|
||||
}
|
||||
|
||||
const isProjectInitialized = ref(false)
|
||||
const showCreateDialog = ref(false)
|
||||
@ -53,7 +60,7 @@ const projectIndustry = ref('')
|
||||
const preparedBy = ref('')
|
||||
const reviewedBy = ref('')
|
||||
const preparedCompany = ref('')
|
||||
const preparedDate = ref('')
|
||||
const preparedDate = ref(getTodayDateString())
|
||||
const preparedDatePickerValue = ref<any>(undefined)
|
||||
|
||||
const normalizeDateString = (value: unknown): string => {
|
||||
@ -123,7 +130,7 @@ const loadFromIndexedDB = async () => {
|
||||
preparedBy.value = typeof data.preparedBy === 'string' ? data.preparedBy : ''
|
||||
reviewedBy.value = typeof data.reviewedBy === 'string' ? data.reviewedBy : ''
|
||||
preparedCompany.value = typeof data.preparedCompany === 'string' ? data.preparedCompany : ''
|
||||
preparedDate.value = normalizeDateString(data.preparedDate)
|
||||
preparedDate.value = normalizeDateString(data.preparedDate) || getTodayDateString()
|
||||
syncPreparedDatePickerFromString()
|
||||
return
|
||||
}
|
||||
@ -134,7 +141,7 @@ const loadFromIndexedDB = async () => {
|
||||
preparedBy.value = ''
|
||||
reviewedBy.value = ''
|
||||
preparedCompany.value = ''
|
||||
preparedDate.value = ''
|
||||
preparedDate.value = getTodayDateString()
|
||||
syncPreparedDatePickerFromString()
|
||||
} catch (error) {
|
||||
console.error('loadFromIndexedDB failed:', error)
|
||||
@ -143,7 +150,7 @@ const loadFromIndexedDB = async () => {
|
||||
preparedBy.value = ''
|
||||
reviewedBy.value = ''
|
||||
preparedCompany.value = ''
|
||||
preparedDate.value = ''
|
||||
preparedDate.value = getTodayDateString()
|
||||
syncPreparedDatePickerFromString()
|
||||
}
|
||||
}
|
||||
@ -182,7 +189,7 @@ const createProject = async () => {
|
||||
preparedBy.value = ''
|
||||
reviewedBy.value = ''
|
||||
preparedCompany.value = ''
|
||||
preparedDate.value = ''
|
||||
preparedDate.value = getTodayDateString()
|
||||
syncPreparedDatePickerFromString()
|
||||
isProjectInitialized.value = true
|
||||
showCreateDialog.value = false
|
||||
@ -320,7 +327,7 @@ onMounted(async () => {
|
||||
</template>
|
||||
</div>
|
||||
<DatePickerTrigger as-child>
|
||||
<button type="button" class="inline-flex h-6 w-6 items-center justify-center text-muted-foreground">
|
||||
<button type="button" class="cursor-pointer inline-flex h-6 w-6 items-center justify-center text-muted-foreground">
|
||||
<CalendarIcon class="h-4 w-4" />
|
||||
</button>
|
||||
</DatePickerTrigger>
|
||||
@ -333,7 +340,7 @@ onMounted(async () => {
|
||||
<DatePickerPrev as-child>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex h-9 w-9 items-center justify-center rounded-md border text-sm transition hover:bg-muted"
|
||||
class=" cursor-potiner inline-flex h-9 w-9 items-center justify-center rounded-md border text-sm transition hover:bg-muted"
|
||||
>
|
||||
‹
|
||||
</button>
|
||||
@ -342,7 +349,7 @@ onMounted(async () => {
|
||||
<DatePickerNext as-child>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex h-9 w-9 items-center justify-center rounded-md border text-sm transition hover:bg-muted"
|
||||
class="cursor-potiner inline-flex h-9 w-9 items-center justify-center rounded-md border text-sm transition hover:bg-muted"
|
||||
>
|
||||
›
|
||||
</button>
|
||||
@ -380,7 +387,7 @@ onMounted(async () => {
|
||||
<DatePickerCellTrigger
|
||||
:day="dateValue"
|
||||
:month="month.value"
|
||||
class="h-full w-full rounded-md border border-transparent bg-transparent text-base outline-none transition hover:bg-muted data-[outside-view]:text-muted-foreground/40 data-[selected]:border-primary data-[selected]:bg-transparent data-[selected]:text-foreground data-[disabled]:opacity-40 data-[unavailable]:text-muted-foreground/40"
|
||||
class="cursor-pointer h-full w-full rounded-md border border-transparent bg-transparent text-base outline-none transition hover:bg-muted data-[outside-view]:text-muted-foreground/40 data-[selected]:border-primary data-[selected]:bg-transparent data-[selected]:text-foreground data-[disabled]:opacity-40 data-[unavailable]:text-muted-foreground/40"
|
||||
>
|
||||
{{ dateValue.day }}
|
||||
</DatePickerCellTrigger>
|
||||
@ -394,10 +401,11 @@ onMounted(async () => {
|
||||
<DatePickerClose as-child>
|
||||
<button
|
||||
type="button"
|
||||
class="h-8 rounded-md border px-3 text-xs text-foreground transition hover:bg-muted"
|
||||
class="cursor-pointer h-8 rounded-md border px-3 text-xs text-foreground transition hover:bg-muted mr-2"
|
||||
>
|
||||
关闭
|
||||
确认
|
||||
</button>
|
||||
|
||||
</DatePickerClose>
|
||||
</div>
|
||||
</DatePickerContent>
|
||||
|
||||
@ -434,7 +434,7 @@ const handleCellValueChanged = () => {
|
||||
if (gridPersistTimer) clearTimeout(gridPersistTimer)
|
||||
gridPersistTimer = setTimeout(() => {
|
||||
void saveToIndexedDB()
|
||||
}, 1000)
|
||||
}, 300)
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
|
||||
@ -55,10 +55,14 @@ interface DetailRow {
|
||||
benchmarkBudget: number | null
|
||||
benchmarkBudgetBasic: number | null
|
||||
benchmarkBudgetOptional: number | null
|
||||
benchmarkBudgetBasicChecked: boolean
|
||||
benchmarkBudgetOptionalChecked: boolean
|
||||
basicFormula: string
|
||||
optionalFormula: string
|
||||
consultCategoryFactor: number | null
|
||||
majorFactor: number | null
|
||||
workStageFactor: number | null
|
||||
workRatio: number | null
|
||||
budgetFee: number | null
|
||||
budgetFeeBasic: number | null
|
||||
budgetFeeOptional: number | null
|
||||
@ -80,6 +84,8 @@ const props = defineProps<{
|
||||
}>()
|
||||
const DB_KEY = computed(() => `tzGMF-${props.contractId}-${props.serviceId}`)
|
||||
const HT_DB_KEY = computed(() => `ht-info-v3-${props.contractId}`)
|
||||
const HT_CONSULT_FACTOR_KEY = computed(() => `ht-consult-category-factor-v1-${props.contractId}`)
|
||||
const HT_MAJOR_FACTOR_KEY = computed(() => `ht-major-factor-v1-${props.contractId}`)
|
||||
const BASE_INFO_KEY = 'xm-base-info-v1'
|
||||
const activeIndustryCode = ref('')
|
||||
const PRICING_CLEAR_SKIP_PREFIX = 'pricing-clear-skip:'
|
||||
@ -97,8 +103,8 @@ const getDefaultMajorFactorById = (id: string): number | null => majorFactorMap.
|
||||
|
||||
const loadFactorDefaults = async () => {
|
||||
const [consultMap, majorMap] = await Promise.all([
|
||||
loadConsultCategoryFactorMap(),
|
||||
loadMajorFactorMap()
|
||||
loadConsultCategoryFactorMap(HT_CONSULT_FACTOR_KEY.value),
|
||||
loadMajorFactorMap(HT_MAJOR_FACTOR_KEY.value)
|
||||
])
|
||||
consultCategoryFactorMap.value = consultMap
|
||||
majorFactorMap.value = majorMap
|
||||
@ -216,10 +222,14 @@ const buildDefaultRows = (): DetailRow[] => {
|
||||
benchmarkBudget: null,
|
||||
benchmarkBudgetBasic: null,
|
||||
benchmarkBudgetOptional: null,
|
||||
benchmarkBudgetBasicChecked: true,
|
||||
benchmarkBudgetOptionalChecked: true,
|
||||
basicFormula: '',
|
||||
optionalFormula: '',
|
||||
consultCategoryFactor: null,
|
||||
majorFactor: null,
|
||||
workStageFactor: 1,
|
||||
workRatio: 100,
|
||||
budgetFee: null,
|
||||
budgetFeeBasic: null,
|
||||
budgetFeeOptional: null,
|
||||
@ -239,10 +249,14 @@ type SourceRow = Pick<DetailRow, 'id'> &
|
||||
| 'benchmarkBudget'
|
||||
| 'benchmarkBudgetBasic'
|
||||
| 'benchmarkBudgetOptional'
|
||||
| 'benchmarkBudgetBasicChecked'
|
||||
| 'benchmarkBudgetOptionalChecked'
|
||||
| 'basicFormula'
|
||||
| 'optionalFormula'
|
||||
| 'consultCategoryFactor'
|
||||
| 'majorFactor'
|
||||
| 'workStageFactor'
|
||||
| 'workRatio'
|
||||
| 'budgetFee'
|
||||
| 'budgetFeeBasic'
|
||||
| 'budgetFeeOptional'
|
||||
@ -277,6 +291,10 @@ const mergeWithDictRows = (
|
||||
benchmarkBudget: typeof fromDb.benchmarkBudget === 'number' ? fromDb.benchmarkBudget : null,
|
||||
benchmarkBudgetBasic: typeof fromDb.benchmarkBudgetBasic === 'number' ? fromDb.benchmarkBudgetBasic : null,
|
||||
benchmarkBudgetOptional: typeof fromDb.benchmarkBudgetOptional === 'number' ? fromDb.benchmarkBudgetOptional : null,
|
||||
benchmarkBudgetBasicChecked:
|
||||
typeof fromDb.benchmarkBudgetBasicChecked === 'boolean' ? fromDb.benchmarkBudgetBasicChecked : true,
|
||||
benchmarkBudgetOptionalChecked:
|
||||
typeof fromDb.benchmarkBudgetOptionalChecked === 'boolean' ? fromDb.benchmarkBudgetOptionalChecked : true,
|
||||
basicFormula: typeof fromDb.basicFormula === 'string' ? fromDb.basicFormula : '',
|
||||
optionalFormula: typeof fromDb.optionalFormula === 'string' ? fromDb.optionalFormula : '',
|
||||
consultCategoryFactor:
|
||||
@ -295,6 +313,8 @@ const mergeWithDictRows = (
|
||||
: hasMajorFactor
|
||||
? null
|
||||
: getDefaultMajorFactorById(row.id),
|
||||
workStageFactor: typeof fromDb.workStageFactor === 'number' ? fromDb.workStageFactor : row.workStageFactor,
|
||||
workRatio: typeof fromDb.workRatio === 'number' ? fromDb.workRatio : row.workRatio,
|
||||
budgetFee: typeof fromDb.budgetFee === 'number' ? fromDb.budgetFee : null,
|
||||
budgetFeeBasic: typeof fromDb.budgetFeeBasic === 'number' ? fromDb.budgetFeeBasic : null,
|
||||
budgetFeeOptional: typeof fromDb.budgetFeeOptional === 'number' ? fromDb.budgetFeeOptional : null,
|
||||
@ -335,10 +355,46 @@ const formatReadonlyMoney = (params: any) => {
|
||||
return formatThousands(roundTo(params.value, 2))
|
||||
}
|
||||
|
||||
type BudgetCheckField = 'benchmarkBudgetBasicChecked' | 'benchmarkBudgetOptionalChecked'
|
||||
|
||||
const createBudgetCellRendererWithCheck = (checkField: BudgetCheckField) => (params: any) => {
|
||||
const valueText = formatReadonlyMoney(params)
|
||||
const hasValue = params.value != null && params.value !== ''
|
||||
if (params.node?.group || params.node?.rowPinned || !params.data || !hasValue) {
|
||||
return valueText
|
||||
}
|
||||
|
||||
const wrapper = document.createElement('div')
|
||||
wrapper.style.display = 'flex'
|
||||
wrapper.style.alignItems = 'center'
|
||||
wrapper.style.justifyContent = 'flex-end'
|
||||
wrapper.style.gap = '6px'
|
||||
wrapper.style.width = '100%'
|
||||
|
||||
const checkbox = document.createElement('input')
|
||||
checkbox.type = 'checkbox'
|
||||
checkbox.className = 'cursor-pointer'
|
||||
|
||||
checkbox.checked = params.data[checkField] !== false
|
||||
checkbox.addEventListener('click', event => event.stopPropagation())
|
||||
checkbox.addEventListener('change', () => {
|
||||
params.data[checkField] = checkbox.checked
|
||||
handleCellValueChanged()
|
||||
})
|
||||
|
||||
const valueSpan = document.createElement('span')
|
||||
valueSpan.textContent = valueText
|
||||
wrapper.append(checkbox, valueSpan)
|
||||
|
||||
return wrapper
|
||||
}
|
||||
|
||||
const getBenchmarkBudgetSplitByAmount = (row?: Pick<DetailRow, 'amount'>) =>
|
||||
getBenchmarkBudgetSplitByScale(row?.amount, 'cost')
|
||||
|
||||
const getBudgetFee = (row?: Pick<DetailRow, 'amount' | 'majorFactor' | 'consultCategoryFactor'>) => {
|
||||
const getBudgetFee = (
|
||||
row?: Pick<DetailRow, 'amount' | 'majorFactor' | 'consultCategoryFactor' | 'workStageFactor' | 'workRatio'>
|
||||
) => {
|
||||
const benchmarkBudgetSplit = getBenchmarkBudgetSplitByAmount(row)
|
||||
if (!benchmarkBudgetSplit) return null
|
||||
|
||||
@ -346,19 +402,25 @@ const getBudgetFee = (row?: Pick<DetailRow, 'amount' | 'majorFactor' | 'consultC
|
||||
benchmarkBudgetBasic: benchmarkBudgetSplit.basic,
|
||||
benchmarkBudgetOptional: benchmarkBudgetSplit.optional,
|
||||
majorFactor: row?.majorFactor,
|
||||
consultCategoryFactor: row?.consultCategoryFactor
|
||||
consultCategoryFactor: row?.consultCategoryFactor,
|
||||
workStageFactor: row?.workStageFactor,
|
||||
workRatio: row?.workRatio
|
||||
})
|
||||
return splitBudgetFee ? splitBudgetFee.total : null
|
||||
}
|
||||
|
||||
const getBudgetFeeSplit = (row?: Pick<DetailRow, 'amount' | 'majorFactor' | 'consultCategoryFactor'>) => {
|
||||
const getBudgetFeeSplit = (
|
||||
row?: Pick<DetailRow, 'amount' | 'majorFactor' | 'consultCategoryFactor' | 'workStageFactor' | 'workRatio'>
|
||||
) => {
|
||||
const benchmarkBudgetSplit = getBenchmarkBudgetSplitByAmount(row)
|
||||
if (!benchmarkBudgetSplit) return null
|
||||
return getScaleBudgetFeeSplit({
|
||||
benchmarkBudgetBasic: benchmarkBudgetSplit.basic,
|
||||
benchmarkBudgetOptional: benchmarkBudgetSplit.optional,
|
||||
majorFactor: row?.majorFactor,
|
||||
consultCategoryFactor: row?.consultCategoryFactor
|
||||
consultCategoryFactor: row?.consultCategoryFactor,
|
||||
workStageFactor: row?.workStageFactor,
|
||||
workRatio: row?.workRatio
|
||||
})
|
||||
}
|
||||
|
||||
@ -367,7 +429,7 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
|
||||
headerName: '造价金额(万元)',
|
||||
field: 'amount',
|
||||
headerClass: 'ag-right-aligned-header',
|
||||
minWidth: 100,
|
||||
minWidth: 90,
|
||||
flex: 2,
|
||||
|
||||
editable: params => !params.node?.group && !params.node?.rowPinned && Boolean(params.data?.hasCost),
|
||||
@ -383,37 +445,6 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
|
||||
valueParser: params => parseNumberOrNull(params.newValue, { precision: 2 }),
|
||||
valueFormatter: formatEditableMoney
|
||||
},
|
||||
|
||||
{
|
||||
headerName: '咨询分类系数',
|
||||
field: 'consultCategoryFactor',
|
||||
width: 80,
|
||||
minWidth: 70,
|
||||
maxWidth: 90,
|
||||
editable: params => !params.node?.group && !params.node?.rowPinned,
|
||||
cellClass: params => (!params.node?.group && !params.node?.rowPinned ? 'editable-cell-line' : ''),
|
||||
cellClassRules: {
|
||||
'editable-cell-empty': params =>
|
||||
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
|
||||
},
|
||||
valueParser: params => parseNumberOrNull(params.newValue, { precision: 2 }),
|
||||
valueFormatter: formatConsultCategoryFactor
|
||||
},
|
||||
{
|
||||
headerName: '专业系数',
|
||||
field: 'majorFactor',
|
||||
width: 80,
|
||||
minWidth: 70,
|
||||
maxWidth: 90,
|
||||
editable: params => !params.node?.group && !params.node?.rowPinned,
|
||||
cellClass: params => (!params.node?.group && !params.node?.rowPinned ? 'editable-cell-line' : ''),
|
||||
cellClassRules: {
|
||||
'editable-cell-empty': params =>
|
||||
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
|
||||
},
|
||||
valueParser: params => parseNumberOrNull(params.newValue, { precision: 2 }),
|
||||
valueFormatter: formatMajorFactor
|
||||
},
|
||||
{
|
||||
headerName: '基准预算(元)',
|
||||
marryChildren: true,
|
||||
@ -431,6 +462,7 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
|
||||
params.node?.rowPinned
|
||||
? params.data?.benchmarkBudgetBasic ?? null
|
||||
: getBenchmarkBudgetSplitByAmount(params.data)?.basic ?? null,
|
||||
cellRenderer: createBudgetCellRendererWithCheck('benchmarkBudgetBasicChecked'),
|
||||
valueFormatter: formatReadonlyMoney
|
||||
},
|
||||
{
|
||||
@ -446,6 +478,22 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
|
||||
params.node?.rowPinned
|
||||
? params.data?.benchmarkBudgetOptional ?? null
|
||||
: getBenchmarkBudgetSplitByAmount(params.data)?.optional ?? null,
|
||||
cellRenderer: createBudgetCellRendererWithCheck('benchmarkBudgetOptionalChecked'),
|
||||
valueFormatter: formatReadonlyMoney
|
||||
},
|
||||
{
|
||||
headerName: '小计',
|
||||
field: 'benchmarkBudget',
|
||||
colId: 'benchmarkBudgetTotal',
|
||||
headerClass: 'ag-right-aligned-header',
|
||||
minWidth: 100,
|
||||
flex: 1,
|
||||
cellClass: 'ag-right-aligned-cell',
|
||||
aggFunc: decimalAggSum,
|
||||
valueGetter: params =>
|
||||
params.node?.rowPinned
|
||||
? params.data?.benchmarkBudget ?? null
|
||||
: getBenchmarkBudgetSplitByAmount(params.data)?.total ?? null,
|
||||
valueFormatter: formatReadonlyMoney
|
||||
}
|
||||
]
|
||||
@ -455,41 +503,71 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
|
||||
marryChildren: true,
|
||||
children: [
|
||||
{
|
||||
headerName: '基本工作',
|
||||
field: 'budgetFeeBasic',
|
||||
colId: 'budgetFeeBasic',
|
||||
headerClass: 'ag-right-aligned-header',
|
||||
minWidth: 120,
|
||||
headerName: '咨询分类系数',
|
||||
field: 'consultCategoryFactor',
|
||||
colId: 'consultCategoryFactor',
|
||||
minWidth: 80,
|
||||
flex: 1,
|
||||
cellClass: 'ag-right-aligned-cell',
|
||||
aggFunc: decimalAggSum,
|
||||
valueGetter: params =>
|
||||
params.node?.rowPinned
|
||||
? params.data?.budgetFeeBasic ?? null
|
||||
: getBudgetFeeSplit(params.data)?.basic ?? null,
|
||||
valueFormatter: formatReadonlyMoney
|
||||
editable: params => !params.node?.group && !params.node?.rowPinned,
|
||||
cellClass: params => (!params.node?.group && !params.node?.rowPinned ? 'editable-cell-line' : ''),
|
||||
cellClassRules: {
|
||||
'editable-cell-empty': params =>
|
||||
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
|
||||
},
|
||||
valueParser: params => parseNumberOrNull(params.newValue, { precision: 2 }),
|
||||
valueFormatter: formatConsultCategoryFactor
|
||||
},
|
||||
{
|
||||
headerName: '可选工作',
|
||||
field: 'budgetFeeOptional',
|
||||
colId: 'budgetFeeOptional',
|
||||
headerClass: 'ag-right-aligned-header',
|
||||
minWidth: 120,
|
||||
headerName: '专业系数',
|
||||
field: 'majorFactor',
|
||||
colId: 'majorFactor',
|
||||
minWidth: 80,
|
||||
flex: 1,
|
||||
cellClass: 'ag-right-aligned-cell',
|
||||
aggFunc: decimalAggSum,
|
||||
valueGetter: params =>
|
||||
params.node?.rowPinned
|
||||
? params.data?.budgetFeeOptional ?? null
|
||||
: getBudgetFeeSplit(params.data)?.optional ?? null,
|
||||
valueFormatter: formatReadonlyMoney
|
||||
editable: params => !params.node?.group && !params.node?.rowPinned,
|
||||
cellClass: params => (!params.node?.group && !params.node?.rowPinned ? 'editable-cell-line' : ''),
|
||||
cellClassRules: {
|
||||
'editable-cell-empty': params =>
|
||||
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
|
||||
},
|
||||
valueParser: params => parseNumberOrNull(params.newValue, { precision: 2 }),
|
||||
valueFormatter: formatMajorFactor
|
||||
},
|
||||
{
|
||||
headerName: '工作环节系数(编审系数)',
|
||||
field: 'workStageFactor',
|
||||
colId: 'workStageFactor',
|
||||
minWidth: 80,
|
||||
flex: 1,
|
||||
editable: params => !params.node?.group && !params.node?.rowPinned,
|
||||
cellClass: params => (!params.node?.group && !params.node?.rowPinned ? 'editable-cell-line' : ''),
|
||||
cellClassRules: {
|
||||
'editable-cell-empty': params =>
|
||||
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
|
||||
},
|
||||
valueParser: params => parseNumberOrNull(params.newValue, { precision: 2 }),
|
||||
valueFormatter: formatEditableNumber
|
||||
},
|
||||
{
|
||||
headerName: '工作占比(%)',
|
||||
field: 'workRatio',
|
||||
colId: 'workRatio',
|
||||
minWidth: 80,
|
||||
flex: 1,
|
||||
editable: params => !params.node?.group && !params.node?.rowPinned,
|
||||
cellClass: params => (!params.node?.group && !params.node?.rowPinned ? 'editable-cell-line' : ''),
|
||||
cellClassRules: {
|
||||
'editable-cell-empty': params =>
|
||||
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
|
||||
},
|
||||
valueParser: params => parseNumberOrNull(params.newValue, { precision: 2 }),
|
||||
valueFormatter: formatEditableNumber
|
||||
},
|
||||
{
|
||||
headerName: '合计',
|
||||
field: 'budgetFee',
|
||||
colId: 'budgetFeeTotal',
|
||||
headerClass: 'ag-right-aligned-header',
|
||||
minWidth: 130,
|
||||
minWidth: 120,
|
||||
flex: 1,
|
||||
cellClass: 'ag-right-aligned-cell',
|
||||
aggFunc: decimalAggSum,
|
||||
@ -501,7 +579,7 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
|
||||
{
|
||||
headerName: '说明',
|
||||
field: 'remark',
|
||||
minWidth: 180,
|
||||
minWidth: 100,
|
||||
flex: 1.2,
|
||||
cellEditor: 'agLargeTextCellEditor',
|
||||
wrapText: true,
|
||||
@ -570,10 +648,14 @@ const pinnedTopRowData = computed(() => [
|
||||
benchmarkBudget: totalBenchmarkBudget.value,
|
||||
benchmarkBudgetBasic: totalBenchmarkBudgetBasic.value,
|
||||
benchmarkBudgetOptional: totalBenchmarkBudgetOptional.value,
|
||||
benchmarkBudgetBasicChecked: true,
|
||||
benchmarkBudgetOptionalChecked: true,
|
||||
basicFormula: '',
|
||||
optionalFormula: '',
|
||||
consultCategoryFactor: null,
|
||||
majorFactor: null,
|
||||
workStageFactor: null,
|
||||
workRatio: null,
|
||||
budgetFee: totalBudgetFee.value,
|
||||
budgetFeeBasic: totalBudgetFeeBasic.value,
|
||||
budgetFeeOptional: totalBudgetFeeOptional.value,
|
||||
@ -582,30 +664,35 @@ const pinnedTopRowData = computed(() => [
|
||||
}
|
||||
])
|
||||
|
||||
const buildPersistDetailRows = () =>
|
||||
detailRows.value.map(row => {
|
||||
const syncComputedValuesToDetailRows = () => {
|
||||
for (const row of detailRows.value) {
|
||||
const benchmarkBudgetSplit = getBenchmarkBudgetSplitByAmount(row)
|
||||
const budgetFeeSplit = benchmarkBudgetSplit
|
||||
? getScaleBudgetFeeSplit({
|
||||
benchmarkBudgetBasic: benchmarkBudgetSplit.basic,
|
||||
benchmarkBudgetOptional: benchmarkBudgetSplit.optional,
|
||||
majorFactor: row.majorFactor,
|
||||
consultCategoryFactor: row.consultCategoryFactor
|
||||
consultCategoryFactor: row.consultCategoryFactor,
|
||||
workStageFactor: row.workStageFactor,
|
||||
workRatio: row.workRatio
|
||||
})
|
||||
: null
|
||||
|
||||
return {
|
||||
...row,
|
||||
benchmarkBudget: benchmarkBudgetSplit?.total ?? null,
|
||||
benchmarkBudgetBasic: benchmarkBudgetSplit?.basic ?? null,
|
||||
benchmarkBudgetOptional: benchmarkBudgetSplit?.optional ?? null,
|
||||
basicFormula: benchmarkBudgetSplit?.basicFormula ?? '',
|
||||
optionalFormula: benchmarkBudgetSplit?.optionalFormula ?? '',
|
||||
budgetFee: budgetFeeSplit?.total ?? null,
|
||||
budgetFeeBasic: budgetFeeSplit?.basic ?? null,
|
||||
budgetFeeOptional: budgetFeeSplit?.optional ?? null
|
||||
row.benchmarkBudget = benchmarkBudgetSplit?.total ?? null
|
||||
row.benchmarkBudgetBasic = benchmarkBudgetSplit?.basic ?? null
|
||||
row.benchmarkBudgetOptional = benchmarkBudgetSplit?.optional ?? null
|
||||
row.basicFormula = benchmarkBudgetSplit?.basicFormula ?? ''
|
||||
row.optionalFormula = benchmarkBudgetSplit?.optionalFormula ?? ''
|
||||
row.budgetFee = budgetFeeSplit?.total ?? null
|
||||
row.budgetFeeBasic = budgetFeeSplit?.basic ?? null
|
||||
row.budgetFeeOptional = budgetFeeSplit?.optional ?? null
|
||||
}
|
||||
}
|
||||
|
||||
const buildPersistDetailRows = () => {
|
||||
syncComputedValuesToDetailRows()
|
||||
return detailRows.value.map(row => ({ ...row }))
|
||||
}
|
||||
})
|
||||
|
||||
const saveToIndexedDB = async () => {
|
||||
if (shouldSkipPersist()) return
|
||||
@ -637,30 +724,35 @@ const loadFromIndexedDB = async () => {
|
||||
|
||||
|
||||
await ensureFactorDefaultsLoaded()
|
||||
if (shouldForceDefaultLoad()) {
|
||||
const applyContractDefaultRows = async () => {
|
||||
const htData = await localforage.getItem<{ detailRows: SourceRow[] }>(HT_DB_KEY.value)
|
||||
detailRows.value = htData?.detailRows
|
||||
? mergeWithDictRows(htData.detailRows, { includeAmount: false, includeFactorValues: false })
|
||||
: buildDefaultRows()
|
||||
const hasContractRows = Array.isArray(htData?.detailRows) && htData.detailRows.length > 0
|
||||
detailRows.value = hasContractRows
|
||||
? mergeWithDictRows(htData!.detailRows, { includeFactorValues: true })
|
||||
: buildDefaultRows().map(row => ({
|
||||
...row,
|
||||
consultCategoryFactor: getDefaultConsultCategoryFactor(),
|
||||
majorFactor: getDefaultMajorFactorById(row.id)
|
||||
}))
|
||||
syncComputedValuesToDetailRows()
|
||||
}
|
||||
if (shouldForceDefaultLoad()) {
|
||||
await applyContractDefaultRows()
|
||||
return
|
||||
}
|
||||
|
||||
const data = await localforage.getItem<XmInfoState>(DB_KEY.value)
|
||||
if (data) {
|
||||
detailRows.value = mergeWithDictRows(data.detailRows)
|
||||
syncComputedValuesToDetailRows()
|
||||
return
|
||||
}
|
||||
|
||||
const htData = await localforage.getItem<{ detailRows: SourceRow[] }>(HT_DB_KEY.value)
|
||||
if (htData?.detailRows) {
|
||||
detailRows.value = mergeWithDictRows(htData.detailRows, { includeAmount: false, includeFactorValues: false })
|
||||
return
|
||||
}
|
||||
|
||||
detailRows.value = buildDefaultRows()
|
||||
await applyContractDefaultRows()
|
||||
} catch (error) {
|
||||
console.error('loadFromIndexedDB failed:', error)
|
||||
detailRows.value = buildDefaultRows()
|
||||
syncComputedValuesToDetailRows()
|
||||
}
|
||||
}
|
||||
|
||||
@ -687,6 +779,15 @@ const importContractData = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const clearAllData = async () => {
|
||||
try {
|
||||
detailRows.value = buildDefaultRows()
|
||||
await saveToIndexedDB()
|
||||
} catch (error) {
|
||||
console.error('clearAllData failed:', error)
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => pricingPaneReloadStore.getReloadVersion(props.contractId, props.serviceId),
|
||||
(nextVersion, prevVersion) => {
|
||||
@ -702,10 +803,11 @@ let persistTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
let gridPersistTimer: ReturnType<typeof setTimeout> | null = null
|
||||
const handleCellValueChanged = () => {
|
||||
syncComputedValuesToDetailRows()
|
||||
if (gridPersistTimer) clearTimeout(gridPersistTimer)
|
||||
gridPersistTimer = setTimeout(() => {
|
||||
void saveToIndexedDB()
|
||||
}, 1000)
|
||||
}, 300)
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
@ -748,6 +850,29 @@ const processCellFromClipboard = (params: any) => {
|
||||
<div class="rounded-lg border bg-card xmMx flex min-h-0 flex-1 flex-col">
|
||||
<div class="flex items-center justify-between border-b px-4 py-3">
|
||||
<h3 class="text-sm font-semibold text-foreground">投资规模明细</h3>
|
||||
<div class="flex items-center gap-2">
|
||||
<AlertDialogRoot>
|
||||
<AlertDialogTrigger as-child>
|
||||
<Button type="button" variant="outline" size="sm">清空</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogPortal>
|
||||
<AlertDialogOverlay class="fixed inset-0 z-50 bg-black/45" />
|
||||
<AlertDialogContent class="fixed left-1/2 top-1/2 z-50 w-[92vw] max-w-md -translate-x-1/2 -translate-y-1/2 rounded-lg border bg-background p-5 shadow-xl">
|
||||
<AlertDialogTitle class="text-base font-semibold">确认清空当前明细</AlertDialogTitle>
|
||||
<AlertDialogDescription class="mt-2 text-sm text-muted-foreground">
|
||||
将清空当前投资规模明细,是否继续?
|
||||
</AlertDialogDescription>
|
||||
<div class="mt-4 flex items-center justify-end gap-2">
|
||||
<AlertDialogCancel as-child>
|
||||
<Button variant="outline">取消</Button>
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction as-child>
|
||||
<Button variant="destructive" @click="clearAllData">确认清空</Button>
|
||||
</AlertDialogAction>
|
||||
</div>
|
||||
</AlertDialogContent>
|
||||
</AlertDialogPortal>
|
||||
</AlertDialogRoot>
|
||||
<AlertDialogRoot>
|
||||
<AlertDialogTrigger as-child>
|
||||
<Button type="button" variant="outline" size="sm">使用默认数据</Button>
|
||||
@ -771,6 +896,7 @@ const processCellFromClipboard = (params: any) => {
|
||||
</AlertDialogPortal>
|
||||
</AlertDialogRoot>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ag-theme-quartz h-full min-h-0 w-full flex-1">
|
||||
<AgGridVue :style="{ height: '100%' }" :rowData="detailRows" :pinnedTopRowData="pinnedTopRowData"
|
||||
@ -784,5 +910,3 @@ const processCellFromClipboard = (params: any) => {
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
|
||||
@ -56,10 +56,14 @@ interface DetailRow {
|
||||
benchmarkBudget: number | null
|
||||
benchmarkBudgetBasic: number | null
|
||||
benchmarkBudgetOptional: number | null
|
||||
benchmarkBudgetBasicChecked: boolean
|
||||
benchmarkBudgetOptionalChecked: boolean
|
||||
basicFormula: string
|
||||
optionalFormula: string
|
||||
consultCategoryFactor: number | null
|
||||
majorFactor: number | null
|
||||
workStageFactor: number | null
|
||||
workRatio: number | null
|
||||
budgetFee: number | null
|
||||
budgetFeeBasic: number | null
|
||||
budgetFeeOptional: number | null
|
||||
@ -81,6 +85,8 @@ const props = defineProps<{
|
||||
}>()
|
||||
const DB_KEY = computed(() => `ydGMF-${props.contractId}-${props.serviceId}`)
|
||||
const HT_DB_KEY = computed(() => `ht-info-v3-${props.contractId}`)
|
||||
const HT_CONSULT_FACTOR_KEY = computed(() => `ht-consult-category-factor-v1-${props.contractId}`)
|
||||
const HT_MAJOR_FACTOR_KEY = computed(() => `ht-major-factor-v1-${props.contractId}`)
|
||||
const BASE_INFO_KEY = 'xm-base-info-v1'
|
||||
const activeIndustryCode = ref('')
|
||||
const PRICING_CLEAR_SKIP_PREFIX = 'pricing-clear-skip:'
|
||||
@ -99,8 +105,8 @@ const getDefaultMajorFactorById = (id: string): number | null => majorFactorMap.
|
||||
|
||||
const loadFactorDefaults = async () => {
|
||||
const [consultMap, majorMap] = await Promise.all([
|
||||
loadConsultCategoryFactorMap(),
|
||||
loadMajorFactorMap()
|
||||
loadConsultCategoryFactorMap(HT_CONSULT_FACTOR_KEY.value),
|
||||
loadMajorFactorMap(HT_MAJOR_FACTOR_KEY.value)
|
||||
])
|
||||
consultCategoryFactorMap.value = consultMap
|
||||
majorFactorMap.value = majorMap
|
||||
@ -218,10 +224,14 @@ const buildDefaultRows = (): DetailRow[] => {
|
||||
benchmarkBudget: null,
|
||||
benchmarkBudgetBasic: null,
|
||||
benchmarkBudgetOptional: null,
|
||||
benchmarkBudgetBasicChecked: true,
|
||||
benchmarkBudgetOptionalChecked: true,
|
||||
basicFormula: '',
|
||||
optionalFormula: '',
|
||||
consultCategoryFactor: null,
|
||||
majorFactor: null,
|
||||
workStageFactor: 1,
|
||||
workRatio: 100,
|
||||
budgetFee: null,
|
||||
budgetFeeBasic: null,
|
||||
budgetFeeOptional: null,
|
||||
@ -242,10 +252,14 @@ type SourceRow = Pick<DetailRow, 'id'> &
|
||||
| 'benchmarkBudget'
|
||||
| 'benchmarkBudgetBasic'
|
||||
| 'benchmarkBudgetOptional'
|
||||
| 'benchmarkBudgetBasicChecked'
|
||||
| 'benchmarkBudgetOptionalChecked'
|
||||
| 'basicFormula'
|
||||
| 'optionalFormula'
|
||||
| 'consultCategoryFactor'
|
||||
| 'majorFactor'
|
||||
| 'workStageFactor'
|
||||
| 'workRatio'
|
||||
| 'budgetFee'
|
||||
| 'budgetFeeBasic'
|
||||
| 'budgetFeeOptional'
|
||||
@ -281,6 +295,10 @@ const mergeWithDictRows = (
|
||||
benchmarkBudget: typeof fromDb.benchmarkBudget === 'number' ? fromDb.benchmarkBudget : null,
|
||||
benchmarkBudgetBasic: typeof fromDb.benchmarkBudgetBasic === 'number' ? fromDb.benchmarkBudgetBasic : null,
|
||||
benchmarkBudgetOptional: typeof fromDb.benchmarkBudgetOptional === 'number' ? fromDb.benchmarkBudgetOptional : null,
|
||||
benchmarkBudgetBasicChecked:
|
||||
typeof fromDb.benchmarkBudgetBasicChecked === 'boolean' ? fromDb.benchmarkBudgetBasicChecked : true,
|
||||
benchmarkBudgetOptionalChecked:
|
||||
typeof fromDb.benchmarkBudgetOptionalChecked === 'boolean' ? fromDb.benchmarkBudgetOptionalChecked : true,
|
||||
basicFormula: typeof fromDb.basicFormula === 'string' ? fromDb.basicFormula : '',
|
||||
optionalFormula: typeof fromDb.optionalFormula === 'string' ? fromDb.optionalFormula : '',
|
||||
consultCategoryFactor:
|
||||
@ -299,6 +317,8 @@ const mergeWithDictRows = (
|
||||
: hasMajorFactor
|
||||
? null
|
||||
: getDefaultMajorFactorById(row.id),
|
||||
workStageFactor: typeof fromDb.workStageFactor === 'number' ? fromDb.workStageFactor : row.workStageFactor,
|
||||
workRatio: typeof fromDb.workRatio === 'number' ? fromDb.workRatio : row.workRatio,
|
||||
budgetFee: typeof fromDb.budgetFee === 'number' ? fromDb.budgetFee : null,
|
||||
budgetFeeBasic: typeof fromDb.budgetFeeBasic === 'number' ? fromDb.budgetFeeBasic : null,
|
||||
budgetFeeOptional: typeof fromDb.budgetFeeOptional === 'number' ? fromDb.budgetFeeOptional : null,
|
||||
@ -328,10 +348,45 @@ const formatReadonlyMoney = (params: any) => {
|
||||
return formatThousands(roundTo(params.value, 2))
|
||||
}
|
||||
|
||||
type BudgetCheckField = 'benchmarkBudgetBasicChecked' | 'benchmarkBudgetOptionalChecked'
|
||||
|
||||
const createBudgetCellRendererWithCheck = (checkField: BudgetCheckField) => (params: any) => {
|
||||
const valueText = formatReadonlyMoney(params)
|
||||
const hasValue = params.value != null && params.value !== ''
|
||||
if (params.node?.group || params.node?.rowPinned || !params.data || !hasValue) {
|
||||
return valueText
|
||||
}
|
||||
|
||||
const wrapper = document.createElement('div')
|
||||
wrapper.style.display = 'flex'
|
||||
wrapper.style.alignItems = 'center'
|
||||
wrapper.style.justifyContent = 'flex-end'
|
||||
wrapper.style.gap = '6px'
|
||||
wrapper.style.width = '100%'
|
||||
|
||||
const checkbox = document.createElement('input')
|
||||
checkbox.type = 'checkbox'
|
||||
checkbox.className = 'cursor-pointer'
|
||||
checkbox.checked = params.data[checkField] !== false
|
||||
checkbox.addEventListener('click', event => event.stopPropagation())
|
||||
checkbox.addEventListener('change', () => {
|
||||
params.data[checkField] = checkbox.checked
|
||||
handleCellValueChanged()
|
||||
})
|
||||
|
||||
const valueSpan = document.createElement('span')
|
||||
valueSpan.textContent = valueText
|
||||
wrapper.append(checkbox, valueSpan)
|
||||
|
||||
return wrapper
|
||||
}
|
||||
|
||||
const getBenchmarkBudgetSplitByLandArea = (row?: Pick<DetailRow, 'landArea'>) =>
|
||||
getBenchmarkBudgetSplitByScale(row?.landArea, 'area')
|
||||
|
||||
const getBudgetFee = (row?: Pick<DetailRow, 'landArea' | 'majorFactor' | 'consultCategoryFactor'>) => {
|
||||
const getBudgetFee = (
|
||||
row?: Pick<DetailRow, 'landArea' | 'majorFactor' | 'consultCategoryFactor' | 'workStageFactor' | 'workRatio'>
|
||||
) => {
|
||||
const benchmarkBudgetSplit = getBenchmarkBudgetSplitByLandArea(row)
|
||||
if (!benchmarkBudgetSplit) return null
|
||||
|
||||
@ -339,19 +394,25 @@ const getBudgetFee = (row?: Pick<DetailRow, 'landArea' | 'majorFactor' | 'consul
|
||||
benchmarkBudgetBasic: benchmarkBudgetSplit.basic,
|
||||
benchmarkBudgetOptional: benchmarkBudgetSplit.optional,
|
||||
majorFactor: row?.majorFactor,
|
||||
consultCategoryFactor: row?.consultCategoryFactor
|
||||
consultCategoryFactor: row?.consultCategoryFactor,
|
||||
workStageFactor: row?.workStageFactor,
|
||||
workRatio: row?.workRatio
|
||||
})
|
||||
return splitBudgetFee ? splitBudgetFee.total : null
|
||||
}
|
||||
|
||||
const getBudgetFeeSplit = (row?: Pick<DetailRow, 'landArea' | 'majorFactor' | 'consultCategoryFactor'>) => {
|
||||
const getBudgetFeeSplit = (
|
||||
row?: Pick<DetailRow, 'landArea' | 'majorFactor' | 'consultCategoryFactor' | 'workStageFactor' | 'workRatio'>
|
||||
) => {
|
||||
const benchmarkBudgetSplit = getBenchmarkBudgetSplitByLandArea(row)
|
||||
if (!benchmarkBudgetSplit) return null
|
||||
return getScaleBudgetFeeSplit({
|
||||
benchmarkBudgetBasic: benchmarkBudgetSplit.basic,
|
||||
benchmarkBudgetOptional: benchmarkBudgetSplit.optional,
|
||||
majorFactor: row?.majorFactor,
|
||||
consultCategoryFactor: row?.consultCategoryFactor
|
||||
consultCategoryFactor: row?.consultCategoryFactor,
|
||||
workStageFactor: row?.workStageFactor,
|
||||
workRatio: row?.workRatio
|
||||
})
|
||||
}
|
||||
|
||||
@ -371,8 +432,9 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
|
||||
headerName: '用地面积(亩)',
|
||||
field: 'landArea',
|
||||
headerClass: 'ag-right-aligned-header',
|
||||
minWidth: 170,
|
||||
flex: 1,
|
||||
minWidth: 90,
|
||||
|
||||
flex: 2,
|
||||
|
||||
editable: params => !params.node?.group && !params.node?.rowPinned && Boolean(params.data?.hasArea),
|
||||
cellClass: params =>
|
||||
@ -389,13 +451,69 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
|
||||
},
|
||||
valueFormatter: formatEditableFlexibleNumber
|
||||
},
|
||||
|
||||
{
|
||||
headerName: '基准预算(元)',
|
||||
marryChildren: true,
|
||||
children: [
|
||||
{
|
||||
headerName: '基本工作',
|
||||
field: 'benchmarkBudgetBasic',
|
||||
colId: 'benchmarkBudgetBasic',
|
||||
headerClass: 'ag-right-aligned-header',
|
||||
minWidth: 130,
|
||||
flex: 1,
|
||||
cellClass: 'ag-right-aligned-cell',
|
||||
aggFunc: decimalAggSum,
|
||||
valueGetter: params =>
|
||||
params.node?.rowPinned
|
||||
? params.data?.benchmarkBudgetBasic ?? null
|
||||
: getBenchmarkBudgetSplitByLandArea(params.data)?.basic ?? null,
|
||||
cellRenderer: createBudgetCellRendererWithCheck('benchmarkBudgetBasicChecked'),
|
||||
valueFormatter: formatReadonlyMoney
|
||||
},
|
||||
{
|
||||
headerName: '可选工作',
|
||||
field: 'benchmarkBudgetOptional',
|
||||
colId: 'benchmarkBudgetOptional',
|
||||
headerClass: 'ag-right-aligned-header',
|
||||
minWidth: 130,
|
||||
flex: 1,
|
||||
cellClass: 'ag-right-aligned-cell',
|
||||
aggFunc: decimalAggSum,
|
||||
valueGetter: params =>
|
||||
params.node?.rowPinned
|
||||
? params.data?.benchmarkBudgetOptional ?? null
|
||||
: getBenchmarkBudgetSplitByLandArea(params.data)?.optional ?? null,
|
||||
cellRenderer: createBudgetCellRendererWithCheck('benchmarkBudgetOptionalChecked'),
|
||||
valueFormatter: formatReadonlyMoney
|
||||
},
|
||||
{
|
||||
headerName: '小计',
|
||||
field: 'benchmarkBudget',
|
||||
colId: 'benchmarkBudgetTotal',
|
||||
headerClass: 'ag-right-aligned-header',
|
||||
minWidth: 100,
|
||||
flex: 1,
|
||||
cellClass: 'ag-right-aligned-cell',
|
||||
aggFunc: decimalAggSum,
|
||||
valueGetter: params =>
|
||||
params.node?.rowPinned
|
||||
? params.data?.benchmarkBudget ?? null
|
||||
: getBenchmarkBudgetSplitByLandArea(params.data)?.total ?? null,
|
||||
valueFormatter: formatReadonlyMoney
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
headerName: '预算费用',
|
||||
marryChildren: true,
|
||||
children: [
|
||||
{
|
||||
headerName: '咨询分类系数',
|
||||
field: 'consultCategoryFactor',
|
||||
width: 80,
|
||||
minWidth: 70,
|
||||
maxWidth: 90,
|
||||
colId: 'consultCategoryFactor',
|
||||
minWidth: 80,
|
||||
flex: 1,
|
||||
editable: params => !params.node?.group && !params.node?.rowPinned,
|
||||
cellClass: params => (!params.node?.group && !params.node?.rowPinned ? 'editable-cell-line' : ''),
|
||||
cellClassRules: {
|
||||
@ -408,9 +526,9 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
|
||||
{
|
||||
headerName: '专业系数',
|
||||
field: 'majorFactor',
|
||||
width: 80,
|
||||
minWidth: 70,
|
||||
maxWidth: 90,
|
||||
colId: 'majorFactor',
|
||||
minWidth: 80,
|
||||
flex: 1,
|
||||
editable: params => !params.node?.group && !params.node?.rowPinned,
|
||||
cellClass: params => (!params.node?.group && !params.node?.rowPinned ? 'editable-cell-line' : ''),
|
||||
cellClassRules: {
|
||||
@ -421,81 +539,41 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
|
||||
valueFormatter: formatMajorFactor
|
||||
},
|
||||
{
|
||||
headerName: '基准预算(元)',
|
||||
marryChildren: true,
|
||||
children: [
|
||||
{
|
||||
headerName: '基本工作',
|
||||
field: 'benchmarkBudgetBasic',
|
||||
colId: 'benchmarkBudgetBasic',
|
||||
headerClass: 'ag-right-aligned-header',
|
||||
minWidth: 140,
|
||||
headerName: '工作环节系数(编审系数)',
|
||||
field: 'workStageFactor',
|
||||
colId: 'workStageFactor',
|
||||
minWidth: 80,
|
||||
flex: 1,
|
||||
cellClass: 'ag-right-aligned-cell',
|
||||
aggFunc: decimalAggSum,
|
||||
valueGetter: params =>
|
||||
params.node?.rowPinned
|
||||
? params.data?.benchmarkBudgetBasic ?? null
|
||||
: getBenchmarkBudgetSplitByLandArea(params.data)?.basic ?? null,
|
||||
valueFormatter: formatReadonlyMoney
|
||||
editable: params => !params.node?.group && !params.node?.rowPinned,
|
||||
cellClass: params => (!params.node?.group && !params.node?.rowPinned ? 'editable-cell-line' : ''),
|
||||
cellClassRules: {
|
||||
'editable-cell-empty': params =>
|
||||
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
|
||||
},
|
||||
valueParser: params => parseNumberOrNull(params.newValue, { precision: 2 }),
|
||||
valueFormatter: formatEditableNumber
|
||||
},
|
||||
{
|
||||
headerName: '可选工作',
|
||||
field: 'benchmarkBudgetOptional',
|
||||
colId: 'benchmarkBudgetOptional',
|
||||
headerClass: 'ag-right-aligned-header',
|
||||
minWidth: 140,
|
||||
headerName: '工作占比(%)',
|
||||
field: 'workRatio',
|
||||
colId: 'workRatio',
|
||||
minWidth: 80,
|
||||
flex: 1,
|
||||
cellClass: 'ag-right-aligned-cell',
|
||||
aggFunc: decimalAggSum,
|
||||
valueGetter: params =>
|
||||
params.node?.rowPinned
|
||||
? params.data?.benchmarkBudgetOptional ?? null
|
||||
: getBenchmarkBudgetSplitByLandArea(params.data)?.optional ?? null,
|
||||
valueFormatter: formatReadonlyMoney
|
||||
}
|
||||
]
|
||||
editable: params => !params.node?.group && !params.node?.rowPinned,
|
||||
cellClass: params => (!params.node?.group && !params.node?.rowPinned ? 'editable-cell-line' : ''),
|
||||
cellClassRules: {
|
||||
'editable-cell-empty': params =>
|
||||
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
|
||||
},
|
||||
{
|
||||
headerName: '预算费用',
|
||||
marryChildren: true,
|
||||
children: [
|
||||
{
|
||||
headerName: '基本工作',
|
||||
field: 'budgetFeeBasic',
|
||||
colId: 'budgetFeeBasic',
|
||||
headerClass: 'ag-right-aligned-header',
|
||||
minWidth: 130,
|
||||
flex: 1,
|
||||
cellClass: 'ag-right-aligned-cell',
|
||||
aggFunc: decimalAggSum,
|
||||
valueGetter: params =>
|
||||
params.node?.rowPinned
|
||||
? params.data?.budgetFeeBasic ?? null
|
||||
: getBudgetFeeSplit(params.data)?.basic ?? null,
|
||||
valueFormatter: formatReadonlyMoney
|
||||
},
|
||||
{
|
||||
headerName: '可选工作',
|
||||
field: 'budgetFeeOptional',
|
||||
colId: 'budgetFeeOptional',
|
||||
headerClass: 'ag-right-aligned-header',
|
||||
minWidth: 130,
|
||||
flex: 1,
|
||||
cellClass: 'ag-right-aligned-cell',
|
||||
aggFunc: decimalAggSum,
|
||||
valueGetter: params =>
|
||||
params.node?.rowPinned
|
||||
? params.data?.budgetFeeOptional ?? null
|
||||
: getBudgetFeeSplit(params.data)?.optional ?? null,
|
||||
valueFormatter: formatReadonlyMoney
|
||||
valueParser: params => parseNumberOrNull(params.newValue, { precision: 2 }),
|
||||
valueFormatter: formatEditableNumber
|
||||
},
|
||||
{
|
||||
headerName: '合计',
|
||||
field: 'budgetFee',
|
||||
colId: 'budgetFeeTotal',
|
||||
headerClass: 'ag-right-aligned-header',
|
||||
minWidth: 140,
|
||||
minWidth: 120,
|
||||
flex: 1,
|
||||
cellClass: 'ag-right-aligned-cell',
|
||||
aggFunc: decimalAggSum,
|
||||
@ -507,7 +585,7 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
|
||||
{
|
||||
headerName: '说明',
|
||||
field: 'remark',
|
||||
minWidth: 180,
|
||||
minWidth: 100,
|
||||
flex: 1.2,
|
||||
cellEditor: 'agLargeTextCellEditor',
|
||||
wrapText: true,
|
||||
@ -529,7 +607,7 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
|
||||
|
||||
const autoGroupColumnDef: ColDef = {
|
||||
headerName: '专业编码以及工程专业名称',
|
||||
minWidth: 320,
|
||||
minWidth: 250,
|
||||
pinned: 'left',
|
||||
flex: 2,
|
||||
|
||||
@ -575,10 +653,14 @@ const pinnedTopRowData = computed(() => [
|
||||
benchmarkBudget: totalBenchmarkBudget.value,
|
||||
benchmarkBudgetBasic: totalBenchmarkBudgetBasic.value,
|
||||
benchmarkBudgetOptional: totalBenchmarkBudgetOptional.value,
|
||||
benchmarkBudgetBasicChecked: true,
|
||||
benchmarkBudgetOptionalChecked: true,
|
||||
basicFormula: '',
|
||||
optionalFormula: '',
|
||||
consultCategoryFactor: null,
|
||||
majorFactor: null,
|
||||
workStageFactor: null,
|
||||
workRatio: null,
|
||||
budgetFee: totalBudgetFee.value,
|
||||
budgetFeeBasic: totalBudgetFeeBasic.value,
|
||||
budgetFeeOptional: totalBudgetFeeOptional.value,
|
||||
@ -587,30 +669,35 @@ const pinnedTopRowData = computed(() => [
|
||||
}
|
||||
])
|
||||
|
||||
const buildPersistDetailRows = () =>
|
||||
detailRows.value.map(row => {
|
||||
const syncComputedValuesToDetailRows = () => {
|
||||
for (const row of detailRows.value) {
|
||||
const benchmarkBudgetSplit = getBenchmarkBudgetSplitByLandArea(row)
|
||||
const budgetFeeSplit = benchmarkBudgetSplit
|
||||
? getScaleBudgetFeeSplit({
|
||||
benchmarkBudgetBasic: benchmarkBudgetSplit.basic,
|
||||
benchmarkBudgetOptional: benchmarkBudgetSplit.optional,
|
||||
majorFactor: row.majorFactor,
|
||||
consultCategoryFactor: row.consultCategoryFactor
|
||||
consultCategoryFactor: row.consultCategoryFactor,
|
||||
workStageFactor: row.workStageFactor,
|
||||
workRatio: row.workRatio
|
||||
})
|
||||
: null
|
||||
|
||||
return {
|
||||
...row,
|
||||
benchmarkBudget: benchmarkBudgetSplit?.total ?? null,
|
||||
benchmarkBudgetBasic: benchmarkBudgetSplit?.basic ?? null,
|
||||
benchmarkBudgetOptional: benchmarkBudgetSplit?.optional ?? null,
|
||||
basicFormula: benchmarkBudgetSplit?.basicFormula ?? '',
|
||||
optionalFormula: benchmarkBudgetSplit?.optionalFormula ?? '',
|
||||
budgetFee: budgetFeeSplit?.total ?? null,
|
||||
budgetFeeBasic: budgetFeeSplit?.basic ?? null,
|
||||
budgetFeeOptional: budgetFeeSplit?.optional ?? null
|
||||
row.benchmarkBudget = benchmarkBudgetSplit?.total ?? null
|
||||
row.benchmarkBudgetBasic = benchmarkBudgetSplit?.basic ?? null
|
||||
row.benchmarkBudgetOptional = benchmarkBudgetSplit?.optional ?? null
|
||||
row.basicFormula = benchmarkBudgetSplit?.basicFormula ?? ''
|
||||
row.optionalFormula = benchmarkBudgetSplit?.optionalFormula ?? ''
|
||||
row.budgetFee = budgetFeeSplit?.total ?? null
|
||||
row.budgetFeeBasic = budgetFeeSplit?.basic ?? null
|
||||
row.budgetFeeOptional = budgetFeeSplit?.optional ?? null
|
||||
}
|
||||
}
|
||||
|
||||
const buildPersistDetailRows = () => {
|
||||
syncComputedValuesToDetailRows()
|
||||
return detailRows.value.map(row => ({ ...row }))
|
||||
}
|
||||
})
|
||||
|
||||
const saveToIndexedDB = async () => {
|
||||
if (shouldSkipPersist()) return
|
||||
@ -641,30 +728,35 @@ const loadFromIndexedDB = async () => {
|
||||
typeof baseInfo?.projectIndustry === 'string' ? baseInfo.projectIndustry.trim() : ''
|
||||
|
||||
await ensureFactorDefaultsLoaded()
|
||||
if (shouldForceDefaultLoad()) {
|
||||
const applyContractDefaultRows = async () => {
|
||||
const htData = await localforage.getItem<{ detailRows: SourceRow[] }>(HT_DB_KEY.value)
|
||||
detailRows.value = htData?.detailRows
|
||||
? mergeWithDictRows(htData.detailRows, { includeScaleValues: false, includeFactorValues: false })
|
||||
: buildDefaultRows()
|
||||
const hasContractRows = Array.isArray(htData?.detailRows) && htData.detailRows.length > 0
|
||||
detailRows.value = hasContractRows
|
||||
? mergeWithDictRows(htData!.detailRows, { includeFactorValues: true })
|
||||
: buildDefaultRows().map(row => ({
|
||||
...row,
|
||||
consultCategoryFactor: getDefaultConsultCategoryFactor(),
|
||||
majorFactor: getDefaultMajorFactorById(row.id)
|
||||
}))
|
||||
syncComputedValuesToDetailRows()
|
||||
}
|
||||
if (shouldForceDefaultLoad()) {
|
||||
await applyContractDefaultRows()
|
||||
return
|
||||
}
|
||||
|
||||
const data = await localforage.getItem<XmInfoState>(DB_KEY.value)
|
||||
if (data) {
|
||||
detailRows.value = mergeWithDictRows(data.detailRows)
|
||||
syncComputedValuesToDetailRows()
|
||||
return
|
||||
}
|
||||
|
||||
const htData = await localforage.getItem<{ detailRows: SourceRow[] }>(HT_DB_KEY.value)
|
||||
if (htData?.detailRows) {
|
||||
detailRows.value = mergeWithDictRows(htData.detailRows, { includeScaleValues: false, includeFactorValues: false })
|
||||
return
|
||||
}
|
||||
|
||||
detailRows.value = buildDefaultRows()
|
||||
await applyContractDefaultRows()
|
||||
} catch (error) {
|
||||
console.error('loadFromIndexedDB failed:', error)
|
||||
detailRows.value = buildDefaultRows()
|
||||
syncComputedValuesToDetailRows()
|
||||
}
|
||||
}
|
||||
|
||||
@ -691,6 +783,15 @@ const importContractData = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const clearAllData = async () => {
|
||||
try {
|
||||
detailRows.value = buildDefaultRows()
|
||||
await saveToIndexedDB()
|
||||
} catch (error) {
|
||||
console.error('clearAllData failed:', error)
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => pricingPaneReloadStore.getReloadVersion(props.contractId, props.serviceId),
|
||||
(nextVersion, prevVersion) => {
|
||||
@ -706,10 +807,11 @@ let persistTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
let gridPersistTimer: ReturnType<typeof setTimeout> | null = null
|
||||
const handleCellValueChanged = () => {
|
||||
syncComputedValuesToDetailRows()
|
||||
if (gridPersistTimer) clearTimeout(gridPersistTimer)
|
||||
gridPersistTimer = setTimeout(() => {
|
||||
void saveToIndexedDB()
|
||||
}, 1000)
|
||||
}, 300)
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
@ -752,6 +854,29 @@ const processCellFromClipboard = (params: any) => {
|
||||
<div class="rounded-lg border bg-card xmMx flex min-h-0 flex-1 flex-col">
|
||||
<div class="flex items-center justify-between border-b px-4 py-3">
|
||||
<h3 class="text-sm font-semibold text-foreground">用地规模明细</h3>
|
||||
<div class="flex items-center gap-2">
|
||||
<AlertDialogRoot>
|
||||
<AlertDialogTrigger as-child>
|
||||
<Button type="button" variant="outline" size="sm">清空</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogPortal>
|
||||
<AlertDialogOverlay class="fixed inset-0 z-50 bg-black/45" />
|
||||
<AlertDialogContent class="fixed left-1/2 top-1/2 z-50 w-[92vw] max-w-md -translate-x-1/2 -translate-y-1/2 rounded-lg border bg-background p-5 shadow-xl">
|
||||
<AlertDialogTitle class="text-base font-semibold">确认清空当前明细</AlertDialogTitle>
|
||||
<AlertDialogDescription class="mt-2 text-sm text-muted-foreground">
|
||||
将清空当前用地规模明细,是否继续?
|
||||
</AlertDialogDescription>
|
||||
<div class="mt-4 flex items-center justify-end gap-2">
|
||||
<AlertDialogCancel as-child>
|
||||
<Button variant="outline">取消</Button>
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction as-child>
|
||||
<Button variant="destructive" @click="clearAllData">确认清空</Button>
|
||||
</AlertDialogAction>
|
||||
</div>
|
||||
</AlertDialogContent>
|
||||
</AlertDialogPortal>
|
||||
</AlertDialogRoot>
|
||||
<AlertDialogRoot>
|
||||
<AlertDialogTrigger as-child>
|
||||
<Button type="button" variant="outline" size="sm">使用默认数据</Button>
|
||||
@ -775,6 +900,7 @@ const processCellFromClipboard = (params: any) => {
|
||||
</AlertDialogPortal>
|
||||
</AlertDialogRoot>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ag-theme-quartz h-full min-h-0 w-full flex-1">
|
||||
<AgGridVue :style="{ height: '100%' }" :rowData="detailRows" :pinnedTopRowData="pinnedTopRowData"
|
||||
|
||||
@ -43,6 +43,7 @@ const props = defineProps<{
|
||||
serviceId: string | number
|
||||
}>()
|
||||
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:'
|
||||
const PRICING_FORCE_DEFAULT_PREFIX = 'pricing-force-default:'
|
||||
const pricingPaneReloadStore = usePricingPaneReloadStore()
|
||||
@ -55,7 +56,7 @@ const getDefaultConsultCategoryFactor = () =>
|
||||
|
||||
const ensureFactorDefaultsLoaded = async () => {
|
||||
if (factorDefaultsLoaded) return
|
||||
consultCategoryFactorMap.value = await loadConsultCategoryFactorMap()
|
||||
consultCategoryFactorMap.value = await loadConsultCategoryFactorMap(HT_CONSULT_FACTOR_KEY.value)
|
||||
factorDefaultsLoaded = true
|
||||
}
|
||||
|
||||
@ -481,7 +482,7 @@ const handleCellValueChanged = () => {
|
||||
if (gridPersistTimer) clearTimeout(gridPersistTimer)
|
||||
gridPersistTimer = setTimeout(() => {
|
||||
void saveToIndexedDB()
|
||||
}, 1000)
|
||||
}, 300)
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
|
||||
@ -9,7 +9,7 @@ import { AG_GRID_LOCALE_CN } from '@ag-grid-community/locale'
|
||||
import { addNumbers } from '@/lib/decimal'
|
||||
import { parseNumberOrNull } from '@/lib/number'
|
||||
import { formatThousands, formatThousandsFlexible } from '@/lib/numberFormat'
|
||||
import { getPricingMethodTotalsForServices } from '@/lib/pricingMethodTotals'
|
||||
import { getPricingMethodTotalsForService, getPricingMethodTotalsForServices } from '@/lib/pricingMethodTotals'
|
||||
import { ZXFW_RELOAD_SERVICE_KEY } from '@/lib/zxFwPricingSync'
|
||||
import { Pencil, Eraser, Trash2 } from 'lucide-vue-next'
|
||||
import {
|
||||
@ -311,19 +311,19 @@ const clearRowValues = async (row: DetailRow) => {
|
||||
// 若该服务编辑页已打开,先关闭,避免子页面卸载时把旧数据写回缓? tabStore.removeTab(`zxfw-edit-${props.contractId}-${row.id}`)
|
||||
await nextTick()
|
||||
await clearPricingPaneValues(row.id)
|
||||
// const totals = await getPricingMethodTotalsForService({
|
||||
// contractId: props.contractId,
|
||||
// serviceId: row.id
|
||||
// })
|
||||
const totals = await getPricingMethodTotalsForService({
|
||||
contractId: props.contractId,
|
||||
serviceId: row.id
|
||||
})
|
||||
const clearedRows = detailRows.value.map(item =>
|
||||
item.id !== row.id
|
||||
? item
|
||||
: {
|
||||
...item,
|
||||
investScale: null,
|
||||
landScale: null,
|
||||
workload: null,
|
||||
hourly: null
|
||||
investScale: totals.investScale,
|
||||
landScale: totals.landScale,
|
||||
workload: totals.workload,
|
||||
hourly: totals.hourly
|
||||
}
|
||||
)
|
||||
const nextInvestScale = getMethodTotalFromRows(clearedRows, 'investScale')
|
||||
@ -384,7 +384,7 @@ const ActionCellRenderer = defineComponent({
|
||||
]),
|
||||
h('button', { class: 'zxfw-action-btn', 'data-action': 'clear', type: 'button' }, [
|
||||
h(Eraser, { size: 13, 'aria-hidden': 'true' }),
|
||||
h('span', '清空')
|
||||
h('span', '恢复默认')
|
||||
]),
|
||||
h('button', { class: 'zxfw-action-btn zxfw-action-btn--danger', 'data-action': 'delete', type: 'button' }, [
|
||||
h(Trash2, { size: 13, 'aria-hidden': 'true' }),
|
||||
@ -527,17 +527,47 @@ const detailGridOptions: GridOptions<DetailRow> = {
|
||||
}
|
||||
}
|
||||
|
||||
const fillPricingTotalsForSelectedRows = async () => {
|
||||
const serviceRows = detailRows.value.filter(row => !isFixedRow(row))
|
||||
if (serviceRows.length === 0) return
|
||||
const applyFixedRowTotals = (rows: DetailRow[]) => {
|
||||
const nextInvestScale = getMethodTotalFromRows(rows, 'investScale')
|
||||
const nextLandScale = getMethodTotalFromRows(rows, 'landScale')
|
||||
const nextWorkload = getMethodTotalFromRows(rows, 'workload')
|
||||
const nextHourly = getMethodTotalFromRows(rows, 'hourly')
|
||||
return rows.map(row =>
|
||||
isFixedRow(row)
|
||||
? {
|
||||
...row,
|
||||
investScale: nextInvestScale,
|
||||
landScale: nextLandScale,
|
||||
workload: nextWorkload,
|
||||
hourly: nextHourly,
|
||||
subtotal: addNumbers(nextInvestScale, nextLandScale, nextWorkload, nextHourly)
|
||||
}
|
||||
: row
|
||||
)
|
||||
}
|
||||
|
||||
const fillPricingTotalsForServiceIds = async (serviceIds: string[]) => {
|
||||
const targetIds = Array.from(
|
||||
new Set(
|
||||
serviceIds.filter(id =>
|
||||
detailRows.value.some(row => !isFixedRow(row) && String(row.id) === String(id))
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
if (targetIds.length === 0) {
|
||||
detailRows.value = applyFixedRowTotals(detailRows.value)
|
||||
return
|
||||
}
|
||||
|
||||
const totalsByServiceId = await getPricingMethodTotalsForServices({
|
||||
contractId: props.contractId,
|
||||
serviceIds: serviceRows.map(row => row.id)
|
||||
serviceIds: targetIds
|
||||
})
|
||||
|
||||
detailRows.value = detailRows.value.map(row => {
|
||||
if (isFixedRow(row)) return row
|
||||
const targetSet = new Set(targetIds.map(id => String(id)))
|
||||
const nextRows = detailRows.value.map(row => {
|
||||
if (isFixedRow(row) || !targetSet.has(String(row.id))) return row
|
||||
const totals = totalsByServiceId.get(String(row.id))
|
||||
if (!totals) return row
|
||||
return {
|
||||
@ -548,6 +578,8 @@ const fillPricingTotalsForSelectedRows = async () => {
|
||||
hourly: totals.hourly
|
||||
}
|
||||
})
|
||||
|
||||
detailRows.value = applyFixedRowTotals(nextRows)
|
||||
}
|
||||
|
||||
const applySelection = (codes: string[]) => {
|
||||
@ -601,7 +633,11 @@ const applySelection = (codes: string[]) => {
|
||||
}
|
||||
|
||||
const handleServiceSelectionChange = async (ids: string[]) => {
|
||||
const prevIds = [...selectedIds.value]
|
||||
applySelection(ids)
|
||||
const nextSelectedSet = new Set(selectedIds.value)
|
||||
const addedIds = selectedIds.value.filter(id => !prevIds.includes(id) && nextSelectedSet.has(id))
|
||||
await fillPricingTotalsForServiceIds(addedIds)
|
||||
await saveToIndexedDB()
|
||||
}
|
||||
|
||||
@ -776,12 +812,7 @@ const loadFromIndexedDB = async () => {
|
||||
hourly: typeof old.hourly === 'number' ? old.hourly : null
|
||||
}
|
||||
})
|
||||
|
||||
try {
|
||||
// await fillPricingTotalsForSelectedRows()
|
||||
} catch (error) {
|
||||
console.error('fillPricingTotalsForSelectedRows failed:', error)
|
||||
}
|
||||
detailRows.value = applyFixedRowTotals(detailRows.value)
|
||||
} catch (error) {
|
||||
console.error('loadFromIndexedDB failed:', error)
|
||||
selectedIds.value = []
|
||||
@ -802,7 +833,7 @@ const handleCellValueChanged = () => {
|
||||
if (gridPersistTimer) clearTimeout(gridPersistTimer)
|
||||
gridPersistTimer = setTimeout(() => {
|
||||
void saveToIndexedDB()
|
||||
}, 1000)
|
||||
}, 300)
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
@ -849,16 +880,16 @@ onBeforeUnmount(() => {
|
||||
<AlertDialogPortal>
|
||||
<AlertDialogOverlay class="fixed inset-0 z-50 bg-black/45" />
|
||||
<AlertDialogContent class="fixed left-1/2 top-1/2 z-[70] w-[92vw] max-w-md -translate-x-1/2 -translate-y-1/2 rounded-lg border bg-background p-5 shadow-xl">
|
||||
<AlertDialogTitle class="text-base font-semibold">确认清空服务数据</AlertDialogTitle>
|
||||
<AlertDialogTitle class="text-base font-semibold">确认恢复默认数据</AlertDialogTitle>
|
||||
<AlertDialogDescription class="mt-2 text-sm text-muted-foreground">
|
||||
将清空“{{ pendingClearServiceName }}”的计价数据是否继续?
|
||||
会使用合同卡片里面最新填写的规模信息以及系数,自动计算默认数据,覆盖“{{ pendingClearServiceName }}”当前数据,是否继续?
|
||||
</AlertDialogDescription>
|
||||
<div class="mt-4 flex items-center justify-end gap-2">
|
||||
<AlertDialogCancel as-child>
|
||||
<Button variant="outline">取消</Button>
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction as-child>
|
||||
<Button variant="destructive" @click="confirmClearRow">确认清空</Button>
|
||||
<Button variant="destructive" @click="confirmClearRow">确认恢复</Button>
|
||||
</AlertDialogAction>
|
||||
</div>
|
||||
</AlertDialogContent>
|
||||
|
||||
@ -9,6 +9,7 @@ const borderConfig = {
|
||||
|
||||
export const myTheme = themeQuartz.withParams({
|
||||
wrapperBorder: false,
|
||||
wrapperBorderRadius: 0,
|
||||
headerBackgroundColor: '#f0f2f3',
|
||||
headerTextColor: '#374151',
|
||||
headerFontSize: 15,
|
||||
@ -25,6 +26,7 @@ export const gridOptions: GridOptions = {
|
||||
tooltipShowMode: 'whenTruncated',
|
||||
suppressAggFuncInHeader: true,
|
||||
singleClickEdit: true,
|
||||
stopEditingWhenCellsLoseFocus: true,
|
||||
suppressClickEdit: false,
|
||||
suppressContextMenu: false,
|
||||
groupDefaultExpanded: -1,
|
||||
|
||||
@ -14,6 +14,14 @@ interface StoredDetailRowsState<T = any> {
|
||||
detailRows?: T[]
|
||||
}
|
||||
|
||||
interface StoredFactorState {
|
||||
detailRows?: Array<{
|
||||
id: string
|
||||
standardFactor?: number | null
|
||||
budgetValue?: number | null
|
||||
}>
|
||||
}
|
||||
|
||||
type MaybeNumber = number | null | undefined
|
||||
|
||||
interface ScaleRow {
|
||||
@ -22,6 +30,8 @@ interface ScaleRow {
|
||||
landArea: number | null
|
||||
consultCategoryFactor: number | null
|
||||
majorFactor: number | null
|
||||
workStageFactor: number | null
|
||||
workRatio: number | null
|
||||
}
|
||||
|
||||
interface WorkloadRow {
|
||||
@ -92,25 +102,74 @@ const getDefaultMajorFactorById = (id: string) => {
|
||||
return toFiniteNumberOrNull(major?.defCoe)
|
||||
}
|
||||
|
||||
const resolveFactorValue = (
|
||||
row: { budgetValue?: number | null; standardFactor?: number | null } | undefined,
|
||||
fallback: number | null
|
||||
) => {
|
||||
if (!row) return fallback
|
||||
const budgetValue = toFiniteNumberOrNull(row.budgetValue)
|
||||
if (budgetValue != null) return budgetValue
|
||||
const standardFactor = toFiniteNumberOrNull(row.standardFactor)
|
||||
if (standardFactor != null) return standardFactor
|
||||
return fallback
|
||||
}
|
||||
|
||||
const buildConsultCategoryFactorMap = (state: StoredFactorState | null) => {
|
||||
const map = new Map<string, number | null>()
|
||||
const serviceDict = getServiceDictById() as Record<string, ServiceLite | undefined>
|
||||
for (const [id, item] of Object.entries(serviceDict)) {
|
||||
map.set(String(id), toFiniteNumberOrNull(item?.defCoe))
|
||||
}
|
||||
for (const row of state?.detailRows || []) {
|
||||
if (!row?.id) continue
|
||||
const id = String(row.id)
|
||||
map.set(id, resolveFactorValue(row, map.get(id) ?? null))
|
||||
}
|
||||
return map
|
||||
}
|
||||
|
||||
const buildMajorFactorMap = (state: StoredFactorState | null) => {
|
||||
const map = new Map<string, number | null>()
|
||||
for (const [id, item] of majorById.entries()) {
|
||||
map.set(String(id), toFiniteNumberOrNull(item?.defCoe))
|
||||
}
|
||||
for (const row of state?.detailRows || []) {
|
||||
if (!row?.id) continue
|
||||
const rowId = String(row.id)
|
||||
const id = map.has(rowId) ? rowId : majorIdAliasMap.get(rowId) || rowId
|
||||
map.set(id, resolveFactorValue(row, map.get(id) ?? null))
|
||||
}
|
||||
return map
|
||||
}
|
||||
|
||||
const getMajorLeafIds = () =>
|
||||
getMajorDictEntries()
|
||||
.filter(({ item }) => Boolean(item?.code && String(item.code).includes('-')))
|
||||
.map(({ id }) => id)
|
||||
|
||||
const buildDefaultScaleRows = (serviceId: string | number): ScaleRow[] => {
|
||||
const defaultConsultCategoryFactor = getDefaultConsultCategoryFactor(serviceId)
|
||||
const buildDefaultScaleRows = (
|
||||
serviceId: string | number,
|
||||
consultCategoryFactorMap?: Map<string, number | null>,
|
||||
majorFactorMap?: Map<string, number | null>
|
||||
): ScaleRow[] => {
|
||||
const defaultConsultCategoryFactor =
|
||||
consultCategoryFactorMap?.get(String(serviceId)) ?? getDefaultConsultCategoryFactor(serviceId)
|
||||
return getMajorLeafIds().map(id => ({
|
||||
id,
|
||||
amount: null,
|
||||
landArea: null,
|
||||
consultCategoryFactor: defaultConsultCategoryFactor,
|
||||
majorFactor: getDefaultMajorFactorById(id)
|
||||
majorFactor: majorFactorMap?.get(id) ?? getDefaultMajorFactorById(id),
|
||||
workStageFactor: 1,
|
||||
workRatio: 100
|
||||
}))
|
||||
}
|
||||
|
||||
const mergeScaleRows = (
|
||||
serviceId: string | number,
|
||||
rowsFromDb: Array<Partial<ScaleRow> & Pick<ScaleRow, 'id'>> | undefined
|
||||
rowsFromDb: Array<Partial<ScaleRow> & Pick<ScaleRow, 'id'>> | undefined,
|
||||
consultCategoryFactorMap?: Map<string, number | null>,
|
||||
majorFactorMap?: Map<string, number | null>
|
||||
): ScaleRow[] => {
|
||||
const dbValueMap = toRowMap(rowsFromDb)
|
||||
for (const row of rowsFromDb || []) {
|
||||
@ -121,13 +180,16 @@ const mergeScaleRows = (
|
||||
}
|
||||
}
|
||||
|
||||
const defaultConsultCategoryFactor = getDefaultConsultCategoryFactor(serviceId)
|
||||
return buildDefaultScaleRows(serviceId).map(row => {
|
||||
const defaultConsultCategoryFactor =
|
||||
consultCategoryFactorMap?.get(String(serviceId)) ?? getDefaultConsultCategoryFactor(serviceId)
|
||||
return buildDefaultScaleRows(serviceId, consultCategoryFactorMap, majorFactorMap).map(row => {
|
||||
const fromDb = dbValueMap.get(row.id)
|
||||
if (!fromDb) return row
|
||||
|
||||
const hasConsultCategoryFactor = hasOwn(fromDb, 'consultCategoryFactor')
|
||||
const hasMajorFactor = hasOwn(fromDb, 'majorFactor')
|
||||
const hasWorkStageFactor = hasOwn(fromDb, 'workStageFactor')
|
||||
const hasWorkRatio = hasOwn(fromDb, 'workRatio')
|
||||
|
||||
return {
|
||||
...row,
|
||||
@ -138,7 +200,13 @@ const mergeScaleRows = (
|
||||
(hasConsultCategoryFactor ? null : defaultConsultCategoryFactor),
|
||||
majorFactor:
|
||||
toFiniteNumberOrNull(fromDb.majorFactor) ??
|
||||
(hasMajorFactor ? null : getDefaultMajorFactorById(row.id))
|
||||
(hasMajorFactor ? null : (majorFactorMap?.get(row.id) ?? getDefaultMajorFactorById(row.id))),
|
||||
workStageFactor:
|
||||
toFiniteNumberOrNull((fromDb as Partial<ScaleRow>).workStageFactor) ??
|
||||
(hasWorkStageFactor ? null : row.workStageFactor),
|
||||
workRatio:
|
||||
toFiniteNumberOrNull((fromDb as Partial<ScaleRow>).workRatio) ??
|
||||
(hasWorkRatio ? null : row.workRatio)
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -153,7 +221,9 @@ const getInvestmentBudgetFee = (row: ScaleRow) => {
|
||||
return getScaleBudgetFee({
|
||||
benchmarkBudget: getBenchmarkBudgetByAmount(row.amount),
|
||||
majorFactor: row.majorFactor,
|
||||
consultCategoryFactor: row.consultCategoryFactor
|
||||
consultCategoryFactor: row.consultCategoryFactor,
|
||||
workStageFactor: row.workStageFactor,
|
||||
workRatio: row.workRatio
|
||||
})
|
||||
}
|
||||
|
||||
@ -161,7 +231,9 @@ const getLandBudgetFee = (row: ScaleRow) => {
|
||||
return getScaleBudgetFee({
|
||||
benchmarkBudget: getBenchmarkBudgetByLandArea(row.landArea),
|
||||
majorFactor: row.majorFactor,
|
||||
consultCategoryFactor: row.consultCategoryFactor
|
||||
consultCategoryFactor: row.consultCategoryFactor,
|
||||
workStageFactor: row.workStageFactor,
|
||||
workRatio: row.workRatio
|
||||
})
|
||||
}
|
||||
|
||||
@ -170,8 +242,12 @@ const getTaskEntriesByServiceId = (serviceId: string | number) =>
|
||||
.sort((a, b) => Number(a[0]) - Number(b[0]))
|
||||
.filter(([, task]) => Number(task.serviceID) === Number(serviceId))
|
||||
|
||||
const buildDefaultWorkloadRows = (serviceId: string | number): WorkloadRow[] => {
|
||||
const defaultConsultCategoryFactor = getDefaultConsultCategoryFactor(serviceId)
|
||||
const buildDefaultWorkloadRows = (
|
||||
serviceId: string | number,
|
||||
consultCategoryFactorMap?: Map<string, number | null>
|
||||
): WorkloadRow[] => {
|
||||
const defaultConsultCategoryFactor =
|
||||
consultCategoryFactorMap?.get(String(serviceId)) ?? getDefaultConsultCategoryFactor(serviceId)
|
||||
return getTaskEntriesByServiceId(serviceId).map(([taskId, task], order) => ({
|
||||
id: `task-${taskId}-${order}`,
|
||||
conversion: toFiniteNumberOrNull(task.conversion),
|
||||
@ -184,11 +260,12 @@ const buildDefaultWorkloadRows = (serviceId: string | number): WorkloadRow[] =>
|
||||
|
||||
const mergeWorkloadRows = (
|
||||
serviceId: string | number,
|
||||
rowsFromDb: Array<Partial<WorkloadRow> & Pick<WorkloadRow, 'id'>> | undefined
|
||||
rowsFromDb: Array<Partial<WorkloadRow> & Pick<WorkloadRow, 'id'>> | undefined,
|
||||
consultCategoryFactorMap?: Map<string, number | null>
|
||||
): WorkloadRow[] => {
|
||||
const dbValueMap = toRowMap(rowsFromDb)
|
||||
|
||||
return buildDefaultWorkloadRows(serviceId).map(row => {
|
||||
return buildDefaultWorkloadRows(serviceId, consultCategoryFactorMap).map(row => {
|
||||
const fromDb = dbValueMap.get(row.id)
|
||||
if (!fromDb) return row
|
||||
|
||||
@ -270,15 +347,27 @@ const calcHourlyServiceBudget = (row: HourlyRow) => {
|
||||
const resolveScaleRows = (
|
||||
serviceId: string,
|
||||
pricingData: StoredDetailRowsState | null,
|
||||
htData: StoredDetailRowsState | null
|
||||
htData: StoredDetailRowsState | null,
|
||||
consultCategoryFactorMap?: Map<string, number | null>,
|
||||
majorFactorMap?: Map<string, number | null>
|
||||
) => {
|
||||
if (pricingData?.detailRows != null) {
|
||||
return mergeScaleRows(serviceId, pricingData.detailRows as any)
|
||||
return mergeScaleRows(
|
||||
serviceId,
|
||||
pricingData.detailRows as any,
|
||||
consultCategoryFactorMap,
|
||||
majorFactorMap
|
||||
)
|
||||
}
|
||||
if (htData?.detailRows != null) {
|
||||
return mergeScaleRows(serviceId, htData.detailRows as any)
|
||||
return mergeScaleRows(
|
||||
serviceId,
|
||||
htData.detailRows as any,
|
||||
consultCategoryFactorMap,
|
||||
majorFactorMap
|
||||
)
|
||||
}
|
||||
return buildDefaultScaleRows(serviceId)
|
||||
return buildDefaultScaleRows(serviceId, consultCategoryFactorMap, majorFactorMap)
|
||||
}
|
||||
|
||||
export const getPricingMethodTotalsForService = async (params: {
|
||||
@ -287,33 +376,52 @@ export const getPricingMethodTotalsForService = async (params: {
|
||||
}): Promise<PricingMethodTotals> => {
|
||||
const serviceId = String(params.serviceId)
|
||||
const htDbKey = `ht-info-v3-${params.contractId}`
|
||||
const consultFactorDbKey = `ht-consult-category-factor-v1-${params.contractId}`
|
||||
const majorFactorDbKey = `ht-major-factor-v1-${params.contractId}`
|
||||
const investDbKey = `tzGMF-${params.contractId}-${serviceId}`
|
||||
const landDbKey = `ydGMF-${params.contractId}-${serviceId}`
|
||||
const workloadDbKey = `gzlF-${params.contractId}-${serviceId}`
|
||||
const hourlyDbKey = `hourlyPricing-${params.contractId}-${serviceId}`
|
||||
|
||||
const [investData, landData, workloadData, hourlyData, htData] = await Promise.all([
|
||||
const [investData, landData, workloadData, hourlyData, htData, consultFactorData, majorFactorData] = await Promise.all([
|
||||
localforage.getItem<StoredDetailRowsState>(investDbKey),
|
||||
localforage.getItem<StoredDetailRowsState>(landDbKey),
|
||||
localforage.getItem<StoredDetailRowsState>(workloadDbKey),
|
||||
localforage.getItem<StoredDetailRowsState>(hourlyDbKey),
|
||||
localforage.getItem<StoredDetailRowsState>(htDbKey)
|
||||
localforage.getItem<StoredDetailRowsState>(htDbKey),
|
||||
localforage.getItem<StoredFactorState>(consultFactorDbKey),
|
||||
localforage.getItem<StoredFactorState>(majorFactorDbKey)
|
||||
])
|
||||
|
||||
const consultCategoryFactorMap = buildConsultCategoryFactorMap(consultFactorData)
|
||||
const majorFactorMap = buildMajorFactorMap(majorFactorData)
|
||||
|
||||
// 优先使用对应计费页的数据;不存在时回退合同段规模信息,再回退默认字典行。
|
||||
const investRows = resolveScaleRows(serviceId, investData, htData)
|
||||
const investRows = resolveScaleRows(
|
||||
serviceId,
|
||||
investData,
|
||||
htData,
|
||||
consultCategoryFactorMap,
|
||||
majorFactorMap
|
||||
)
|
||||
const investScale = sumByNumber(investRows, row => getInvestmentBudgetFee(row))
|
||||
|
||||
const landRows = resolveScaleRows(serviceId, landData, htData)
|
||||
const landRows = resolveScaleRows(
|
||||
serviceId,
|
||||
landData,
|
||||
htData,
|
||||
consultCategoryFactorMap,
|
||||
majorFactorMap
|
||||
)
|
||||
const landScale = sumByNumber(landRows, row => getLandBudgetFee(row))
|
||||
|
||||
const defaultWorkloadRows = buildDefaultWorkloadRows(serviceId)
|
||||
const defaultWorkloadRows = buildDefaultWorkloadRows(serviceId, consultCategoryFactorMap)
|
||||
const workload =
|
||||
defaultWorkloadRows.length === 0
|
||||
? null
|
||||
: sumByNumber(
|
||||
workloadData?.detailRows != null
|
||||
? mergeWorkloadRows(serviceId, workloadData.detailRows as any)
|
||||
? mergeWorkloadRows(serviceId, workloadData.detailRows as any, consultCategoryFactorMap)
|
||||
: defaultWorkloadRows,
|
||||
row => calcWorkloadServiceFee(row)
|
||||
)
|
||||
|
||||
@ -43,28 +43,42 @@ export const getScaleBudgetFeeSplit = (params: {
|
||||
benchmarkBudgetOptional: unknown
|
||||
majorFactor: unknown
|
||||
consultCategoryFactor: unknown
|
||||
workStageFactor?: unknown
|
||||
workRatio?: unknown
|
||||
}): ScaleFeeSplitResult | null => {
|
||||
const hasWorkStageFactor = Object.prototype.hasOwnProperty.call(params, 'workStageFactor')
|
||||
const hasWorkRatio = Object.prototype.hasOwnProperty.call(params, 'workRatio')
|
||||
const benchmarkBudgetBasic = toFiniteNumberOrNull(params.benchmarkBudgetBasic)
|
||||
const benchmarkBudgetOptional = toFiniteNumberOrNull(params.benchmarkBudgetOptional)
|
||||
const majorFactor = toFiniteNumberOrNull(params.majorFactor)
|
||||
const consultCategoryFactor = toFiniteNumberOrNull(params.consultCategoryFactor)
|
||||
const workStageFactor = hasWorkStageFactor ? toFiniteNumberOrNull(params.workStageFactor) : 1
|
||||
const workRatio = hasWorkRatio ? toFiniteNumberOrNull(params.workRatio) : 100
|
||||
|
||||
if (
|
||||
benchmarkBudgetBasic == null ||
|
||||
benchmarkBudgetOptional == null ||
|
||||
majorFactor == null ||
|
||||
consultCategoryFactor == null
|
||||
consultCategoryFactor == null ||
|
||||
workStageFactor == null ||
|
||||
workRatio == null
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
const basic = roundTo(toDecimal(benchmarkBudgetBasic).mul(majorFactor).mul(consultCategoryFactor), 2)
|
||||
const optional = roundTo(toDecimal(benchmarkBudgetOptional).mul(majorFactor).mul(consultCategoryFactor), 2)
|
||||
const multiplier = toDecimal(consultCategoryFactor)
|
||||
.mul(majorFactor)
|
||||
.mul(workStageFactor)
|
||||
.mul(workRatio)
|
||||
.div(100)
|
||||
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,
|
||||
total: roundTo(addNumbers(basic, optional), 2),
|
||||
total: roundTo(toDecimal(roundedBenchmarkBudget).mul(multiplier), 2),
|
||||
basicFormula: '',
|
||||
optionalFormula: ''
|
||||
}
|
||||
@ -74,14 +88,32 @@ export const getScaleBudgetFee = (params: {
|
||||
benchmarkBudget: unknown
|
||||
majorFactor: unknown
|
||||
consultCategoryFactor: unknown
|
||||
workStageFactor?: unknown
|
||||
workRatio?: unknown
|
||||
}) => {
|
||||
const hasWorkStageFactor = Object.prototype.hasOwnProperty.call(params, 'workStageFactor')
|
||||
const hasWorkRatio = Object.prototype.hasOwnProperty.call(params, 'workRatio')
|
||||
const benchmarkBudget = toFiniteNumberOrNull(params.benchmarkBudget)
|
||||
const majorFactor = toFiniteNumberOrNull(params.majorFactor)
|
||||
const consultCategoryFactor = toFiniteNumberOrNull(params.consultCategoryFactor)
|
||||
const workStageFactor = hasWorkStageFactor ? toFiniteNumberOrNull(params.workStageFactor) : 1
|
||||
const workRatio = hasWorkRatio ? toFiniteNumberOrNull(params.workRatio) : 100
|
||||
|
||||
if (benchmarkBudget == null || majorFactor == null || consultCategoryFactor == null) {
|
||||
if (
|
||||
benchmarkBudget == null ||
|
||||
majorFactor == null ||
|
||||
consultCategoryFactor == null ||
|
||||
workStageFactor == null ||
|
||||
workRatio == null
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
return roundTo(toDecimal(benchmarkBudget).mul(majorFactor).mul(consultCategoryFactor), 2)
|
||||
const roundedBenchmarkBudget = roundTo(benchmarkBudget, 2)
|
||||
const multiplier = toDecimal(consultCategoryFactor)
|
||||
.mul(majorFactor)
|
||||
.mul(workStageFactor)
|
||||
.mul(workRatio)
|
||||
.div(100)
|
||||
return roundTo(toDecimal(roundedBenchmarkBudget).mul(multiplier), 2)
|
||||
}
|
||||
|
||||
@ -57,8 +57,8 @@ const loadFactorMap = async (
|
||||
return map
|
||||
}
|
||||
|
||||
export const loadConsultCategoryFactorMap = async () =>
|
||||
loadFactorMap(CONSULT_CATEGORY_FACTOR_KEY, getServiceDictById() as FactorDict)
|
||||
export const loadConsultCategoryFactorMap = async (storageKey = CONSULT_CATEGORY_FACTOR_KEY) =>
|
||||
loadFactorMap(storageKey, getServiceDictById() as FactorDict)
|
||||
|
||||
export const loadMajorFactorMap = async () =>
|
||||
loadFactorMap(MAJOR_FACTOR_KEY, getMajorDictById() as FactorDict, getMajorIdAliasMap())
|
||||
export const loadMajorFactorMap = async (storageKey = MAJOR_FACTOR_KEY) =>
|
||||
loadFactorMap(storageKey, getMajorDictById() as FactorDict, getMajorIdAliasMap())
|
||||
|
||||
@ -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/pricingmethodtotals.ts","./src/lib/pricingscalefee.ts","./src/lib/utils.ts","./src/lib/xmfactordefaults.ts","./src/lib/zwarchive.ts","./src/lib/zxfwpricingsync.ts","./src/pinia/pricingpanereload.ts","./src/pinia/tab.ts","./src/app.vue","./src/components/common/commonaggrid.vue","./src/components/common/methodunavailablenotice.vue","./src/components/common/xmfactorgrid.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/contractdetailview.vue","./src/components/views/ht.vue","./src/components/views/servicecheckboxselector.vue","./src/components/views/xm.vue","./src/components/views/xmconsultcategoryfactor.vue","./src/components/views/xmmajorfactor.vue","./src/components/views/zxfwview.vue","./src/components/views/htinfo.vue","./src/components/views/info.vue","./src/components/views/xminfo.vue","./src/components/views/zxfw.vue","./src/components/views/pricingview/hourlypricingpane.vue","./src/components/views/pricingview/investmentscalepricingpane.vue","./src/components/views/pricingview/landscalepricingpane.vue","./src/components/views/pricingview/workloadpricingpane.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/decimal.ts","./src/lib/diyaggridoptions.ts","./src/lib/number.ts","./src/lib/numberformat.ts","./src/lib/pricingmethodtotals.ts","./src/lib/pricingscalefee.ts","./src/lib/utils.ts","./src/lib/xmfactordefaults.ts","./src/lib/zwarchive.ts","./src/lib/zxfwpricingsync.ts","./src/pinia/pricingpanereload.ts","./src/pinia/tab.ts","./src/app.vue","./src/components/common/methodunavailablenotice.vue","./src/components/common/xmfactorgrid.vue","./src/components/common/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/ht.vue","./src/components/views/htconsultcategoryfactor.vue","./src/components/views/htmajorfactor.vue","./src/components/views/servicecheckboxselector.vue","./src/components/views/xmconsultcategoryfactor.vue","./src/components/views/xmmajorfactor.vue","./src/components/views/zxfwview.vue","./src/components/views/htcard.vue","./src/components/views/htinfo.vue","./src/components/views/info.vue","./src/components/views/xmcard.vue","./src/components/views/xminfo.vue","./src/components/views/zxfw.vue","./src/components/views/pricingview/hourlypricingpane.vue","./src/components/views/pricingview/investmentscalepricingpane.vue","./src/components/views/pricingview/landscalepricingpane.vue","./src/components/views/pricingview/workloadpricingpane.vue","./src/layout/tab.vue","./src/layout/typeline.vue"],"version":"5.9.3"}
|
||||
@ -20,7 +20,10 @@ export default defineConfig({
|
||||
assetsDir: 'static',
|
||||
// 3. 生成的静态资源文件名是否包含哈希(用于缓存控制)
|
||||
assetsInlineLimit: 4096, // 小于4kb的资源内联,不生成文件
|
||||
rollupOptions: {
|
||||
rolldownOptions: {
|
||||
checks: {
|
||||
pluginTimings: false
|
||||
},
|
||||
// 4. 自定义入口/出口(进阶,一般无需修改)
|
||||
output: {
|
||||
// 自定义 chunk 文件名(拆分公共代码)
|
||||
@ -53,7 +56,7 @@ export default defineConfig({
|
||||
}
|
||||
}
|
||||
},
|
||||
chunkSizeWarningLimit: 800,
|
||||
chunkSizeWarningLimit: 1800,
|
||||
// 5. 生产环境是否生成 sourcemap(默认 false,关闭可减小包体积)
|
||||
sourcemap: false,
|
||||
// 6. 清空输出目录(默认 true)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user