fix bug
This commit is contained in:
1 parent
1910f15564
commit
5bb6609ef8
18 files changed
+1447
-979
No files matched your search
@@ -1,13 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
import { onActivated, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { defineComponent, h, onActivated, onBeforeUnmount, onMounted, ref, watch, type PropType } from 'vue'
|
||||
import { AgGridVue } from 'ag-grid-vue3'
|
||||
import type { ColDef, GridApi, GridOptions, GridReadyEvent } from 'ag-grid-community'
|
||||
import type { ColDef, GridApi, GridOptions, GridReadyEvent, ICellRendererParams } from 'ag-grid-community'
|
||||
import { AG_GRID_LOCALE_CN } from '@ag-grid-community/locale'
|
||||
import localforage from 'localforage'
|
||||
import { myTheme, gridOptions } from '@/lib/diyAgGridOptions'
|
||||
import { parseNumberOrNull } from '@/lib/number'
|
||||
import { formatThousandsFlexible } from '@/lib/numberFormat'
|
||||
import { roundTo, toDecimal } from '@/lib/decimal'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { useHtFeeMethodReloadStore } from '@/pinia/htFeeMethodReload'
|
||||
import { Trash2 } from 'lucide-vue-next'
|
||||
import {
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogOverlay,
|
||||
AlertDialogPortal,
|
||||
AlertDialogRoot,
|
||||
AlertDialogTitle
|
||||
} from 'reka-ui'
|
||||
|
||||
interface FeeRow {
|
||||
id: string
|
||||
@@ -17,6 +30,7 @@ interface FeeRow {
|
||||
unitPrice: number | null
|
||||
budgetFee: number | null
|
||||
remark: string
|
||||
actions?: unknown
|
||||
}
|
||||
|
||||
interface FeeGridState {
|
||||
@@ -26,9 +40,13 @@ interface FeeGridState {
|
||||
const props = defineProps<{
|
||||
title: string
|
||||
storageKey: string
|
||||
syncMainStorageKey?: string
|
||||
syncRowId?: string
|
||||
}>()
|
||||
const htFeeMethodReloadStore = useHtFeeMethodReloadStore()
|
||||
|
||||
const createRowId = () => `fee-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`
|
||||
const SUBTOTAL_ROW_ID = 'fee-subtotal-fixed'
|
||||
|
||||
const createDefaultRow = (): FeeRow => ({
|
||||
id: createRowId(),
|
||||
@@ -40,20 +58,77 @@ const createDefaultRow = (): FeeRow => ({
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const detailRows = ref<FeeRow[]>([createDefaultRow()])
|
||||
const createSubtotalRow = (): FeeRow => ({
|
||||
id: SUBTOTAL_ROW_ID,
|
||||
feeItem: '小计',
|
||||
unit: '',
|
||||
quantity: null,
|
||||
unitPrice: null,
|
||||
budgetFee: 0,
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const isSubtotalRow = (row?: FeeRow | null) => row?.id === SUBTOTAL_ROW_ID
|
||||
|
||||
const ensureSubtotalRow = (rows: FeeRow[]) => {
|
||||
const normalRows = rows.filter(row => !isSubtotalRow(row))
|
||||
if (normalRows.length === 0) return []
|
||||
return [...normalRows, createSubtotalRow()]
|
||||
}
|
||||
|
||||
const detailRows = ref<FeeRow[]>([])
|
||||
const gridApi = ref<GridApi<FeeRow> | null>(null)
|
||||
const deleteConfirmOpen = ref(false)
|
||||
const pendingDeleteRowId = ref<string | null>(null)
|
||||
const pendingDeleteRowName = ref('')
|
||||
|
||||
const addRow = () => {
|
||||
const normalRows = detailRows.value.filter(row => !isSubtotalRow(row))
|
||||
detailRows.value = ensureSubtotalRow([...normalRows, createDefaultRow()])
|
||||
syncComputedValuesToRows()
|
||||
void saveToIndexedDB()
|
||||
}
|
||||
|
||||
const deleteRow = (id: string) => {
|
||||
const normalRows = detailRows.value.filter(row => !isSubtotalRow(row) && row.id !== id)
|
||||
detailRows.value = ensureSubtotalRow(normalRows)
|
||||
syncComputedValuesToRows()
|
||||
void saveToIndexedDB()
|
||||
}
|
||||
|
||||
const requestDeleteRow = (id: string, name?: string) => {
|
||||
pendingDeleteRowId.value = id
|
||||
pendingDeleteRowName.value = String(name || '').trim() || '当前行'
|
||||
deleteConfirmOpen.value = true
|
||||
}
|
||||
|
||||
const handleDeleteConfirmOpenChange = (open: boolean) => {
|
||||
deleteConfirmOpen.value = open
|
||||
}
|
||||
|
||||
const confirmDeleteRow = () => {
|
||||
const id = pendingDeleteRowId.value
|
||||
if (!id) return
|
||||
deleteRow(id)
|
||||
deleteConfirmOpen.value = false
|
||||
pendingDeleteRowId.value = null
|
||||
pendingDeleteRowName.value = ''
|
||||
}
|
||||
|
||||
const formatEditableText = (params: any) => {
|
||||
if (isSubtotalRow(params.data)) return ''
|
||||
if (params.value == null || params.value === '') return '点击输入'
|
||||
return String(params.value)
|
||||
}
|
||||
|
||||
const formatEditableQuantity = (params: any) => {
|
||||
if (isSubtotalRow(params.data)) return ''
|
||||
if (params.value == null || params.value === '') return '点击输入'
|
||||
return formatThousandsFlexible(params.value, 3)
|
||||
}
|
||||
|
||||
const formatEditableUnitPrice = (params: any) => {
|
||||
if (isSubtotalRow(params.data)) return ''
|
||||
if (params.value == null || params.value === '') return '点击输入'
|
||||
return formatThousandsFlexible(params.value, 3)
|
||||
}
|
||||
@@ -64,18 +139,32 @@ const formatReadonlyBudgetFee = (params: any) => {
|
||||
}
|
||||
|
||||
const syncComputedValuesToRows = () => {
|
||||
let totalBudgetFee = 0
|
||||
for (const row of detailRows.value) {
|
||||
if (isSubtotalRow(row)) continue
|
||||
if (row.quantity == null || row.unitPrice == null) {
|
||||
row.budgetFee = null
|
||||
continue
|
||||
}
|
||||
row.budgetFee = roundTo(toDecimal(row.quantity).mul(row.unitPrice), 2)
|
||||
if (typeof row.budgetFee === 'number' && Number.isFinite(row.budgetFee)) {
|
||||
totalBudgetFee = roundTo(toDecimal(totalBudgetFee).add(row.budgetFee), 2)
|
||||
}
|
||||
}
|
||||
const subtotalRow = detailRows.value.find(row => isSubtotalRow(row))
|
||||
if (subtotalRow) {
|
||||
subtotalRow.feeItem = '小计'
|
||||
subtotalRow.unit = ''
|
||||
subtotalRow.quantity = null
|
||||
subtotalRow.unitPrice = null
|
||||
subtotalRow.budgetFee = totalBudgetFee
|
||||
subtotalRow.remark = ''
|
||||
}
|
||||
}
|
||||
|
||||
const mergeWithStoredRows = (rowsFromDb: unknown): FeeRow[] => {
|
||||
if (!Array.isArray(rowsFromDb) || rowsFromDb.length === 0) {
|
||||
return [createDefaultRow()]
|
||||
return []
|
||||
}
|
||||
|
||||
const rows: FeeRow[] = rowsFromDb.map(item => {
|
||||
@@ -91,7 +180,7 @@ const mergeWithStoredRows = (rowsFromDb: unknown): FeeRow[] => {
|
||||
}
|
||||
})
|
||||
|
||||
return rows.length > 0 ? rows : [createDefaultRow()]
|
||||
return ensureSubtotalRow(rows)
|
||||
}
|
||||
|
||||
const buildPersistDetailRows = () => {
|
||||
@@ -104,7 +193,11 @@ const saveToIndexedDB = async () => {
|
||||
const payload: FeeGridState = {
|
||||
detailRows: JSON.parse(JSON.stringify(buildPersistDetailRows()))
|
||||
}
|
||||
|
||||
await localforage.setItem(props.storageKey, payload)
|
||||
if (props.syncMainStorageKey && props.syncRowId) {
|
||||
htFeeMethodReloadStore.emit(props.syncMainStorageKey, props.syncRowId)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('saveToIndexedDB failed:', error)
|
||||
}
|
||||
@@ -117,7 +210,7 @@ const loadFromIndexedDB = async () => {
|
||||
syncComputedValuesToRows()
|
||||
} catch (error) {
|
||||
console.error('loadFromIndexedDB failed:', error)
|
||||
detailRows.value = [createDefaultRow()]
|
||||
detailRows.value = []
|
||||
syncComputedValuesToRows()
|
||||
}
|
||||
}
|
||||
@@ -136,18 +229,22 @@ const columnDefs: ColDef<FeeRow>[] = [
|
||||
valueGetter: params =>
|
||||
params.node?.rowPinned
|
||||
? ''
|
||||
: isSubtotalRow(params.data)
|
||||
? '小计'
|
||||
: typeof params.node?.rowIndex === 'number'
|
||||
? params.node.rowIndex + 1
|
||||
: ''
|
||||
: '',
|
||||
colSpan: params => (isSubtotalRow(params.data) ? 2 : 1)
|
||||
},
|
||||
{
|
||||
headerName: '费用项',
|
||||
field: 'feeItem',
|
||||
minWidth: 140,
|
||||
flex: 1.4,
|
||||
editable: true,
|
||||
editable: params => !isSubtotalRow(params.data),
|
||||
valueGetter: params => (isSubtotalRow(params.data) ? '' : (params.data?.feeItem ?? '')),
|
||||
valueFormatter: formatEditableText,
|
||||
cellClass: 'editable-cell-line',
|
||||
cellClass: params => (isSubtotalRow(params.data) ? '' : 'editable-cell-line'),
|
||||
cellClassRules: {
|
||||
'editable-cell-empty': params => params.value == null || params.value === ''
|
||||
}
|
||||
@@ -157,7 +254,7 @@ const columnDefs: ColDef<FeeRow>[] = [
|
||||
field: 'unit',
|
||||
minWidth: 90,
|
||||
flex: 0.9,
|
||||
editable: true,
|
||||
editable: params => !isSubtotalRow(params.data),
|
||||
valueFormatter: formatEditableText,
|
||||
cellClass: 'editable-cell-line',
|
||||
cellClassRules: {
|
||||
@@ -171,7 +268,7 @@ const columnDefs: ColDef<FeeRow>[] = [
|
||||
flex: 1,
|
||||
headerClass: 'ag-right-aligned-header',
|
||||
cellClass: 'ag-right-aligned-cell editable-cell-line',
|
||||
editable: true,
|
||||
editable: params => !isSubtotalRow(params.data),
|
||||
valueParser: params => parseNumberOrNull(params.newValue, { precision: 3 }),
|
||||
valueFormatter: formatEditableQuantity,
|
||||
cellClassRules: {
|
||||
@@ -185,7 +282,7 @@ const columnDefs: ColDef<FeeRow>[] = [
|
||||
flex: 1.1,
|
||||
headerClass: 'ag-right-aligned-header',
|
||||
cellClass: 'ag-right-aligned-cell editable-cell-line',
|
||||
editable: true,
|
||||
editable: params => !isSubtotalRow(params.data),
|
||||
valueParser: params => parseNumberOrNull(params.newValue, { precision: 2 }),
|
||||
valueFormatter: formatEditableUnitPrice,
|
||||
cellClassRules: {
|
||||
@@ -207,7 +304,7 @@ const columnDefs: ColDef<FeeRow>[] = [
|
||||
field: 'remark',
|
||||
minWidth: 170,
|
||||
flex: 2,
|
||||
editable: true,
|
||||
editable: params => !isSubtotalRow(params.data),
|
||||
cellEditor: 'agLargeTextCellEditor',
|
||||
wrapText: true,
|
||||
autoHeight: true,
|
||||
@@ -217,6 +314,47 @@ const columnDefs: ColDef<FeeRow>[] = [
|
||||
cellClassRules: {
|
||||
'editable-cell-empty': params => params.value == null || params.value === ''
|
||||
}
|
||||
},
|
||||
{
|
||||
headerName: '操作',
|
||||
field: 'actions',
|
||||
minWidth: 92,
|
||||
maxWidth: 110,
|
||||
flex: 0.8,
|
||||
editable: false,
|
||||
sortable: false,
|
||||
filter: false,
|
||||
suppressMovable: true,
|
||||
cellRenderer: defineComponent({
|
||||
name: 'HtFeeGridActionCellRenderer',
|
||||
props: {
|
||||
params: {
|
||||
type: Object as PropType<ICellRendererParams<FeeRow>>,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
setup(rendererProps) {
|
||||
return () => {
|
||||
const row = rendererProps.params.data
|
||||
if (!row || isSubtotalRow(row)) return null
|
||||
const onDelete = (event: MouseEvent) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
requestDeleteRow(row.id, row.feeItem)
|
||||
}
|
||||
return h(
|
||||
'button',
|
||||
{
|
||||
type: 'button',
|
||||
class:
|
||||
'inline-flex cursor-pointer items-center gap-1 rounded border border-red-200 px-2 py-1 text-xs text-red-600 hover:bg-red-50',
|
||||
onClick: onDelete
|
||||
},
|
||||
[h(Trash2, { size: 12, 'aria-hidden': 'true' }), h('span', '删除')]
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
]
|
||||
|
||||
@@ -233,7 +371,7 @@ const handleGridReady = (event: GridReadyEvent<FeeRow>) => {
|
||||
|
||||
const handleCellValueChanged = () => {
|
||||
syncComputedValuesToRows()
|
||||
gridApi.value?.refreshCells({ columns: ['budgetFee'], force: true })
|
||||
gridApi.value?.refreshCells({ force: true })
|
||||
if (gridPersistTimer) clearTimeout(gridPersistTimer)
|
||||
gridPersistTimer = setTimeout(() => {
|
||||
void saveToIndexedDB()
|
||||
@@ -267,6 +405,7 @@ onBeforeUnmount(() => {
|
||||
<div class="rounded-lg border bg-card xmMx flex min-h-0 flex-1 flex-col">
|
||||
<div class="flex items-center justify-between border-b px-4 py-3">
|
||||
<h3 class="text-sm font-semibold text-foreground">{{ title }}</h3>
|
||||
<Button type="button" variant="outline" size="sm" @click="addRow">添加行</Button>
|
||||
</div>
|
||||
|
||||
<div class="ag-theme-quartz h-full min-h-0 w-full flex-1">
|
||||
@@ -292,4 +431,24 @@ onBeforeUnmount(() => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AlertDialogRoot :open="deleteConfirmOpen" @update:open="handleDeleteConfirmOpenChange">
|
||||
<AlertDialogPortal>
|
||||
<AlertDialogOverlay class="fixed inset-0 z-50 bg-black/45" />
|
||||
<AlertDialogContent class="fixed left-1/2 top-1/2 z-[70] w-[92vw] max-w-md -translate-x-1/2 -translate-y-1/2 rounded-lg border bg-background p-5 shadow-xl">
|
||||
<AlertDialogTitle class="text-base font-semibold">确认删除行</AlertDialogTitle>
|
||||
<AlertDialogDescription class="mt-2 text-sm text-muted-foreground">
|
||||
将删除“{{ pendingDeleteRowName }}”这条明细,是否继续?
|
||||
</AlertDialogDescription>
|
||||
<div class="mt-4 flex items-center justify-end gap-2">
|
||||
<AlertDialogCancel as-child>
|
||||
<Button variant="outline">取消</Button>
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction as-child>
|
||||
<Button variant="destructive" @click="confirmDeleteRow">确认删除</Button>
|
||||
</AlertDialogAction>
|
||||
</div>
|
||||
</AlertDialogContent>
|
||||
</AlertDialogPortal>
|
||||
</AlertDialogRoot>
|
||||
</template>
|
||||
Reference in new issue
Block a user