fix someone
This commit is contained in:
1 parent
c482faacbf
commit
ad4e9cdee0
17 files changed
+1084
-311
No files matched your search
@@ -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'
|
||||
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -392,12 +399,13 @@ onMounted(async () => {
|
||||
</DatePickerCalendar>
|
||||
<div class="mt-2 flex justify-end">
|
||||
<DatePickerClose as-child>
|
||||
<button
|
||||
<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,28 +850,52 @@ 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>
|
||||
<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 @click="importContractData">确认覆盖</Button>
|
||||
</AlertDialogAction>
|
||||
</div>
|
||||
</AlertDialogContent>
|
||||
</AlertDialogPortal>
|
||||
</AlertDialogRoot>
|
||||
<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>
|
||||
</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 @click="importContractData">确认覆盖</Button>
|
||||
</AlertDialogAction>
|
||||
</div>
|
||||
</AlertDialogContent>
|
||||
</AlertDialogPortal>
|
||||
</AlertDialogRoot>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ag-theme-quartz h-full min-h-0 w-full flex-1">
|
||||
@@ -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,37 +451,6 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
|
||||
},
|
||||
valueFormatter: formatEditableFlexibleNumber
|
||||
},
|
||||
|
||||
{
|
||||
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,
|
||||
@@ -429,7 +460,7 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
|
||||
field: 'benchmarkBudgetBasic',
|
||||
colId: 'benchmarkBudgetBasic',
|
||||
headerClass: 'ag-right-aligned-header',
|
||||
minWidth: 140,
|
||||
minWidth: 130,
|
||||
flex: 1,
|
||||
cellClass: 'ag-right-aligned-cell',
|
||||
aggFunc: decimalAggSum,
|
||||
@@ -437,6 +468,7 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
|
||||
params.node?.rowPinned
|
||||
? params.data?.benchmarkBudgetBasic ?? null
|
||||
: getBenchmarkBudgetSplitByLandArea(params.data)?.basic ?? null,
|
||||
cellRenderer: createBudgetCellRendererWithCheck('benchmarkBudgetBasicChecked'),
|
||||
valueFormatter: formatReadonlyMoney
|
||||
},
|
||||
{
|
||||
@@ -444,7 +476,7 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
|
||||
field: 'benchmarkBudgetOptional',
|
||||
colId: 'benchmarkBudgetOptional',
|
||||
headerClass: 'ag-right-aligned-header',
|
||||
minWidth: 140,
|
||||
minWidth: 130,
|
||||
flex: 1,
|
||||
cellClass: 'ag-right-aligned-cell',
|
||||
aggFunc: decimalAggSum,
|
||||
@@ -452,6 +484,22 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
|
||||
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
|
||||
}
|
||||
]
|
||||
@@ -461,41 +509,71 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
|
||||
marryChildren: true,
|
||||
children: [
|
||||
{
|
||||
headerName: '基本工作',
|
||||
field: 'budgetFeeBasic',
|
||||
colId: 'budgetFeeBasic',
|
||||
headerClass: 'ag-right-aligned-header',
|
||||
minWidth: 130,
|
||||
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: 130,
|
||||
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: 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,28 +854,52 @@ 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>
|
||||
<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 @click="importContractData">确认覆盖</Button>
|
||||
</AlertDialogAction>
|
||||
</div>
|
||||
</AlertDialogContent>
|
||||
</AlertDialogPortal>
|
||||
</AlertDialogRoot>
|
||||
<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>
|
||||
</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 @click="importContractData">确认覆盖</Button>
|
||||
</AlertDialogAction>
|
||||
</div>
|
||||
</AlertDialogContent>
|
||||
</AlertDialogPortal>
|
||||
</AlertDialogRoot>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ag-theme-quartz h-full min-h-0 w-full flex-1">
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user