fix more
This commit is contained in:
1 parent
5734cfa534
commit
1609f19b9c
14 files changed
+2481
-433
No files matched your search
@@ -0,0 +1,416 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { AgGridVue } from 'ag-grid-vue3'
|
||||
import type { ColDef, GridOptions } from 'ag-grid-community'
|
||||
import localforage from 'localforage'
|
||||
import { majorList } from '@/sql'
|
||||
import 'ag-grid-enterprise'
|
||||
import {
|
||||
|
||||
themeQuartz
|
||||
} from "ag-grid-community"
|
||||
import { AG_GRID_LOCALE_CN } from '@ag-grid-community/locale';
|
||||
// 精简的边框配置(细线条+浅灰色,弱化分割线视觉)
|
||||
const borderConfig = {
|
||||
style: "solid", // 虚线改实线更简洁,也可保留 dotted 但建议用 solid
|
||||
width: 0.5, // 更细的边框,减少视觉干扰
|
||||
color: "#e5e7eb" // 浅灰色边框,清新不刺眼
|
||||
};
|
||||
|
||||
// 简洁清新风格的主题配置
|
||||
const myTheme = themeQuartz.withParams({
|
||||
// 核心:移除外边框,减少视觉包裹感
|
||||
wrapperBorder: false,
|
||||
|
||||
// 表头样式(柔和浅蓝,无加粗,更轻盈)
|
||||
headerBackgroundColor: "#f9fafb", // 极浅的背景色,替代深一点的 #e7f3fc
|
||||
headerTextColor: "#374151", // 深灰色文字,比纯黑更柔和
|
||||
headerFontSize: 15, // 字体稍大一点,更易读
|
||||
headerFontWeight: "normal", // 取消加粗,降低视觉重量
|
||||
|
||||
// 行/列/表头边框(统一浅灰细边框)
|
||||
rowBorder: borderConfig,
|
||||
columnBorder: borderConfig,
|
||||
headerRowBorder: borderConfig,
|
||||
|
||||
|
||||
// 可选:偶数行背景色(轻微区分,更清新)
|
||||
dataBackgroundColor: "#fefefe"
|
||||
});
|
||||
interface DictLeaf {
|
||||
id: string
|
||||
code: string
|
||||
name: string
|
||||
}
|
||||
|
||||
interface DictGroup {
|
||||
id: string
|
||||
code: string
|
||||
name: string
|
||||
children: DictLeaf[]
|
||||
}
|
||||
|
||||
interface DetailRow {
|
||||
id: string
|
||||
groupCode: string
|
||||
groupName: string
|
||||
majorCode: string
|
||||
majorName: string
|
||||
amount: number | null
|
||||
landArea: number | null
|
||||
path: string[]
|
||||
}
|
||||
|
||||
interface XmInfoState {
|
||||
projectName: string
|
||||
detailRows: DetailRow[]
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
contractId: string
|
||||
}>()
|
||||
const DB_KEY = computed(() => `ht-info-v3-${props.contractId}`)
|
||||
|
||||
const detailRows = ref<DetailRow[]>([])
|
||||
|
||||
type majorLite = { code: string; name: string }
|
||||
const serviceEntries = Object.entries(majorList as Record<string, majorLite>)
|
||||
.sort((a, b) => Number(a[0]) - Number(b[0]))
|
||||
.filter((entry): entry is [string, majorLite] => {
|
||||
const item = entry[1]
|
||||
return Boolean(item?.code && item?.name)
|
||||
})
|
||||
|
||||
const detailDict: DictGroup[] = (() => {
|
||||
const groupMap = new Map<string, DictGroup>()
|
||||
const groupOrder: string[] = []
|
||||
const codeLookup = new Map(serviceEntries.map(([key, item]) => [item.code, { id: key, code: item.code, name: item.name }]))
|
||||
|
||||
for (const [key, item] of serviceEntries) {
|
||||
const code = item.code
|
||||
const isGroup = !code.includes('-')
|
||||
if (isGroup) {
|
||||
if (!groupMap.has(code)) groupOrder.push(code)
|
||||
groupMap.set(code, {
|
||||
id: key,
|
||||
code,
|
||||
name: item.name,
|
||||
children: []
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
const parentCode = 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,
|
||||
name: item.name
|
||||
})
|
||||
}
|
||||
|
||||
return groupOrder.map(code => groupMap.get(code)).filter((group): group is DictGroup => Boolean(group))
|
||||
})()
|
||||
|
||||
const idLabelMap = new Map<string, string>()
|
||||
for (const group of detailDict) {
|
||||
idLabelMap.set(group.id, `${group.code} ${group.name}`)
|
||||
for (const child of group.children) {
|
||||
idLabelMap.set(child.id, `${child.code} ${child.name}`)
|
||||
}
|
||||
}
|
||||
|
||||
const buildDefaultRows = (): DetailRow[] => {
|
||||
const rows: DetailRow[] = []
|
||||
for (const group of detailDict) {
|
||||
for (const child of group.children) {
|
||||
rows.push({
|
||||
id: child.id,
|
||||
groupCode: group.code,
|
||||
groupName: group.name,
|
||||
majorCode: child.code,
|
||||
majorName: child.name,
|
||||
amount: null,
|
||||
landArea: null,
|
||||
path: [group.id, child.id]
|
||||
})
|
||||
}
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
const mergeWithDictRows = (rowsFromDb: DetailRow[] | undefined): DetailRow[] => {
|
||||
const dbValueMap = new Map<string, DetailRow>()
|
||||
for (const row of rowsFromDb || []) {
|
||||
dbValueMap.set(row.id, row)
|
||||
}
|
||||
|
||||
return buildDefaultRows().map(row => {
|
||||
const fromDb = dbValueMap.get(row.id)
|
||||
if (!fromDb) return row
|
||||
|
||||
return {
|
||||
...row,
|
||||
amount: typeof fromDb.amount === 'number' ? fromDb.amount : null,
|
||||
landArea: typeof fromDb.landArea === 'number' ? fromDb.landArea : null
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const columnDefs: ColDef<DetailRow>[] = [
|
||||
|
||||
{
|
||||
headerName: '造价金额(万元)',
|
||||
field: 'amount',
|
||||
minWidth: 170,
|
||||
flex: 1, // 核心:开启弹性布局,自动占满剩余空间
|
||||
|
||||
editable: params => !params.node?.group && !params.node?.rowPinned,
|
||||
cellClass: params => (!params.node?.group && !params.node?.rowPinned ? 'editable-cell-line' : ''),
|
||||
cellClassRules: {
|
||||
'editable-cell-empty': params =>
|
||||
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
|
||||
},
|
||||
aggFunc: 'sum',
|
||||
valueParser: params => {
|
||||
if (params.newValue === '' || params.newValue == null) return null
|
||||
const v = Number(params.newValue)
|
||||
return Number.isFinite(v) ? v : null
|
||||
},
|
||||
valueFormatter: params => {
|
||||
if (!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')) {
|
||||
return '点击输入'
|
||||
}
|
||||
if (params.value == null) return ''
|
||||
return Number(params.value).toFixed(2)
|
||||
}
|
||||
},
|
||||
{
|
||||
headerName: '用地面积(亩)',
|
||||
field: 'landArea',
|
||||
minWidth: 170,
|
||||
flex: 1, // 核心:开启弹性布局,自动占满剩余空间
|
||||
|
||||
editable: params => !params.node?.group && !params.node?.rowPinned,
|
||||
cellClass: params => (!params.node?.group && !params.node?.rowPinned ? 'editable-cell-line' : ''),
|
||||
cellClassRules: {
|
||||
'editable-cell-empty': params =>
|
||||
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
|
||||
},
|
||||
aggFunc: 'sum',
|
||||
valueParser: params => {
|
||||
if (params.newValue === '' || params.newValue == null) return null
|
||||
const v = Number(params.newValue)
|
||||
return Number.isFinite(v) ? v : null
|
||||
},
|
||||
valueFormatter: params => {
|
||||
if (!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')) {
|
||||
return '点击输入'
|
||||
}
|
||||
if (params.value == null) return ''
|
||||
return Number(params.value).toFixed(2)
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
const autoGroupColumnDef: ColDef = {
|
||||
headerName: '专业编码以及工程专业名称',
|
||||
minWidth: 320,
|
||||
pinned: 'left',
|
||||
flex:2, // 核心:开启弹性布局,自动占满剩余空间
|
||||
|
||||
cellRendererParams: {
|
||||
suppressCount: true
|
||||
},
|
||||
valueFormatter: params => {
|
||||
if (params.node?.rowPinned) {
|
||||
return '总合计'
|
||||
}
|
||||
const nodeId = String(params.value || '')
|
||||
return idLabelMap.get(nodeId) || nodeId
|
||||
}
|
||||
}
|
||||
|
||||
const gridOptions: GridOptions<DetailRow> = {
|
||||
treeData: true,
|
||||
animateRows: true,
|
||||
singleClickEdit: true,
|
||||
suppressClickEdit: false,
|
||||
suppressContextMenu: false,
|
||||
groupDefaultExpanded: -1,
|
||||
suppressFieldDotNotation: true,
|
||||
getDataPath: data => data.path,
|
||||
getContextMenuItems: () => ['copy', 'paste', 'separator', 'export'],
|
||||
defaultColDef: {
|
||||
resizable: true,
|
||||
sortable: false,
|
||||
filter: false
|
||||
}
|
||||
}
|
||||
|
||||
const totalAmount = computed(() =>
|
||||
detailRows.value.reduce((sum, row) => sum + (row.amount || 0), 0)
|
||||
)
|
||||
|
||||
const totalLandArea = computed(() =>
|
||||
detailRows.value.reduce((sum, row) => sum + (row.landArea || 0), 0)
|
||||
)
|
||||
const pinnedTopRowData = computed(() => [
|
||||
{
|
||||
id: 'pinned-total-row',
|
||||
groupCode: '',
|
||||
groupName: '',
|
||||
majorCode: '',
|
||||
majorName: '',
|
||||
amount: totalAmount.value,
|
||||
landArea: totalLandArea.value,
|
||||
path: ['TOTAL']
|
||||
}
|
||||
])
|
||||
|
||||
|
||||
|
||||
const saveToIndexedDB = async () => {
|
||||
try {
|
||||
const payload = {
|
||||
detailRows: JSON.parse(JSON.stringify(detailRows.value))
|
||||
}
|
||||
console.log('Saving to IndexedDB:', payload)
|
||||
await localforage.setItem(DB_KEY.value, payload)
|
||||
} catch (error) {
|
||||
console.error('saveToIndexedDB failed:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const loadFromIndexedDB = async () => {
|
||||
try {
|
||||
const data = await localforage.getItem<XmInfoState>(DB_KEY.value)
|
||||
if (data) {
|
||||
detailRows.value = mergeWithDictRows(data.detailRows)
|
||||
return
|
||||
}
|
||||
|
||||
detailRows.value = buildDefaultRows()
|
||||
} catch (error) {
|
||||
console.error('loadFromIndexedDB failed:', error)
|
||||
detailRows.value = buildDefaultRows()
|
||||
}
|
||||
}
|
||||
|
||||
let persistTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
|
||||
|
||||
|
||||
let gridPersistTimer: ReturnType<typeof setTimeout> | null = null
|
||||
const handleCellValueChanged = () => {
|
||||
if (gridPersistTimer) clearTimeout(gridPersistTimer)
|
||||
gridPersistTimer = setTimeout(() => {
|
||||
void saveToIndexedDB()
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
|
||||
onMounted(async () => {
|
||||
await loadFromIndexedDB()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (persistTimer) clearTimeout(persistTimer)
|
||||
if (gridPersistTimer) clearTimeout(gridPersistTimer)
|
||||
void saveToIndexedDB()
|
||||
})
|
||||
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 (e) {
|
||||
// 解析失败时返回原始值,无需额外处理
|
||||
}
|
||||
return params.value;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-6">
|
||||
|
||||
|
||||
<div class="rounded-lg border bg-card xmMx">
|
||||
<div class="flex items-center justify-between border-b px-4 py-3">
|
||||
<h3 class="text-sm font-semibold text-foreground">合同规模明细</h3>
|
||||
<div class="text-xs text-muted-foreground">导入导出</div>
|
||||
</div>
|
||||
|
||||
<div class="ag-theme-quartz h-[580px] w-full">
|
||||
<AgGridVue
|
||||
:style="{ height: '100%' }"
|
||||
:rowData="detailRows"
|
||||
:pinnedTopRowData="pinnedTopRowData"
|
||||
:columnDefs="columnDefs"
|
||||
:autoGroupColumnDef="autoGroupColumnDef"
|
||||
:gridOptions="gridOptions"
|
||||
:theme="myTheme"
|
||||
@cell-value-changed="handleCellValueChanged"
|
||||
:suppressColumnVirtualisation="true"
|
||||
:suppressRowVirtualisation="true"
|
||||
:cellSelection="{ handle: { mode: 'range' } }"
|
||||
:enableClipboard="true"
|
||||
:localeText="AG_GRID_LOCALE_CN"
|
||||
:tooltipShowDelay="500"
|
||||
:headerHeight="50"
|
||||
:processCellForClipboard="processCellForClipboard"
|
||||
:processCellFromClipboard="processCellFromClipboard"
|
||||
:undoRedoCellEditing="true"
|
||||
:undoRedoCellEditingLimit="20"
|
||||
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style >
|
||||
.ag-floating-top{
|
||||
overflow-y:auto !important
|
||||
}
|
||||
|
||||
.xmMx .editable-cell-line .ag-cell-value {
|
||||
display: inline-block;
|
||||
min-width: 84%;
|
||||
padding: 2px 4px;
|
||||
border-bottom: 1px solid #cbd5e1;
|
||||
}
|
||||
|
||||
.xmMx .editable-cell-line.ag-cell-focus .ag-cell-value,
|
||||
.xmMx .editable-cell-line:hover .ag-cell-value {
|
||||
border-bottom-color: #2563eb;
|
||||
}
|
||||
|
||||
.xmMx .editable-cell-empty .ag-cell-value {
|
||||
color: #94a3b8 !important;
|
||||
font-style: italic;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.xmMx .ag-cell.editable-cell-empty,
|
||||
.xmMx .ag-cell.editable-cell-empty .ag-cell-value {
|
||||
color: #94a3b8 !important;
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user