641 lines
20 KiB
Vue
641 lines
20 KiB
Vue
<script setup lang="ts">
|
|
import { computed, onActivated, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
|
import { AgGridVue } from 'ag-grid-vue3'
|
|
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 { decimalAggSum, roundTo, sumByNumber } from '@/lib/decimal'
|
|
import { formatThousandsFlexible } from '@/lib/numberFormat'
|
|
import { industryTypeList, getMajorDictEntries, isMajorIdInIndustryScope } from '@/sql'
|
|
import { SwitchRoot, SwitchThumb } from 'reka-ui'
|
|
import { useKvStore } from '@/pinia/kv'
|
|
|
|
|
|
|
|
interface DictLeaf {
|
|
id: string
|
|
code: string
|
|
name: string
|
|
hasCost: boolean
|
|
|
|
hasArea: boolean
|
|
}
|
|
|
|
interface DictGroup {
|
|
id: string
|
|
code: string
|
|
name: string
|
|
children: DictLeaf[]
|
|
}
|
|
|
|
|
|
|
|
interface XmScaleState {
|
|
detailRows?: DetailRow[]
|
|
totalAmount: number
|
|
roughCalcEnabled: boolean
|
|
|
|
}
|
|
interface XmBaseInfoState {
|
|
projectIndustry?: string
|
|
}
|
|
|
|
const XM_SCALE_FLUSH_EVENT = 'jgjs:xm-scale-flush-request'
|
|
type MajorLite = { code: string; name: string; hasCost?: boolean; hasArea?: boolean }
|
|
const kvStore = useKvStore()
|
|
|
|
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 || []) {
|
|
if (row?.isGroupRow === true) continue
|
|
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 buildGroupRows = (rows: DetailRow[]): DetailRow[] => {
|
|
const rowById = new Map(rows.map(row => [String(row.id || ''), row] as const))
|
|
const groupRows: DetailRow[] = []
|
|
for (const group of detailDict.value) {
|
|
let amountTotal = 0
|
|
let hasAmount = false
|
|
let landAreaTotal = 0
|
|
let hasLandArea = false
|
|
for (const child of group.children) {
|
|
const leaf = rowById.get(String(child.id || ''))
|
|
const amount = leaf?.amount
|
|
if (typeof amount === 'number' && Number.isFinite(amount)) {
|
|
amountTotal += amount
|
|
hasAmount = true
|
|
}
|
|
const landArea = leaf?.landArea
|
|
if (typeof landArea === 'number' && Number.isFinite(landArea)) {
|
|
landAreaTotal += landArea
|
|
hasLandArea = true
|
|
}
|
|
}
|
|
groupRows.push({
|
|
id: group.id,
|
|
groupCode: group.code,
|
|
groupName: group.name,
|
|
majorCode: group.code,
|
|
majorName: group.name,
|
|
hasCost: true,
|
|
hasArea: true,
|
|
amount: hasAmount ? roundTo(amountTotal, 3) : null,
|
|
landArea: hasLandArea ? roundTo(landAreaTotal, 3) : null,
|
|
path: [`${group.code} ${group.name}`],
|
|
hide: false,
|
|
isGroupRow: true
|
|
})
|
|
}
|
|
return groupRows
|
|
}
|
|
|
|
|
|
const applyPinnedTotalAmount = (
|
|
api: GridApi<DetailRow> | null | undefined,
|
|
totalAmount: number | null | undefined
|
|
) => {
|
|
const normalized = typeof totalAmount === 'number' && Number.isFinite(totalAmount)
|
|
? roundTo(totalAmount, 2)
|
|
: null
|
|
pinnedTopRowData.value[0].amount = normalized
|
|
const pinnedTopNode = api?.getPinnedTopRow(0)
|
|
if (pinnedTopNode) {
|
|
pinnedTopNode.setDataValue('amount', normalized)
|
|
}
|
|
}
|
|
|
|
const loadFromIndexedDB = async (api: GridApi<DetailRow>) => {
|
|
try {
|
|
const [baseInfo, contractData] = await Promise.all([
|
|
kvStore.getItem<XmBaseInfoState>(props.baseInfoKey || 'xm-base-info-v1'),
|
|
kvStore.getItem<XmScaleState>(props.dbKey)
|
|
])
|
|
|
|
activeIndustryId.value =
|
|
typeof baseInfo?.projectIndustry === 'string' ? baseInfo.projectIndustry.trim() : ''
|
|
|
|
if (!activeIndustryId.value) {
|
|
detailDict.value = []
|
|
detailRows.value = []
|
|
roughCalcEnabled.value = false
|
|
applyPinnedTotalAmount(api, null)
|
|
return
|
|
}
|
|
|
|
const filteredEntries = majorEntries.filter(([id]) =>
|
|
isMajorIdInIndustryScope(id, activeIndustryId.value)
|
|
)
|
|
detailDict.value = buildDetailDict(filteredEntries)
|
|
|
|
roughCalcEnabled.value = Boolean(contractData?.roughCalcEnabled)
|
|
applyPinnedTotalAmount(api, contractData?.totalAmount)
|
|
const contractRows = Array.isArray(contractData?.detailRows) ? contractData.detailRows : []
|
|
const hasContractRows = contractRows.length > 0
|
|
const hasContractScaleValue = hasContractRows
|
|
? contractRows.some(row => {
|
|
const amount = row?.amount
|
|
const landArea = row?.landArea
|
|
return (
|
|
(typeof amount === 'number' && Number.isFinite(amount)) ||
|
|
(typeof landArea === 'number' && Number.isFinite(landArea))
|
|
)
|
|
})
|
|
: false
|
|
const isLegacyEmptyScaleRows =
|
|
hasContractRows &&
|
|
!hasContractScaleValue &&
|
|
!roughCalcEnabled.value &&
|
|
typeof contractData?.totalAmount === 'number' &&
|
|
Number.isFinite(contractData.totalAmount)
|
|
if (hasContractRows && !isLegacyEmptyScaleRows) {
|
|
detailRows.value = mergeWithDictRows(contractRows)
|
|
return
|
|
}
|
|
|
|
if (props.xmInfoKey) {
|
|
// 首次创建合同段时,默认继承项目规模信息(同一套专业字典,按 id 对齐)
|
|
const xmData = await kvStore.getItem<XmScaleState>(props.xmInfoKey)
|
|
roughCalcEnabled.value = Boolean(xmData?.roughCalcEnabled)
|
|
applyPinnedTotalAmount(api, xmData?.totalAmount)
|
|
|
|
if (Array.isArray(xmData?.detailRows) && xmData.detailRows.length > 0) {
|
|
detailRows.value = mergeWithDictRows(xmData.detailRows)
|
|
return
|
|
}
|
|
}
|
|
detailRows.value = buildDefaultRows()
|
|
|
|
|
|
void saveToIndexedDB()
|
|
} catch (error) {
|
|
console.error('loadFromIndexedDB failed:', error)
|
|
activeIndustryId.value = ''
|
|
detailRows.value = []
|
|
roughCalcEnabled.value = false
|
|
applyPinnedTotalAmount(api, null)
|
|
}
|
|
}
|
|
|
|
interface DetailRow {
|
|
id: string
|
|
groupCode: string
|
|
groupName: string
|
|
majorCode: string
|
|
majorName: string
|
|
hasCost: boolean
|
|
hasArea: boolean
|
|
amount: number | null
|
|
landArea: number | null
|
|
path: string[]
|
|
hide?: boolean
|
|
isGroupRow?: boolean
|
|
}
|
|
|
|
interface XmBaseInfoState {
|
|
projectIndustry?: string
|
|
}
|
|
|
|
interface GridPersistState {
|
|
detailRows?: DetailRow[]
|
|
roughCalcEnabled?: boolean
|
|
totalAmount?: number | null
|
|
}
|
|
|
|
const props = defineProps<{
|
|
title: string
|
|
dbKey: string
|
|
xmInfoKey?: string | null
|
|
baseInfoKey?: string
|
|
}>()
|
|
|
|
let persistTimer: ReturnType<typeof setTimeout> | null = null
|
|
const gridApi = ref<GridApi<DetailRow> | 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 roughCalcEnabled = ref(false)
|
|
const visibleRowData = computed(() => { return detailRows.value.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>[] = [
|
|
{
|
|
headerName: '造价金额(万元)',
|
|
field: 'amount',
|
|
headerClass: 'ag-right-aligned-header',
|
|
minWidth: 100,
|
|
flex: 1,
|
|
editable: params => {
|
|
if (roughCalcEnabled.value) return Boolean(params.node?.rowPinned)
|
|
return !params.node?.group && !params.node?.rowPinned && Boolean(params.data?.hasCost)
|
|
},
|
|
cellClass: params =>
|
|
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 =>
|
|
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 => {
|
|
if (params.newValue === '' || params.newValue == null) return null
|
|
const v = Number(params.newValue)
|
|
return Number.isFinite(v) ? roundTo(v, 3) : null
|
|
},
|
|
valueFormatter: params => {
|
|
if (roughCalcEnabled.value) {
|
|
if (!params.node?.rowPinned) return ''
|
|
if (params.value == null || params.value === '') return '点击输入'
|
|
return formatThousandsFlexible(params.value, 3)
|
|
}
|
|
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 formatThousandsFlexible(params.value, 3)
|
|
}
|
|
},
|
|
{
|
|
headerName: '用地面积(亩)',
|
|
field: 'landArea',
|
|
headerClass: 'ag-right-aligned-header',
|
|
minWidth: 100,
|
|
flex: 1,
|
|
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'
|
|
: '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 (roughCalcEnabled.value) {
|
|
return ''
|
|
}
|
|
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 formatThousandsFlexible(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 pinnedTopRowData = ref<DetailRow[]>([
|
|
{
|
|
id: 'pinned-total-row',
|
|
groupCode: '',
|
|
groupName: '',
|
|
majorCode: '',
|
|
majorName: '',
|
|
hasCost: false,
|
|
hasArea: false,
|
|
amount: null,
|
|
landArea: null,
|
|
path: ['TOTAL']
|
|
}
|
|
])
|
|
|
|
const saveToIndexedDB = async () => {
|
|
try {
|
|
const leafRows = detailRows.value.map(row => ({
|
|
...JSON.parse(JSON.stringify(row)),
|
|
hide: Boolean(row.hide),
|
|
isGroupRow: false
|
|
}))
|
|
const totalAmountFromRows = (() => {
|
|
let hasValue = false
|
|
let total = 0
|
|
for (const row of leafRows) {
|
|
const amount = row?.amount
|
|
if (typeof amount !== 'number' || !Number.isFinite(amount)) continue
|
|
total += amount
|
|
hasValue = true
|
|
}
|
|
return hasValue ? roundTo(total, 2) : null
|
|
})()
|
|
const pinnedAmount = pinnedTopRowData.value[0].amount
|
|
const normalizedPinnedAmount =
|
|
typeof pinnedAmount === 'number' && Number.isFinite(pinnedAmount)
|
|
? roundTo(pinnedAmount, 2)
|
|
: null
|
|
const normalizedTotalAmount = roughCalcEnabled.value ? normalizedPinnedAmount : totalAmountFromRows
|
|
pinnedTopRowData.value[0].amount = normalizedTotalAmount
|
|
const payload: GridPersistState = {
|
|
detailRows: [...leafRows, ...buildGroupRows(leafRows)]
|
|
}
|
|
payload.roughCalcEnabled = roughCalcEnabled.value
|
|
payload.totalAmount = normalizedTotalAmount
|
|
await kvStore.setItem(props.dbKey, payload)
|
|
} catch (error) {
|
|
console.error('saveToIndexedDB failed:', error)
|
|
}
|
|
}
|
|
|
|
const schedulePersist = () => {
|
|
if (persistTimer) clearTimeout(persistTimer)
|
|
persistTimer = setTimeout(() => {
|
|
void saveToIndexedDB()
|
|
}, 600)
|
|
}
|
|
|
|
const handleFlushPersistRequest = (event: Event) => {
|
|
const customEvent = event as CustomEvent<{ done?: () => void }>
|
|
const done = customEvent?.detail?.done
|
|
if (persistTimer) {
|
|
clearTimeout(persistTimer)
|
|
persistTimer = null
|
|
}
|
|
void saveToIndexedDB().finally(() => {
|
|
done?.()
|
|
})
|
|
}
|
|
|
|
const setDetailRowsHidden = (hidden: boolean) => {
|
|
for (const row of detailRows.value) {
|
|
row.hide = hidden
|
|
}
|
|
}
|
|
|
|
let oldValue:number|null
|
|
const onRoughCalcSwitch = (checked: boolean) => {
|
|
gridApi.value?.stopEditing(true)
|
|
roughCalcEnabled.value = checked
|
|
setDetailRowsHidden(checked)
|
|
if (!checked) {
|
|
oldValue=pinnedTopRowData.value[0].amount
|
|
syncPinnedTotalForNormalMode()
|
|
} else {
|
|
pinnedTopRowData.value[0].amount = oldValue
|
|
const pinnedTopNode = gridApi.value?.getPinnedTopRow(0)
|
|
if (pinnedTopNode) {
|
|
pinnedTopNode.setDataValue('amount', oldValue)
|
|
}
|
|
}
|
|
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 loadFromIndexedDB(event.api)
|
|
|
|
void refreshPinnedTotalLabelCell()
|
|
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
watch(totalLabel, () => {
|
|
refreshPinnedTotalLabelCell()
|
|
})
|
|
|
|
const syncPinnedTotalForNormalMode = () => {
|
|
if (roughCalcEnabled.value) return
|
|
|
|
if (!gridApi.value) {
|
|
pinnedTopRowData.value[0].amount = sumByNumber(detailRows.value, row => row.amount)
|
|
return
|
|
}
|
|
let total = 0
|
|
let hasValue = false
|
|
|
|
detailRows.value.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)
|
|
window.removeEventListener(XM_SCALE_FLUSH_EVENT, handleFlushPersistRequest as EventListener)
|
|
gridApi.value = null
|
|
void saveToIndexedDB()
|
|
})
|
|
|
|
onMounted(() => {
|
|
window.addEventListener(XM_SCALE_FLUSH_EVENT, handleFlushPersistRequest as EventListener)
|
|
})
|
|
</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="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="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>
|
|
</template>
|