calculator2026/src/components/common/xmCommonAgGrid.vue
2026-03-05 17:58:38 +08:00

245 lines
7.4 KiB
Vue

<script setup lang="ts">
import { computed, onActivated, onBeforeUnmount, onMounted, ref } from 'vue'
import { AgGridVue } from 'ag-grid-vue3'
import type { CellValueChangedEvent, ColDef } from 'ag-grid-community'
import { AG_GRID_LOCALE_CN } from '@ag-grid-community/locale'
import { myTheme, gridOptions } from '@/lib/diyAgGridOptions'
import localforage from 'localforage'
import { decimalAggSum, roundTo, sumByNumber } from '@/lib/decimal'
import { formatThousands } from '@/lib/numberFormat'
import { industryTypeList } from '@/sql'
interface DetailRow {
id: string
groupCode: string
groupName: string
majorCode: string
majorName: string
hasCost: boolean
hasArea: boolean
amount: number | null
landArea: number | null
path: string[]
}
interface XmBaseInfoState {
projectIndustry?: string
}
const props = defineProps<{
title: string
rowData: DetailRow[]
dbKey: string
}>()
const BASE_INFO_KEY = 'xm-base-info-v1'
let persistTimer: ReturnType<typeof setTimeout> | null = null
const activeIndustryId = ref('')
const industryNameMap = new Map(
industryTypeList.flatMap(item => [
[String(item.id).trim(), item.name],
[String(item.type).trim(), item.name]
])
)
const totalLabel = computed(() => {
const industryName = industryNameMap.get(activeIndustryId.value.trim()) || ''
return industryName ? `${industryName}总投资` : '总投资'
})
const columnDefs: ColDef<DetailRow>[] = [
{
headerName: '造价金额(万元)',
field: 'amount',
headerClass: 'ag-right-aligned-header',
minWidth: 100,
flex: 1,
editable: params => !params.node?.group && !params.node?.rowPinned && Boolean(params.data?.hasCost),
cellClass: params =>
!params.node?.group && !params.node?.rowPinned && params.data?.hasCost
? 'ag-right-aligned-cell editable-cell-line'
: 'ag-right-aligned-cell',
cellClassRules: {
'editable-cell-empty': params =>
!params.node?.group && !params.node?.rowPinned && Boolean(params.data?.hasCost) && (params.value == null || params.value === '')
},
aggFunc: decimalAggSum,
valueParser: params => {
if (params.newValue === '' || params.newValue == null) return null
const v = Number(params.newValue)
return Number.isFinite(v) ? roundTo(v, 2) : null
},
valueFormatter: params => {
if (!params.node?.group && !params.node?.rowPinned && !params.data?.hasCost) {
return ''
}
if (!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')) {
return '点击输入'
}
if (params.value == null) return ''
return formatThousands(params.value)
}
},
{
headerName: '用地面积(亩)',
field: 'landArea',
headerClass: 'ag-right-aligned-header',
minWidth: 100,
flex: 1,
editable: params => !params.node?.group && !params.node?.rowPinned && Boolean(params.data?.hasArea),
cellClass: params =>
!params.node?.group && !params.node?.rowPinned && params.data?.hasArea
? 'ag-right-aligned-cell editable-cell-line'
: 'ag-right-aligned-cell',
cellClassRules: {
'editable-cell-empty': params =>
!params.node?.group && !params.node?.rowPinned && Boolean(params.data?.hasArea) && (params.value == null || params.value === '')
},
valueParser: params => {
if (params.newValue === '' || params.newValue == null) return null
const v = Number(params.newValue)
return Number.isFinite(v) ? roundTo(v, 3) : null
},
valueFormatter: params => {
if (!params.node?.group && !params.node?.rowPinned && !params.data?.hasArea) {
return ''
}
if (!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')) {
return '点击输入'
}
if (params.value == null) return ''
return formatThousands(params.value, 3)
}
}
]
const autoGroupColumnDef: ColDef = {
headerName: '专业编码以及工程专业名称',
minWidth: 200,
flex: 2,
cellRendererParams: {
suppressCount: true
},
valueFormatter: params => {
if (params.node?.rowPinned) return totalLabel.value
return String(params.value || '')
},
tooltipValueGetter: params => {
if (params.node?.rowPinned) return totalLabel.value
return String(params.value || '')
}
}
const totalAmount = computed(() => sumByNumber(props.rowData, row => row.amount))
const pinnedTopRowData = computed<DetailRow[]>(() => [
{
id: 'pinned-total-row',
groupCode: '',
groupName: '',
majorCode: '',
majorName: totalLabel.value,
hasCost: false,
hasArea: false,
amount: totalAmount.value,
landArea: null,
path: ['TOTAL']
}
])
const saveToIndexedDB = async () => {
try {
await localforage.setItem(props.dbKey, {
detailRows: JSON.parse(JSON.stringify(props.rowData))
})
} catch (error) {
console.error('saveToIndexedDB failed:', error)
}
}
const onCellValueChanged = (_event: CellValueChangedEvent) => {
if (persistTimer) clearTimeout(persistTimer)
persistTimer = setTimeout(() => {
void saveToIndexedDB()
}, 1000)
}
const processCellForClipboard = (params: any) => {
if (Array.isArray(params.value)) {
return JSON.stringify(params.value)
}
return params.value
}
const processCellFromClipboard = (params: any) => {
try {
const parsed = JSON.parse(params.value)
if (Array.isArray(parsed)) return parsed
} catch (error) {
// no-op
}
return params.value
}
const loadIndustryFromBaseInfo = async () => {
try {
const baseInfo = await localforage.getItem<XmBaseInfoState>(BASE_INFO_KEY)
activeIndustryId.value =
typeof baseInfo?.projectIndustry === 'string' ? baseInfo.projectIndustry.trim() : ''
} catch (error) {
console.error('loadIndustryFromBaseInfo failed:', error)
activeIndustryId.value = ''
}
}
onMounted(() => {
void loadIndustryFromBaseInfo()
})
onActivated(() => {
void loadIndustryFromBaseInfo()
})
onBeforeUnmount(() => {
if (persistTimer) clearTimeout(persistTimer)
void saveToIndexedDB()
})
</script>
<template>
<div class="h-full">
<div class="rounded-lg border bg-card xmMx scroll-mt-3 flex flex-col overflow-hidden h-full">
<div class="flex items-center justify-between border-b px-4 py-3">
<h3 class="text-sm font-semibold text-foreground cursor-pointer select-none transition-colors hover:text-primary">
{{ props.title }}
</h3>
<div class="text-xs text-muted-foreground"></div>
</div>
<div class="ag-theme-quartz w-full flex-1 min-h-0 h-full">
<AgGridVue
:style="{ height: '100%' }"
:rowData="props.rowData"
:pinnedTopRowData="pinnedTopRowData"
:columnDefs="columnDefs"
:autoGroupColumnDef="autoGroupColumnDef"
:gridOptions="gridOptions"
:theme="myTheme"
@cell-value-changed="onCellValueChanged"
:suppressColumnVirtualisation="true"
:suppressRowVirtualisation="true"
:cellSelection="{ handle: { mode: 'range' } }"
:enableClipboard="true"
:localeText="AG_GRID_LOCALE_CN"
:tooltipShowDelay="500"
:headerHeight="50"
:suppressHorizontalScroll="true"
:processCellForClipboard="processCellForClipboard"
:processCellFromClipboard="processCellFromClipboard"
:undoRedoCellEditing="true"
:undoRedoCellEditingLimit="20"
/>
</div>
</div>
</div>
</template>