This commit is contained in:
wintsa 2026-02-27 18:05:10 +08:00
parent badf131dde
commit 9849801e46
2 changed files with 78 additions and 69 deletions

View File

@ -5,7 +5,7 @@ import type { ColDef } from 'ag-grid-community'
import localforage from 'localforage'
import { serviceList, taskList } from '@/sql'
import { myTheme, gridOptions } from '@/lib/diyAgGridOptions'
import { decimalAggSum, sumByNumber } from '@/lib/decimal'
import { decimalAggSum } from '@/lib/decimal'
import { usePricingPaneReloadStore } from '@/pinia/pricingPaneReload'
import { AG_GRID_LOCALE_CN } from '@ag-grid-community/locale';
@ -15,6 +15,7 @@ interface DetailRow {
taskCode: string
taskName: string
unit: string
conversion: number | null
workload: number | null
budgetBase: string
budgetReferenceUnitPrice: string
@ -68,9 +69,11 @@ type taskLite = {
name: string
basicParam: string
unit: string
conversion: number | null
maxPrice: number | null
minPrice: number | null
defPrice: number | null
desc: string | null
}
const defaultConsultCategoryFactor = computed<number | null>(() => {
@ -106,6 +109,7 @@ const buildDefaultRows = (): DetailRow[] => {
taskCode: task.code,
taskName: task.name,
unit: task.unit || '',
conversion: typeof task.conversion === 'number' && Number.isFinite(task.conversion) ? task.conversion : null,
workload: null,
budgetBase: task.basicParam || '',
budgetReferenceUnitPrice: formatTaskReferenceUnitPrice(task),
@ -113,7 +117,7 @@ const buildDefaultRows = (): DetailRow[] => {
typeof task.defPrice === 'number' && Number.isFinite(task.defPrice) ? task.defPrice : null,
consultCategoryFactor: defaultConsultCategoryFactor.value,
serviceFee: null,
remark: '',
remark: task.desc|| '',
path: [rowId]
})
}
@ -125,6 +129,7 @@ const buildDefaultRows = (): DetailRow[] => {
taskCode: '无',
taskName: '无',
unit: '',
conversion: null,
workload: null,
budgetBase: '无',
budgetReferenceUnitPrice: '无',
@ -171,6 +176,27 @@ const parseNumberOrNull = (value: unknown) => {
return Number.isFinite(v) ? v : null
}
const calcServiceFee = (row: DetailRow | undefined) => {
if (!row || isNoTaskRow(row)) return null
const price = row.budgetAdoptedUnitPrice
const conversion = row.conversion
const workload = row.workload
const factor = row.consultCategoryFactor
if (
typeof price !== 'number' ||
!Number.isFinite(price) ||
typeof conversion !== 'number' ||
!Number.isFinite(conversion) ||
typeof workload !== 'number' ||
!Number.isFinite(workload) ||
typeof factor !== 'number' ||
!Number.isFinite(factor)
) {
return null
}
return price * conversion * workload * factor
}
const formatEditableNumber = (params: any) => {
if (isNoTaskRow(params.data)) return '无'
if (!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')) {
@ -180,52 +206,45 @@ const formatEditableNumber = (params: any) => {
return Number(params.value).toFixed(2)
}
const isRepeatedTaskNameRow = (params: any) => {
const rowIndex = params.node?.rowIndex
if (typeof rowIndex !== 'number' || rowIndex <= 0) return false
const prevData = params.api.getDisplayedRowAtIndex(rowIndex - 1)?.data as DetailRow | undefined
return prevData?.taskName === params.data?.taskName
}
const spanRowsByTaskName = (params: any) => {
const rowA = params?.nodeA?.data as DetailRow | undefined
const rowB = params?.nodeB?.data as DetailRow | undefined
// debugger
if (!rowA || !rowB) return false
if (isNoTaskRow(rowA) || isNoTaskRow(rowB)) return false
const getTaskNameRowSpan = (params: any) => {
if (params.node?.rowPinned) return 1
if (isRepeatedTaskNameRow(params)) return 1
const currentName = params.data?.taskName
if (!currentName) return 1
let span = 1
let offset = 1
while (true) {
const nextData = params.api.getDisplayedRowAtIndex((params.node?.rowIndex ?? 0) + offset)?.data as DetailRow | undefined
if (!nextData || nextData.taskName !== currentName) break
span += 1
offset += 1
}
return span
return Boolean(rowA.taskName) && Boolean(rowA.budgetBase) && rowA.taskName === rowB.taskName && rowA.budgetBase === rowB.budgetBase
}
const columnDefs: ColDef<DetailRow>[] = [
{
headerName: '编码',
field: 'taskCode',
minWidth: 150,
width: 170,
minWidth: 100,
width: 120,
pinned: 'left',
valueFormatter: params => params.value || ''
},
{
headerName: '名称',
field: 'taskName',
minWidth: 220,
width: 260,
minWidth: 150,
width: 220,
pinned: 'left',
rowSpan: params => getTaskNameRowSpan(params),
valueFormatter: params => (isRepeatedTaskNameRow(params) ? '' : params.value || '')
autoHeight: true,
spanRows: true,
valueFormatter: params => params.value || ''
},
{
headerName: '预算基数',
field: 'budgetBase',
minWidth: 170,
flex: 1,
minWidth: 150,
autoHeight: true,
width: 180,
pinned: 'left',
spanRows: spanRowsByTaskName,
valueFormatter: params => params.value || ''
},
{
@ -300,18 +319,10 @@ const columnDefs: ColDef<DetailRow>[] = [
field: 'serviceFee',
minWidth: 150,
flex: 1,
editable: params => !params.node?.group && !params.node?.rowPinned && !isNoTaskRow(params.data),
cellClass: params => (!params.node?.group && !params.node?.rowPinned ? 'editable-cell-line' : ''),
cellClassRules: {
'editable-cell-empty': params =>
!params.node?.group &&
!params.node?.rowPinned &&
!isNoTaskRow(params.data) &&
(params.value == null || params.value === '')
},
editable: false,
valueGetter: params => calcServiceFee(params.data),
aggFunc: decimalAggSum,
valueParser: params => parseNumberOrNull(params.newValue),
valueFormatter: formatEditableNumber
// valueFormatter: formatEditableNumber
},
{
headerName: '说明',
@ -323,10 +334,9 @@ const columnDefs: ColDef<DetailRow>[] = [
autoHeight: true,
cellStyle: { whiteSpace: 'normal', lineHeight: '1.4' },
editable: params => !params.node?.group && !params.node?.rowPinned && !isNoTaskRow(params.data),
editable: false,
valueFormatter: params => {
if (isNoTaskRow(params.data)) return '无'
if (!params.node?.group && !params.node?.rowPinned && !params.value) return '点击输入'
return params.value || ''
},
cellClass: params => (!params.node?.group && !params.node?.rowPinned ? 'editable-cell-line remark-wrap-cell' : ''),
@ -340,26 +350,6 @@ const columnDefs: ColDef<DetailRow>[] = [
}
]
const totalWorkload = computed(() => sumByNumber(detailRows.value, row => row.workload))
const totalServiceFee = computed(() => sumByNumber(detailRows.value, row => row.serviceFee))
const pinnedTopRowData = computed(() => [
{
id: 'pinned-total-row',
taskCode: '',
taskName: '',
unit: '',
workload: totalWorkload.value,
budgetBase: '',
budgetReferenceUnitPrice: '',
budgetAdoptedUnitPrice: null,
consultCategoryFactor: null,
serviceFee: totalServiceFee.value,
remark: '',
path: ['TOTAL']
}
])
const saveToIndexedDB = async () => {
@ -441,6 +431,18 @@ const processCellFromClipboard = (params: any) => {
}
return params.value;
};
const mydiyTheme = myTheme.withParams({
rowBorder: {
style: "solid",
width: 0.8,
color: "#d8d8dd"
},
columnBorder: {
style: "solid",
width: 0.8,
color: "#d8d8dd"
}
})
</script>
<template>
@ -454,11 +456,15 @@ const processCellFromClipboard = (params: any) => {
</div>
<div class="ag-theme-quartz h-full min-h-0 w-full flex-1">
<AgGridVue :style="{ height: '100%' }" :rowData="detailRows" :pinnedTopRowData="pinnedTopRowData"
:columnDefs="columnDefs" :gridOptions="gridOptions" :theme="myTheme" :treeData="false"
<AgGridVue :style="{ height: '100%' }" :rowData="detailRows"
:columnDefs="columnDefs" :gridOptions="gridOptions" :theme="mydiyTheme" :treeData="false"
:enableCellSpan="true"
@cell-value-changed="handleCellValueChanged" :suppressColumnVirtualisation="true"
:suppressRowVirtualisation="true" :suppressRowTransform="true"
:cellSelection="{ handle: { mode: 'range' } }" :enableClipboard="true"
:suppressRowVirtualisation="true"
:enableClipboard="true"
:localeText="AG_GRID_LOCALE_CN" :tooltipShowDelay="500" :headerHeight="50"
:processCellForClipboard="processCellForClipboard" :processCellFromClipboard="processCellFromClipboard"
:undoRedoCellEditing="true" :undoRedoCellEditingLimit="20" />
@ -466,3 +472,6 @@ const processCellFromClipboard = (params: any) => {
</div>
</div>
</template>
<!-- :rowSelection="'multiple'"
:enableClickSelection="false" -->
<!-- :suppressRowTransform="true" -->

View File

@ -15,7 +15,7 @@ export const myTheme = themeQuartz.withParams({
wrapperBorder: false,
// 表头样式(柔和浅蓝,无加粗,更轻盈)
headerBackgroundColor: "#f9fafb", // 极浅的背景色,替代深一点的 #e7f3fc
headerBackgroundColor: "#f0f2f3", // 极浅的背景色,替代深一点的 #e7f3fc
headerTextColor: "#374151", // 深灰色文字,比纯黑更柔和
headerFontSize: 15, // 字体稍大一点,更易读
headerFontWeight: "normal", // 取消加粗,降低视觉重量