fix
This commit is contained in:
1 parent
75f293f877
commit
d8f8b629d2
8 files changed
+350
-956
No files matched your search
@@ -1,13 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onActivated, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { computed, onActivated, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { AgGridVue } from 'ag-grid-vue3'
|
||||
import type { CellValueChangedEvent, ColDef } from 'ag-grid-community'
|
||||
import type { CellValueChangedEvent, ColDef, GridApi, GridReadyEvent } 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'
|
||||
import { SwitchRoot, SwitchThumb } from 'reka-ui'
|
||||
|
||||
interface DetailRow {
|
||||
id: string
|
||||
@@ -20,12 +21,19 @@ interface DetailRow {
|
||||
amount: number | null
|
||||
landArea: number | null
|
||||
path: string[]
|
||||
hide?: boolean
|
||||
}
|
||||
|
||||
interface XmBaseInfoState {
|
||||
projectIndustry?: string
|
||||
}
|
||||
|
||||
interface GridPersistState {
|
||||
detailRows?: DetailRow[]
|
||||
roughCalcEnabled?: boolean
|
||||
totalAmount?: number | null
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
title: string
|
||||
rowData: DetailRow[]
|
||||
@@ -34,6 +42,7 @@ const props = defineProps<{
|
||||
|
||||
const BASE_INFO_KEY = 'xm-base-info-v1'
|
||||
let persistTimer: ReturnType<typeof setTimeout> | null = null
|
||||
const gridApi = ref<GridApi<DetailRow> | null>(null)
|
||||
const activeIndustryId = ref('')
|
||||
const industryNameMap = new Map(
|
||||
industryTypeList.flatMap(item => [
|
||||
@@ -45,6 +54,18 @@ const totalLabel = computed(() => {
|
||||
const industryName = industryNameMap.get(activeIndustryId.value.trim()) || ''
|
||||
return industryName ? `${industryName}总投资` : '总投资'
|
||||
})
|
||||
const roughCalcEnabled = ref(false)
|
||||
const visibleRowData = computed(() => props.rowData.filter(row => !row.hide))
|
||||
|
||||
const refreshPinnedTotalLabelCell = () => {
|
||||
if (!gridApi.value) return
|
||||
const pinnedTopNode = gridApi.value.getPinnedTopRow(0)
|
||||
if (!pinnedTopNode) return
|
||||
gridApi.value.refreshCells({
|
||||
rowNodes: [pinnedTopNode],
|
||||
force: true
|
||||
})
|
||||
}
|
||||
|
||||
const columnDefs: ColDef<DetailRow>[] = [
|
||||
{
|
||||
@@ -53,14 +74,21 @@ const columnDefs: ColDef<DetailRow>[] = [
|
||||
headerClass: 'ag-right-aligned-header',
|
||||
minWidth: 100,
|
||||
flex: 1,
|
||||
editable: params => !params.node?.group && !params.node?.rowPinned && Boolean(params.data?.hasCost),
|
||||
editable: params => {
|
||||
if (roughCalcEnabled.value) return Boolean(params.node?.rowPinned)
|
||||
return !params.node?.group && !params.node?.rowPinned && Boolean(params.data?.hasCost)
|
||||
},
|
||||
cellClass: params =>
|
||||
!params.node?.group && !params.node?.rowPinned && params.data?.hasCost
|
||||
roughCalcEnabled.value && params.node?.rowPinned
|
||||
? 'ag-right-aligned-cell editable-cell-line'
|
||||
: !roughCalcEnabled.value && !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 === '')
|
||||
roughCalcEnabled.value && params.node?.rowPinned
|
||||
? params.value == null || params.value === ''
|
||||
: !params.node?.group && !params.node?.rowPinned && Boolean(params.data?.hasCost) && (params.value == null || params.value === '')
|
||||
},
|
||||
aggFunc: decimalAggSum,
|
||||
valueParser: params => {
|
||||
@@ -69,6 +97,11 @@ const columnDefs: ColDef<DetailRow>[] = [
|
||||
return Number.isFinite(v) ? roundTo(v, 2) : null
|
||||
},
|
||||
valueFormatter: params => {
|
||||
if (roughCalcEnabled.value) {
|
||||
if (!params.node?.rowPinned) return ''
|
||||
if (params.value == null || params.value === '') return '点击输入'
|
||||
return formatThousands(params.value)
|
||||
}
|
||||
if (!params.node?.group && !params.node?.rowPinned && !params.data?.hasCost) {
|
||||
return ''
|
||||
}
|
||||
@@ -85,7 +118,7 @@ const columnDefs: ColDef<DetailRow>[] = [
|
||||
headerClass: 'ag-right-aligned-header',
|
||||
minWidth: 100,
|
||||
flex: 1,
|
||||
editable: params => !params.node?.group && !params.node?.rowPinned && Boolean(params.data?.hasArea),
|
||||
editable: params => !roughCalcEnabled.value && !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'
|
||||
@@ -100,6 +133,9 @@ const columnDefs: ColDef<DetailRow>[] = [
|
||||
return Number.isFinite(v) ? roundTo(v, 3) : null
|
||||
},
|
||||
valueFormatter: params => {
|
||||
if (roughCalcEnabled.value) {
|
||||
return ''
|
||||
}
|
||||
if (!params.node?.group && !params.node?.rowPinned && !params.data?.hasArea) {
|
||||
return ''
|
||||
}
|
||||
@@ -129,18 +165,16 @@ const autoGroupColumnDef: ColDef = {
|
||||
}
|
||||
}
|
||||
|
||||
const totalAmount = computed(() => sumByNumber(props.rowData, row => row.amount))
|
||||
|
||||
const pinnedTopRowData = computed<DetailRow[]>(() => [
|
||||
const pinnedTopRowData = ref<DetailRow[]>([
|
||||
{
|
||||
id: 'pinned-total-row',
|
||||
groupCode: '',
|
||||
groupName: '',
|
||||
majorCode: '',
|
||||
majorName: totalLabel.value,
|
||||
majorName: '',
|
||||
hasCost: false,
|
||||
hasArea: false,
|
||||
amount: totalAmount.value,
|
||||
amount: null,
|
||||
landArea: null,
|
||||
path: ['TOTAL']
|
||||
}
|
||||
@@ -148,19 +182,74 @@ const pinnedTopRowData = computed<DetailRow[]>(() => [
|
||||
|
||||
const saveToIndexedDB = async () => {
|
||||
try {
|
||||
await localforage.setItem(props.dbKey, {
|
||||
detailRows: JSON.parse(JSON.stringify(props.rowData))
|
||||
})
|
||||
const payload: GridPersistState = {
|
||||
detailRows: props.rowData.map(row => ({
|
||||
...JSON.parse(JSON.stringify(row)),
|
||||
hide: Boolean(row.hide)
|
||||
}))
|
||||
}
|
||||
payload.roughCalcEnabled = roughCalcEnabled.value
|
||||
payload.totalAmount = pinnedTopRowData.value[0].amount
|
||||
await localforage.setItem(props.dbKey, payload)
|
||||
} catch (error) {
|
||||
console.error('saveToIndexedDB failed:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const onCellValueChanged = (_event: CellValueChangedEvent) => {
|
||||
const schedulePersist = () => {
|
||||
if (persistTimer) clearTimeout(persistTimer)
|
||||
persistTimer = setTimeout(() => {
|
||||
void saveToIndexedDB()
|
||||
}, 1000)
|
||||
}, 600)
|
||||
}
|
||||
|
||||
const setDetailRowsHidden = (hidden: boolean) => {
|
||||
for (const row of props.rowData) {
|
||||
row.hide = hidden
|
||||
}
|
||||
}
|
||||
|
||||
const onRoughCalcSwitch = (checked: boolean) => {
|
||||
gridApi.value?.stopEditing(true)
|
||||
roughCalcEnabled.value = checked
|
||||
setDetailRowsHidden(checked)
|
||||
if (!checked) {
|
||||
syncPinnedTotalForNormalMode()
|
||||
}else{
|
||||
pinnedTopRowData.value[0].amount = null
|
||||
const pinnedTopNode = gridApi.value?.getPinnedTopRow(0)
|
||||
if (pinnedTopNode) {
|
||||
pinnedTopNode.setDataValue('amount', null)
|
||||
}
|
||||
}
|
||||
schedulePersist()
|
||||
}
|
||||
|
||||
|
||||
|
||||
const onCellValueChanged = (event: CellValueChangedEvent) => {
|
||||
if (roughCalcEnabled.value && event.node?.rowPinned && event.colDef.field === 'amount') {
|
||||
if (typeof event.newValue === 'number') {
|
||||
pinnedTopRowData.value[0].amount = roundTo(event.newValue, 2)
|
||||
} else {
|
||||
const parsed = Number(event.newValue)
|
||||
pinnedTopRowData.value[0].amount = Number.isFinite(parsed) ? roundTo(parsed, 2) : null
|
||||
}
|
||||
|
||||
} else if (!roughCalcEnabled.value) {
|
||||
syncPinnedTotalForNormalMode()
|
||||
}
|
||||
schedulePersist()
|
||||
}
|
||||
|
||||
const onGridReady = (event: GridReadyEvent<DetailRow>) => {
|
||||
gridApi.value = event.api
|
||||
void loadIndustryFromBaseInfo()
|
||||
|
||||
|
||||
void loadGridPersistState()
|
||||
void refreshPinnedTotalLabelCell()
|
||||
|
||||
}
|
||||
|
||||
const processCellForClipboard = (params: any) => {
|
||||
@@ -191,16 +280,69 @@ const loadIndustryFromBaseInfo = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
void loadIndustryFromBaseInfo()
|
||||
const loadGridPersistState = async () => {
|
||||
try {
|
||||
const data = await localforage.getItem<GridPersistState>(props.dbKey)
|
||||
roughCalcEnabled.value = Boolean(data?.roughCalcEnabled)
|
||||
const detailRows = Array.isArray(data?.detailRows) ? data.detailRows : []
|
||||
const detailRowById = new Map(detailRows.map(row => [row.id, row]))
|
||||
for (const row of props.rowData) {
|
||||
const persisted = detailRowById.get(row.id)
|
||||
if (!persisted) {
|
||||
row.hide = roughCalcEnabled.value
|
||||
continue
|
||||
}
|
||||
row.amount = typeof persisted.amount === 'number' ? roundTo(persisted.amount, 2) : null
|
||||
row.landArea = typeof persisted.landArea === 'number' ? roundTo(persisted.landArea, 3) : null
|
||||
row.hide = typeof persisted.hide === 'boolean' ? persisted.hide : roughCalcEnabled.value
|
||||
}
|
||||
pinnedTopRowData.value[0].amount = typeof data?.totalAmount === 'number' ? data.totalAmount : null
|
||||
|
||||
const pinnedTopNode = (gridApi as any).value.getPinnedTopRow(0)
|
||||
if (pinnedTopNode) {
|
||||
pinnedTopNode.setDataValue('amount', pinnedTopRowData.value[0].amount)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('loadGridPersistState failed:', error)
|
||||
roughCalcEnabled.value = false
|
||||
pinnedTopRowData.value[0].amount= null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
watch(totalLabel, () => {
|
||||
refreshPinnedTotalLabelCell()
|
||||
})
|
||||
|
||||
onActivated(() => {
|
||||
void loadIndustryFromBaseInfo()
|
||||
})
|
||||
const syncPinnedTotalForNormalMode = () => {
|
||||
if (roughCalcEnabled.value) return
|
||||
|
||||
if (!gridApi.value) {
|
||||
pinnedTopRowData.value[0].amount = sumByNumber(props.rowData, row => row.amount)
|
||||
return
|
||||
}
|
||||
let total = 0
|
||||
let hasValue = false
|
||||
|
||||
props.rowData.forEach(node => {
|
||||
|
||||
const amount = node.amount
|
||||
if (typeof amount === 'number' && Number.isFinite(amount)) {
|
||||
total += amount
|
||||
hasValue = true
|
||||
}
|
||||
})
|
||||
pinnedTopRowData.value[0].amount = hasValue ? roundTo(total, 2) : null
|
||||
const pinnedTopNode = gridApi.value.getPinnedTopRow(0)
|
||||
if (pinnedTopNode) {
|
||||
pinnedTopNode.setDataValue('amount', hasValue ? roundTo(total, 2) : null)
|
||||
}
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (persistTimer) clearTimeout(persistTimer)
|
||||
gridApi.value = null
|
||||
void saveToIndexedDB()
|
||||
})
|
||||
</script>
|
||||
@@ -212,18 +354,30 @@ onBeforeUnmount(() => {
|
||||
<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 class="flex items-center gap-2">
|
||||
<span class=" text-xs text-muted-foreground">粗略计算</span>
|
||||
<SwitchRoot
|
||||
class="cursor-pointer peer h-5 w-9 shrink-0 rounded-full border border-transparent bg-muted shadow-sm transition-colors data-[state=checked]:bg-primary"
|
||||
:modelValue="roughCalcEnabled"
|
||||
@update:modelValue="onRoughCalcSwitch"
|
||||
>
|
||||
<SwitchThumb
|
||||
class="block h-4 w-4 translate-x-0.5 rounded-full bg-background shadow transition-transform data-[state=checked]:translate-x-4"
|
||||
/>
|
||||
</SwitchRoot>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ag-theme-quartz w-full flex-1 min-h-0 h-full">
|
||||
<AgGridVue
|
||||
:style="{ height: '100%' }"
|
||||
:rowData="props.rowData"
|
||||
:rowData="visibleRowData"
|
||||
:pinnedTopRowData="pinnedTopRowData"
|
||||
:columnDefs="columnDefs"
|
||||
:autoGroupColumnDef="autoGroupColumnDef"
|
||||
:gridOptions="gridOptions"
|
||||
:theme="myTheme"
|
||||
@grid-ready="onGridReady"
|
||||
@cell-value-changed="onCellValueChanged"
|
||||
:suppressColumnVirtualisation="true"
|
||||
:suppressRowVirtualisation="true"
|
||||
|
||||
Reference in new issue
Block a user