245 lines
7.4 KiB
Vue
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>
|