fix
This commit is contained in:
1 parent
d8f8b629d2
commit
626513bc21
4 files changed
+338
-471
No files matched your search
@@ -7,9 +7,207 @@ 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 { industryTypeList, getMajorDictEntries, isMajorIdInIndustryScope } from '@/sql'
|
||||
import { SwitchRoot, SwitchThumb } from 'reka-ui'
|
||||
|
||||
|
||||
|
||||
interface DictLeaf {
|
||||
id: string
|
||||
code: string
|
||||
name: string
|
||||
hasCost: boolean
|
||||
|
||||
hasArea: boolean
|
||||
}
|
||||
|
||||
interface DictGroup {
|
||||
id: string
|
||||
code: string
|
||||
name: string
|
||||
children: DictLeaf[]
|
||||
}
|
||||
|
||||
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 XmScaleState {
|
||||
detailRows?: DetailRow[]
|
||||
totalAmount: number
|
||||
roughCalcEnabled: boolean
|
||||
|
||||
}
|
||||
interface XmBaseInfoState {
|
||||
projectIndustry?: string
|
||||
}
|
||||
|
||||
const BASE_INFO_KEY = 'xm-base-info-v1'
|
||||
type MajorLite = { code: string; name: string; hasCost?: boolean; hasArea?: boolean }
|
||||
|
||||
const detailRows = ref<DetailRow[]>([])
|
||||
const detailDict = ref<DictGroup[]>([])
|
||||
|
||||
const majorEntries = getMajorDictEntries().map(({ id, item }) => [id, item] as [string, MajorLite])
|
||||
const majorIdAliasMap = new Map(getMajorDictEntries().map(({ rawId, id }) => [rawId, id]))
|
||||
|
||||
const buildDetailDict = (entries: Array<[string, MajorLite]>) => {
|
||||
const groupMap = new Map<string, DictGroup>()
|
||||
const groupOrder: string[] = []
|
||||
const codeLookup = new Map(entries.map(([key, item]) => [item.code, { id: key, ...item }]))
|
||||
|
||||
for (const [key, item] of entries) {
|
||||
const isGroup = !item.code.includes('-')
|
||||
if (isGroup) {
|
||||
if (!groupMap.has(item.code)) groupOrder.push(item.code)
|
||||
groupMap.set(item.code, {
|
||||
id: key,
|
||||
code: item.code,
|
||||
name: item.name,
|
||||
children: []
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
const parentCode = item.code.split('-')[0]
|
||||
if (!groupMap.has(parentCode)) {
|
||||
const parent = codeLookup.get(parentCode)
|
||||
if (!groupOrder.includes(parentCode)) groupOrder.push(parentCode)
|
||||
groupMap.set(parentCode, {
|
||||
id: parent?.id || `group-${parentCode}`,
|
||||
code: parentCode,
|
||||
name: parent?.name || parentCode,
|
||||
children: []
|
||||
})
|
||||
}
|
||||
groupMap.get(parentCode)!.children.push({
|
||||
id: key,
|
||||
code: item.code,
|
||||
name: item.name,
|
||||
|
||||
hasCost: item.hasCost !== false,
|
||||
hasArea: item.hasArea !== false
|
||||
})
|
||||
}
|
||||
|
||||
return groupOrder.map(code => groupMap.get(code)).filter((group): group is DictGroup => Boolean(group))
|
||||
}
|
||||
|
||||
|
||||
|
||||
const buildDefaultRows = (): DetailRow[] => {
|
||||
const rows: DetailRow[] = []
|
||||
for (const group of detailDict.value) {
|
||||
for (const child of group.children) {
|
||||
|
||||
rows.push({
|
||||
id: child.id,
|
||||
groupCode: group.code,
|
||||
groupName: group.name,
|
||||
majorCode: child.code,
|
||||
majorName: child.name,
|
||||
hasCost: child.hasCost,
|
||||
hasArea: child.hasArea,
|
||||
amount: null,
|
||||
landArea: null,
|
||||
path: [`${group.code} ${group.name}`, `${child.code} ${child.name}`]
|
||||
})
|
||||
}
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
const mergeWithDictRows = (rowsFromDb: DetailRow[] | undefined): DetailRow[] => {
|
||||
const dbValueMap = new Map<string, DetailRow>()
|
||||
for (const row of rowsFromDb || []) {
|
||||
const rowId = String(row.id)
|
||||
dbValueMap.set(rowId, row)
|
||||
const aliasId = majorIdAliasMap.get(rowId)
|
||||
if (aliasId && !dbValueMap.has(aliasId)) {
|
||||
dbValueMap.set(aliasId, row)
|
||||
}
|
||||
}
|
||||
|
||||
return buildDefaultRows().map(row => {
|
||||
const fromDb = dbValueMap.get(row.id)
|
||||
if (!fromDb) return row
|
||||
|
||||
return {
|
||||
...row,
|
||||
hide: fromDb.hide,
|
||||
amount: row.hasCost && typeof fromDb.amount === 'number' ? fromDb.amount : null,
|
||||
landArea: row.hasArea && typeof fromDb.landArea === 'number' ? fromDb.landArea : null
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
const loadFromIndexedDB = async (gridApi: any) => {
|
||||
|
||||
|
||||
try {
|
||||
|
||||
const baseInfo = await localforage.getItem<XmBaseInfoState>(BASE_INFO_KEY)
|
||||
activeIndustryId.value =
|
||||
typeof baseInfo?.projectIndustry === 'string' ? baseInfo.projectIndustry.trim() : ''
|
||||
if (!activeIndustryId.value) {
|
||||
detailDict.value = []
|
||||
return
|
||||
}
|
||||
const filteredEntries = majorEntries.filter(([id]) => isMajorIdInIndustryScope(id, activeIndustryId.value))
|
||||
detailDict.value = buildDetailDict(filteredEntries)
|
||||
if (!activeIndustryId.value) {
|
||||
detailRows.value = []
|
||||
return
|
||||
}
|
||||
|
||||
const data = await localforage.getItem<XmScaleState>(props.dbKey)
|
||||
|
||||
roughCalcEnabled.value = data?.roughCalcEnabled || false
|
||||
const pinnedTopNode = gridApi.getPinnedTopRow(0)
|
||||
if (pinnedTopNode) {
|
||||
pinnedTopNode.setDataValue('amount', data?.totalAmount || null)
|
||||
}
|
||||
//
|
||||
if (data?.detailRows) {
|
||||
detailRows.value = mergeWithDictRows(data.detailRows)
|
||||
return
|
||||
}
|
||||
|
||||
if (props.xmInfoKey) {
|
||||
// 首次创建合同段时,默认继承项目规模信息(同一套专业字典,按 id 对齐)
|
||||
const xmData =
|
||||
(await localforage.getItem<XmScaleState>(props.xmInfoKey))
|
||||
roughCalcEnabled.value = xmData?.roughCalcEnabled || false
|
||||
if (pinnedTopNode) {
|
||||
pinnedTopNode.setDataValue('amount', xmData?.totalAmount || null)
|
||||
}
|
||||
if (xmData?.detailRows) {
|
||||
detailRows.value = mergeWithDictRows(xmData.detailRows)
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
detailRows.value = buildDefaultRows()
|
||||
|
||||
saveToIndexedDB()
|
||||
} catch (error) {
|
||||
console.error('loadFromIndexedDB failed:', error)
|
||||
activeIndustryId.value = ''
|
||||
detailRows.value = []
|
||||
}
|
||||
}
|
||||
|
||||
interface DetailRow {
|
||||
id: string
|
||||
groupCode: string
|
||||
@@ -36,11 +234,10 @@ interface GridPersistState {
|
||||
|
||||
const props = defineProps<{
|
||||
title: string
|
||||
rowData: DetailRow[]
|
||||
dbKey: string
|
||||
xmInfoKey?: string | null
|
||||
}>()
|
||||
|
||||
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('')
|
||||
@@ -55,7 +252,7 @@ const totalLabel = computed(() => {
|
||||
return industryName ? `${industryName}总投资` : '总投资'
|
||||
})
|
||||
const roughCalcEnabled = ref(false)
|
||||
const visibleRowData = computed(() => props.rowData.filter(row => !row.hide))
|
||||
const visibleRowData = computed(() => { return detailRows.value.filter(row => !row.hide) })
|
||||
|
||||
const refreshPinnedTotalLabelCell = () => {
|
||||
if (!gridApi.value) return
|
||||
@@ -82,8 +279,8 @@ const columnDefs: ColDef<DetailRow>[] = [
|
||||
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',
|
||||
? 'ag-right-aligned-cell editable-cell-line'
|
||||
: 'ag-right-aligned-cell',
|
||||
cellClassRules: {
|
||||
'editable-cell-empty': params =>
|
||||
roughCalcEnabled.value && params.node?.rowPinned
|
||||
@@ -183,7 +380,7 @@ const pinnedTopRowData = ref<DetailRow[]>([
|
||||
const saveToIndexedDB = async () => {
|
||||
try {
|
||||
const payload: GridPersistState = {
|
||||
detailRows: props.rowData.map(row => ({
|
||||
detailRows: detailRows.value.map(row => ({
|
||||
...JSON.parse(JSON.stringify(row)),
|
||||
hide: Boolean(row.hide)
|
||||
}))
|
||||
@@ -204,7 +401,7 @@ const schedulePersist = () => {
|
||||
}
|
||||
|
||||
const setDetailRowsHidden = (hidden: boolean) => {
|
||||
for (const row of props.rowData) {
|
||||
for (const row of detailRows.value) {
|
||||
row.hide = hidden
|
||||
}
|
||||
}
|
||||
@@ -215,12 +412,12 @@ const onRoughCalcSwitch = (checked: boolean) => {
|
||||
setDetailRowsHidden(checked)
|
||||
if (!checked) {
|
||||
syncPinnedTotalForNormalMode()
|
||||
}else{
|
||||
pinnedTopRowData.value[0].amount = null
|
||||
const pinnedTopNode = gridApi.value?.getPinnedTopRow(0)
|
||||
if (pinnedTopNode) {
|
||||
pinnedTopNode.setDataValue('amount', null)
|
||||
}
|
||||
} else {
|
||||
pinnedTopRowData.value[0].amount = null
|
||||
const pinnedTopNode = gridApi.value?.getPinnedTopRow(0)
|
||||
if (pinnedTopNode) {
|
||||
pinnedTopNode.setDataValue('amount', null)
|
||||
}
|
||||
}
|
||||
schedulePersist()
|
||||
}
|
||||
@@ -230,10 +427,10 @@ const onRoughCalcSwitch = (checked: boolean) => {
|
||||
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)
|
||||
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
|
||||
pinnedTopRowData.value[0].amount = Number.isFinite(parsed) ? roundTo(parsed, 2) : null
|
||||
}
|
||||
|
||||
} else if (!roughCalcEnabled.value) {
|
||||
@@ -244,11 +441,10 @@ const onCellValueChanged = (event: CellValueChangedEvent) => {
|
||||
|
||||
const onGridReady = (event: GridReadyEvent<DetailRow>) => {
|
||||
gridApi.value = event.api
|
||||
void loadIndustryFromBaseInfo()
|
||||
|
||||
void loadFromIndexedDB(event.api)
|
||||
|
||||
void loadGridPersistState()
|
||||
void refreshPinnedTotalLabelCell()
|
||||
void refreshPinnedTotalLabelCell()
|
||||
|
||||
}
|
||||
|
||||
@@ -269,45 +465,9 @@ const processCellFromClipboard = (params: any) => {
|
||||
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 = ''
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -319,14 +479,14 @@ const syncPinnedTotalForNormalMode = () => {
|
||||
if (roughCalcEnabled.value) return
|
||||
|
||||
if (!gridApi.value) {
|
||||
pinnedTopRowData.value[0].amount = sumByNumber(props.rowData, row => row.amount)
|
||||
pinnedTopRowData.value[0].amount = sumByNumber(detailRows.value, row => row.amount)
|
||||
return
|
||||
}
|
||||
let total = 0
|
||||
let hasValue = false
|
||||
|
||||
props.rowData.forEach(node => {
|
||||
|
||||
detailRows.value.forEach(node => {
|
||||
|
||||
const amount = node.amount
|
||||
if (typeof amount === 'number' && Number.isFinite(amount)) {
|
||||
total += amount
|
||||
@@ -335,9 +495,9 @@ const syncPinnedTotalForNormalMode = () => {
|
||||
})
|
||||
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)
|
||||
}
|
||||
if (pinnedTopNode) {
|
||||
pinnedTopNode.setDataValue('amount', hasValue ? roundTo(total, 2) : null)
|
||||
}
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
@@ -351,47 +511,29 @@ onBeforeUnmount(() => {
|
||||
<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">
|
||||
<h3
|
||||
class="text-sm font-semibold text-foreground cursor-pointer select-none transition-colors hover:text-primary">
|
||||
{{ props.title }}
|
||||
</h3>
|
||||
<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"
|
||||
>
|
||||
: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"
|
||||
/>
|
||||
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="visibleRowData"
|
||||
:pinnedTopRowData="pinnedTopRowData"
|
||||
:columnDefs="columnDefs"
|
||||
:autoGroupColumnDef="autoGroupColumnDef"
|
||||
:gridOptions="gridOptions"
|
||||
:theme="myTheme"
|
||||
@grid-ready="onGridReady"
|
||||
@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"
|
||||
/>
|
||||
<AgGridVue :style="{ height: '100%' }" :rowData="visibleRowData" :pinnedTopRowData="pinnedTopRowData"
|
||||
:columnDefs="columnDefs" :autoGroupColumnDef="autoGroupColumnDef" :gridOptions="gridOptions" :theme="myTheme"
|
||||
@grid-ready="onGridReady" @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>
|
||||
|
||||
Reference in new issue
Block a user