-
+
{
:localeText="AG_GRID_LOCALE_CN"
:tooltipShowDelay="500"
:headerHeight="50"
+ :suppressHorizontalScroll="true"
:processCellForClipboard="processCellForClipboard"
:processCellFromClipboard="processCellFromClipboard"
:undoRedoCellEditing="true"
:undoRedoCellEditingLimit="20"
+ @grid-ready="handleGridReady"
+ @grid-size-changed="handleGridSizeChanged"
+ @first-data-rendered="handleFirstDataRendered"
/>
diff --git a/src/components/views/pricingView/HourlyPricingPane.vue b/src/components/views/pricingView/HourlyPricingPane.vue
index c8624d4..ea9d649 100644
--- a/src/components/views/pricingView/HourlyPricingPane.vue
+++ b/src/components/views/pricingView/HourlyPricingPane.vue
@@ -3,33 +3,21 @@ import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { AgGridVue } from 'ag-grid-vue3'
import type { ColDef, ColGroupDef } from 'ag-grid-community'
import localforage from 'localforage'
+import { expertList } from '@/sql'
import { myTheme, gridOptions } from '@/lib/diyAgGridOptions'
-import { decimalAggSum, sumByNumber } 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 { AG_GRID_LOCALE_CN } from '@ag-grid-community/locale';
-interface DictLeaf {
- id: string
- code: string
- name: string
-}
-
-interface DictGroup {
- id: string
- code: string
- name: string
- children: DictLeaf[]
-}
-
interface DetailRow {
id: string
- groupCode: string
- groupName: string
- majorCode: string
- majorName: string
- laborBudgetUnitPrice: number | null
- compositeBudgetUnitPrice: number | null
+ expertCode: string
+ expertName: string
+ laborBudgetUnitPrice: string
+ compositeBudgetUnitPrice: string
adoptedBudgetUnitPrice: number | null
personnelCount: number | null
workdayCount: number | null
@@ -74,14 +62,77 @@ const shouldForceDefaultLoad = () => {
const detailRows = ref
([])
+type ExpertLite = {
+ code: string
+ name: string
+ maxPrice: number | null
+ minPrice: number | null
+ defPrice: number | null
+ manageCoe: number | null
+}
+const expertEntries = Object.entries(expertList as Record)
+ .sort((a, b) => Number(a[0]) - Number(b[0]))
+ .filter((entry): entry is [string, ExpertLite] => {
+ const item = entry[1]
+ return Boolean(item?.code && item?.name)
+ })
+const formatPriceRange = (min: number | null, max: number | null) => {
+ const hasMin = typeof min === 'number' && Number.isFinite(min)
+ const hasMax = typeof max === 'number' && Number.isFinite(max)
+ if (hasMin && hasMax) return `${min}-${max}`
+ if (hasMin) return String(min)
+ if (hasMax) return String(max)
+ return ''
+}
-const idLabelMap = new Map()
+const getCompositeBudgetUnitPriceRange = (expert: ExpertLite) => {
+ if (
+ typeof expert.manageCoe !== 'number' ||
+ !Number.isFinite(expert.manageCoe)
+ ) {
+ return ''
+ }
+ const min = typeof expert.minPrice === 'number' && Number.isFinite(expert.minPrice)
+ ? roundTo(toDecimal(expert.minPrice).mul(expert.manageCoe), 2)
+ : null
+ const max = typeof expert.maxPrice === 'number' && Number.isFinite(expert.maxPrice)
+ ? roundTo(toDecimal(expert.maxPrice).mul(expert.manageCoe), 2)
+ : null
+ return formatPriceRange(min, max)
+}
+const getDefaultAdoptedBudgetUnitPrice = (expert: ExpertLite) => {
+ if (
+ typeof expert.defPrice !== 'number' ||
+ !Number.isFinite(expert.defPrice) ||
+ typeof expert.manageCoe !== 'number' ||
+ !Number.isFinite(expert.manageCoe)
+ ) {
+ return null
+ }
+ return roundTo(toDecimal(expert.defPrice).mul(expert.manageCoe), 2)
+}
const buildDefaultRows = (): DetailRow[] => {
const rows: DetailRow[] = []
+ for (const [expertId, expert] of expertEntries) {
+ const rowId = `expert-${expertId}`
+ rows.push({
+ id: rowId,
+ expertCode: expert.code,
+ expertName: expert.name,
+ laborBudgetUnitPrice: formatPriceRange(expert.minPrice, expert.maxPrice),
+ compositeBudgetUnitPrice: getCompositeBudgetUnitPriceRange(expert),
+ adoptedBudgetUnitPrice: getDefaultAdoptedBudgetUnitPrice(expert),
+ personnelCount: null,
+ workdayCount: null,
+ serviceBudget: null,
+ remark: '',
+ path: [rowId]
+ })
+ }
return rows
}
@@ -98,10 +149,6 @@ const mergeWithDictRows = (rowsFromDb: DetailRow[] | undefined): DetailRow[] =>
return {
...row,
- laborBudgetUnitPrice:
- typeof fromDb.laborBudgetUnitPrice === 'number' ? fromDb.laborBudgetUnitPrice : null,
- compositeBudgetUnitPrice:
- typeof fromDb.compositeBudgetUnitPrice === 'number' ? fromDb.compositeBudgetUnitPrice : null,
adoptedBudgetUnitPrice:
typeof fromDb.adoptedBudgetUnitPrice === 'number' ? fromDb.adoptedBudgetUnitPrice : null,
personnelCount: typeof fromDb.personnelCount === 'number' ? fromDb.personnelCount : null,
@@ -118,6 +165,17 @@ const parseNumberOrNull = (value: unknown) => {
return Number.isFinite(v) ? v : null
}
+const parseNonNegativeIntegerOrNull = (value: unknown) => {
+ if (value === '' || value == null) return null
+ if (typeof value === 'number') {
+ return Number.isInteger(value) && value >= 0 ? value : null
+ }
+ const normalized = String(value).trim()
+ if (!/^\d+$/.test(normalized)) return null
+ const v = Number(normalized)
+ return Number.isSafeInteger(v) ? v : null
+}
+
const formatEditableNumber = (params: any) => {
if (!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')) {
return '点击输入'
@@ -126,6 +184,31 @@ const formatEditableNumber = (params: any) => {
return Number(params.value).toFixed(2)
}
+const formatEditableInteger = (params: any) => {
+ if (!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')) {
+ return '点击输入'
+ }
+ if (params.value == null) return ''
+ return String(Number(params.value))
+}
+
+const calcServiceBudget = (row: DetailRow | undefined) => {
+ const adopted = row?.adoptedBudgetUnitPrice
+ const personnel = row?.personnelCount
+ const workday = row?.workdayCount
+ if (
+ typeof adopted !== 'number' ||
+ !Number.isFinite(adopted) ||
+ typeof personnel !== 'number' ||
+ !Number.isFinite(personnel) ||
+ typeof workday !== 'number' ||
+ !Number.isFinite(workday)
+ ) {
+ return null
+ }
+ return roundTo(toDecimal(adopted).mul(personnel).mul(workday), 2)
+}
+
const editableNumberCol = (
field: K,
headerName: string,
@@ -146,29 +229,96 @@ const editableNumberCol = (
...extra
})
+const editableMoneyCol = (
+ field: K,
+ headerName: string,
+ extra: Partial> = {}
+): ColDef => ({
+ headerName,
+ field,
+ headerClass: 'ag-right-aligned-header',
+ minWidth: 150,
+ flex: 1,
+ editable: params => !params.node?.group && !params.node?.rowPinned,
+ cellClass: params => (!params.node?.group && !params.node?.rowPinned ? 'ag-right-aligned-cell editable-cell-line' : 'ag-right-aligned-cell'),
+ cellClassRules: {
+ 'editable-cell-empty': params =>
+ !params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
+ },
+ valueParser: params => parseNumberOrNull(params.newValue),
+ valueFormatter: params => {
+ if (!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')) {
+ return '点击输入'
+ }
+ if (params.value == null) return ''
+ return formatThousands(params.value)
+ },
+ ...extra
+})
+
+const readonlyTextCol = (
+ field: K,
+ headerName: string,
+ extra: Partial> = {}
+): ColDef => ({
+ headerName,
+ field,
+ minWidth: 170,
+ flex: 1,
+ editable: false,
+ valueFormatter: params => params.value || '',
+ ...extra
+})
+
const columnDefs: (ColDef | ColGroupDef)[] = [
+ {
+ headerName: '编码',
+ field: 'expertCode',
+ minWidth: 120,
+ width: 140,
+ pinned: 'left',
+ colSpan: params => (params.node?.rowPinned ? 2 : 1),
+ valueFormatter: params => (params.node?.rowPinned ? '总合计' : params.value || '')
+ },
{
headerName: '人员名称',
+ field: 'expertName',
minWidth: 200,
width: 220,
pinned: 'left',
- valueGetter: params => {
- if (params.node?.rowPinned) return ''
- return params.node?.group ? params.data?.groupName || '' : params.data?.majorName || ''
- }
+ tooltipField: 'expertName',
+ valueFormatter: params => (params.node?.rowPinned ? '' : params.value || '')
},
{
headerName: '预算参考单价',
marryChildren: true,
children: [
- editableNumberCol('laborBudgetUnitPrice', '人工预算单价(元/工日)'),
- editableNumberCol('compositeBudgetUnitPrice', '综合预算单价(元/工日)')
+ readonlyTextCol('laborBudgetUnitPrice', '人工预算单价(元/工日)'),
+ readonlyTextCol('compositeBudgetUnitPrice', '综合预算单价(元/工日)')
]
},
- editableNumberCol('adoptedBudgetUnitPrice', '预算采用单价(元/工日)'),
- editableNumberCol('personnelCount', '人员数量(人)', { aggFunc: decimalAggSum }),
+ editableMoneyCol('adoptedBudgetUnitPrice', '预算采用单价(元/工日)'),
+ editableNumberCol('personnelCount', '人员数量(人)', {
+ aggFunc: decimalAggSum,
+ valueParser: params => parseNonNegativeIntegerOrNull(params.newValue),
+ valueFormatter: formatEditableInteger
+ }),
editableNumberCol('workdayCount', '工日数量(工日)', { aggFunc: decimalAggSum }),
- editableNumberCol('serviceBudget', '服务预算(元)', { aggFunc: decimalAggSum }),
+ {
+ headerName: '服务预算(元)',
+ field: 'serviceBudget',
+ headerClass: 'ag-right-aligned-header',
+ minWidth: 150,
+ flex: 1,
+ cellClass: 'ag-right-aligned-cell',
+ editable: false,
+ aggFunc: decimalAggSum,
+ valueGetter: params => (params.node?.rowPinned ? params.data?.serviceBudget ?? null : calcServiceBudget(params.data)),
+ valueFormatter: params => {
+ if (params.value == null || params.value === '') return ''
+ return formatThousands(params.value)
+ }
+ },
{
headerName: '说明',
field: 'remark',
@@ -192,39 +342,18 @@ const columnDefs: (ColDef | ColGroupDef)[] = [
}
]
-const autoGroupColumnDef: ColDef = {
- headerName: '编码',
- minWidth: 150,
- pinned: 'left',
- width: 160,
-
- cellRendererParams: {
- suppressCount: true
- },
- valueFormatter: params => {
- if (params.node?.rowPinned) {
- return '总合计'
- }
- const nodeId = String(params.value || '')
- const label = idLabelMap.get(nodeId) || nodeId
- return label.includes(' ') ? label.split(' ')[0] : label
- }
-}
-
const totalPersonnelCount = computed(() => sumByNumber(detailRows.value, row => row.personnelCount))
const totalWorkdayCount = computed(() => sumByNumber(detailRows.value, row => row.workdayCount))
-const totalServiceBudget = computed(() => sumByNumber(detailRows.value, row => row.serviceBudget))
+const totalServiceBudget = computed(() => sumByNumber(detailRows.value, row => calcServiceBudget(row)))
const pinnedTopRowData = computed(() => [
{
id: 'pinned-total-row',
- groupCode: '',
- groupName: '',
- majorCode: '',
- majorName: '',
- laborBudgetUnitPrice: null,
- compositeBudgetUnitPrice: null,
+ expertCode: '总合计',
+ expertName: '',
+ laborBudgetUnitPrice: '',
+ compositeBudgetUnitPrice: '',
adoptedBudgetUnitPrice: null,
personnelCount: totalPersonnelCount.value,
workdayCount: totalWorkdayCount.value,
@@ -244,6 +373,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: 'hourly',
+ value: totalServiceBudget.value
+ })
+ if (synced) {
+ pricingPaneReloadStore.markReload(props.contractId, ZXFW_RELOAD_SERVICE_KEY)
+ }
} catch (error) {
console.error('saveToIndexedDB failed:', error)
}
@@ -315,6 +453,12 @@ const processCellFromClipboard = (params: any) => {
}
return params.value;
};
+
+const handleGridReady = (params: any) => {
+ const w = window as any
+ if (!w.__agGridApis) w.__agGridApis = {}
+ w.__agGridApis = params.api
+}
@@ -329,7 +473,8 @@ const processCellFromClipboard = (params: any) => {
-
-
diff --git a/src/components/views/pricingView/InvestmentScalePricingPane.vue b/src/components/views/pricingView/InvestmentScalePricingPane.vue
index 276437f..d4fd485 100644
--- a/src/components/views/pricingView/InvestmentScalePricingPane.vue
+++ b/src/components/views/pricingView/InvestmentScalePricingPane.vue
@@ -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 { getBasicFeeFromScale, majorList, serviceList } from '@/sql'
+import { getBasicFeeFromScale, majorList } from '@/sql'
import { myTheme, gridOptions } from '@/lib/diyAgGridOptions'
import { addNumbers, 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, loadMajorFactorMap } from '@/lib/xmFactorDefaults'
import { AG_GRID_LOCALE_CN } from '@ag-grid-community/locale';
// 精简的边框配置(细线条+浅灰色,弱化分割线视觉)
@@ -53,6 +56,25 @@ const HT_DB_KEY = computed(() => `ht-info-v3-${props.contractId}`)
const PRICING_CLEAR_SKIP_PREFIX = 'pricing-clear-skip:'
const PRICING_FORCE_DEFAULT_PREFIX = 'pricing-force-default:'
const pricingPaneReloadStore = usePricingPaneReloadStore()
+const consultCategoryFactorMap = ref