完成大部分

This commit is contained in:
zwgczx committed 2026-02-28 16:34:36 +08:00
1 parent e97707ac59
commit 13b03e016e
21 files changed
+1628 -275

No files matched your search

@@ -3,10 +3,13 @@ import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { AgGridVue } from 'ag-grid-vue3'
import type { ColDef } from 'ag-grid-community'
import localforage from 'localforage'
import { serviceList, taskList } from '@/sql'
import { taskList } from '@/sql'
import { myTheme, gridOptions } from '@/lib/diyAgGridOptions'
import { decimalAggSum, roundTo, toDecimal } from '@/lib/decimal'
import { decimalAggSum, roundTo, sumByNumber, toDecimal } from '@/lib/decimal'
import { formatThousands } from '@/lib/numberFormat'
import { syncPricingTotalToZxFw, ZXFW_RELOAD_SERVICE_KEY } from '@/lib/zxFwPricingSync'
import { usePricingPaneReloadStore } from '@/pinia/pricingPaneReload'
import { loadConsultCategoryFactorMap } from '@/lib/xmFactorDefaults'
import { AG_GRID_LOCALE_CN } from '@ag-grid-community/locale';
@@ -40,6 +43,17 @@ const DB_KEY = computed(() => `gzlF-${props.contractId}-${props.serviceId}`)
const PRICING_CLEAR_SKIP_PREFIX = 'pricing-clear-skip:'
const PRICING_FORCE_DEFAULT_PREFIX = 'pricing-force-default:'
const pricingPaneReloadStore = usePricingPaneReloadStore()
const consultCategoryFactorMap = ref<Map<string, number | null>>(new Map())
let factorDefaultsLoaded = false
const getDefaultConsultCategoryFactor = () =>
consultCategoryFactorMap.value.get(String(props.serviceId)) ?? null
const ensureFactorDefaultsLoaded = async () => {
if (factorDefaultsLoaded) return
consultCategoryFactorMap.value = await loadConsultCategoryFactorMap()
factorDefaultsLoaded = true
}
const shouldSkipPersist = () => {
const storageKey = `${PRICING_CLEAR_SKIP_PREFIX}${DB_KEY.value}`
@@ -62,7 +76,6 @@ const shouldForceDefaultLoad = () => {
const detailRows = ref<DetailRow[]>([])
type serviceLite = { defCoe: number | null }
type taskLite = {
serviceID: number
code: string
@@ -76,11 +89,6 @@ type taskLite = {
desc: string | null
}
const defaultConsultCategoryFactor = computed<number | null>(() => {
const service = (serviceList as Record<string, serviceLite | undefined>)[String(props.serviceId)]
return typeof service?.defCoe === 'number' && Number.isFinite(service.defCoe) ? service.defCoe : null
})
const formatTaskReferenceUnitPrice = (task: taskLite) => {
const unit = task.unit || ''
const hasMin = typeof task.minPrice === 'number' && Number.isFinite(task.minPrice)
@@ -121,7 +129,7 @@ const buildDefaultRows = (): DetailRow[] => {
budgetReferenceUnitPrice: formatTaskReferenceUnitPrice(task),
budgetAdoptedUnitPrice:
typeof task.defPrice === 'number' && Number.isFinite(task.defPrice) ? task.defPrice : null,
consultCategoryFactor: defaultConsultCategoryFactor.value,
consultCategoryFactor: getDefaultConsultCategoryFactor(),
serviceFee: null,
remark: task.desc|| '',
path: [rowId]
@@ -193,12 +201,6 @@ const formatEditableNumber = (params: any) => {
return Number(params.value).toFixed(2)
}
const formatReadonlyNumber = (params: any) => {
if (isNoTaskRow(params.data)) return '无'
if (params.value == null || params.value === '') return ''
return roundTo(params.value, 2).toFixed(2)
}
const spanRowsByTaskName = (params: any) => {
const rowA = params?.nodeA?.data as DetailRow | undefined
const rowB = params?.nodeB?.data as DetailRow | undefined
@@ -216,7 +218,8 @@ const columnDefs: ColDef<DetailRow>[] = [
minWidth: 100,
width: 120,
pinned: 'left',
valueFormatter: params => params.value || ''
colSpan: params => (params.node?.rowPinned ? 3 : 1),
valueFormatter: params => (params.node?.rowPinned ? '总合计' : params.value || '')
},
{
headerName: '名称',
@@ -227,7 +230,7 @@ const columnDefs: ColDef<DetailRow>[] = [
autoHeight: true,
spanRows: true,
valueFormatter: params => params.value || ''
valueFormatter: params => (params.node?.rowPinned ? '' : params.value || '')
},
{
headerName: '预算基数',
@@ -238,7 +241,7 @@ const columnDefs: ColDef<DetailRow>[] = [
width: 180,
pinned: 'left',
spanRows: spanRowsByTaskName,
valueFormatter: params => params.value || ''
valueFormatter: params => (params.node?.rowPinned ? '' : params.value || '')
},
{
headerName: '预算参考单价',
@@ -250,10 +253,11 @@ const columnDefs: ColDef<DetailRow>[] = [
{
headerName: '预算采用单价',
field: 'budgetAdoptedUnitPrice',
headerClass: 'ag-right-aligned-header',
minWidth: 170,
flex: 1,
editable: params => !params.node?.group && !params.node?.rowPinned && !isNoTaskRow(params.data),
cellClass: params => (!params.node?.group && !params.node?.rowPinned ? 'editable-cell-line' : ''),
cellClass: params => (!params.node?.group && !params.node?.rowPinned ? 'ag-right-aligned-cell editable-cell-line' : 'ag-right-aligned-cell'),
cellClassRules: {
'editable-cell-empty': params =>
!params.node?.group &&
@@ -269,7 +273,7 @@ const columnDefs: ColDef<DetailRow>[] = [
}
if (params.value == null) return ''
const unit = params.data?.unit || ''
return `${Number(params.value).toFixed(2)}${unit}`
return `${formatThousands(params.value)}${unit}`
}
},
{
@@ -293,8 +297,9 @@ const columnDefs: ColDef<DetailRow>[] = [
{
headerName: '咨询分类系数',
field: 'consultCategoryFactor',
minWidth: 150,
flex: 1,
width: 80,
minWidth: 70,
maxWidth: 90,
editable: params => !params.node?.group && !params.node?.rowPinned && !isNoTaskRow(params.data),
cellClass: params => (!params.node?.group && !params.node?.rowPinned ? 'editable-cell-line' : ''),
cellClassRules: {
@@ -310,12 +315,18 @@ const columnDefs: ColDef<DetailRow>[] = [
{
headerName: '服务费用(元)',
field: 'serviceFee',
headerClass: 'ag-right-aligned-header',
minWidth: 150,
flex: 1,
cellClass: 'ag-right-aligned-cell',
editable: false,
valueGetter: params => calcServiceFee(params.data),
valueGetter: params => (params.node?.rowPinned ? params.data?.serviceFee ?? null : calcServiceFee(params.data)),
aggFunc: decimalAggSum,
valueFormatter: formatReadonlyNumber
valueFormatter: params => {
if (isNoTaskRow(params.data)) return '无'
if (params.value == null || params.value === '') return ''
return formatThousands(roundTo(params.value, 2))
}
},
{
headerName: '说明',
@@ -343,6 +354,27 @@ const columnDefs: ColDef<DetailRow>[] = [
}
]
const totalWorkload = computed(() => sumByNumber(detailRows.value, row => row.workload))
const totalServiceFee = computed(() => sumByNumber(detailRows.value, row => calcServiceFee(row)))
const pinnedTopRowData = computed(() => [
{
id: 'pinned-total-row',
taskCode: '总合计',
taskName: '',
unit: '',
conversion: null,
workload: totalWorkload.value,
budgetBase: '',
budgetReferenceUnitPrice: '',
budgetAdoptedUnitPrice: null,
consultCategoryFactor: null,
serviceFee: totalServiceFee.value,
remark: '',
path: ['TOTAL']
}
])
const saveToIndexedDB = async () => {
@@ -354,6 +386,15 @@ const saveToIndexedDB = async () => {
}
console.log('Saving to IndexedDB:', payload)
await localforage.setItem(DB_KEY.value, payload)
const synced = await syncPricingTotalToZxFw({
contractId: props.contractId,
serviceId: props.serviceId,
field: 'workload',
value: totalServiceFee.value
})
if (synced) {
pricingPaneReloadStore.markReload(props.contractId, ZXFW_RELOAD_SERVICE_KEY)
}
} catch (error) {
console.error('saveToIndexedDB failed:', error)
}
@@ -366,6 +407,8 @@ const loadFromIndexedDB = async () => {
return
}
await ensureFactorDefaultsLoaded()
if (shouldForceDefaultLoad()) {
detailRows.value = buildDefaultRows()
return
@@ -430,6 +473,7 @@ const processCellFromClipboard = (params: any) => {
}
return params.value;
};
const mydiyTheme = myTheme.withParams({
rowBorder: {
style: "solid",
@@ -455,15 +499,15 @@ const mydiyTheme = myTheme.withParams({
</div>
<div v-if="isWorkloadMethodApplicable" class="ag-theme-quartz h-full min-h-0 w-full flex-1">
<AgGridVue :style="{ height: '100%' }" :rowData="detailRows"
<AgGridVue :style="{ height: '100%' }" :rowData="detailRows" :pinnedTopRowData="pinnedTopRowData"
:columnDefs="columnDefs" :gridOptions="gridOptions" :theme="mydiyTheme" :treeData="false"
:enableCellSpan="true"
@cell-value-changed="handleCellValueChanged" :suppressColumnVirtualisation="true"
:suppressRowVirtualisation="true"
:cellSelection="{ handle: { mode: 'range' } }" :enableClipboard="true"
:enableClipboard="true"
:localeText="AG_GRID_LOCALE_CN" :tooltipShowDelay="500" :headerHeight="50"
:processCellForClipboard="processCellForClipboard" :processCellFromClipboard="processCellFromClipboard"
:undoRedoCellEditing="true" :undoRedoCellEditingLimit="20" />
@@ -480,6 +524,3 @@ const mydiyTheme = myTheme.withParams({
</div>
</div>
</template>
<!-- :rowSelection="'multiple'"
:enableClickSelection="false" -->
<!-- :suppressRowTransform="true" -->