Created
September 29, 2024 01:54
-
-
Save xinglongjizi/29b61ff5f13075833056dbd4dc0f72f9 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* | |
| 背景el-table在渲染超过1000个td的场景下会出现性能问题: | |
| 1. 初始渲染时间加长 | |
| 2. 更改某个数据,vue内部进行大量的js计算,阻塞渲染主线程,造成浏览器几秒卡死,不可交互 | |
| 这是由于vue的虚拟dom渲染原理,即使一棵虚拟dom树的某个部分从未改变,还是会在每次重渲染时创建新的 vnode,带来了大量不必要的内存压力和性能问题 | |
| 解决方案,只能上技巧: | |
| 让el-table中的表格内容尽量简单,只做简单的静态数据渲染,交互都脱离el-table | |
| 比如表格中的数据编辑:点击某个td,触发在td编辑数据 | |
| 技巧:以编辑数据,单独谈一个编辑modal为启发,可以做一个蒙层,在这个蒙层中做编辑的ui,根据点击的td的位置信息,将编辑ui定位到td位置重合 | |
| 为了不因为改了一个数据,又触发el-table重新渲染,直接使用dom操作更改数据 | |
| 并且,传入el-table的数据都是经过Object.freeze的 | |
| */ | |
| <template> | |
| <Teleport to="body"> | |
| <div class="input-lay" v-show="showInputLay"> | |
| <form | |
| class="number-input-wrapper" | |
| :style="inputPosition" | |
| @submit="numberSubmit" | |
| > | |
| <input | |
| ref="numInput" | |
| type="number" | |
| min="0" | |
| max="99999999" | |
| step="1" | |
| @keydown="numberKeyDown" | |
| @change="numberChange" | |
| @blur="numberBlur" | |
| /> | |
| </form> | |
| </div> | |
| </Teleport> | |
| <el-table | |
| class="hi-common-el-table" | |
| :data="data" | |
| row-key="id" | |
| border | |
| :max-height="maxHeight" | |
| default-expand-all | |
| @cell-click="cellClick" | |
| show-summary | |
| :summary-method="getSummaries" | |
| > | |
| <el-table-column | |
| prop="name" | |
| label="子系统/模块" | |
| min-width="200" | |
| fixed | |
| show-overflow-tooltip | |
| resizable | |
| > | |
| </el-table-column> | |
| <el-table-column | |
| v-for="col in cols" | |
| :key="col.weekDate" | |
| :prop="col.weekDate" | |
| width="110" | |
| align="center" | |
| :resizable="false" | |
| > | |
| <template #header> | |
| <div class="col-name"> | |
| <span>{{ col.week }}</span> | |
| <span>{{ col.weekDate }}</span> | |
| </div> | |
| </template> | |
| <template #default="{ row }"> | |
| <span class="is-modified" v-if="row[col.weekDate + isModifiedKey]"></span> | |
| <span :class="calcClass(row, col.weekDate)">{{ row[col.weekDate] }}</span> | |
| </template> | |
| </el-table-column> | |
| </el-table> | |
| </template> | |
| <script lang="ts" setup> | |
| import { ref, reactive, computed, nextTick, h } from 'vue' | |
| import type { VNode } from 'vue' | |
| import { useWindowSize } from '@vueuse/core' | |
| import type { TableColumnCtx } from 'element-plus' | |
| import { ElMessage } from 'element-plus' | |
| import { | |
| httpEditPredictionData, | |
| } from '@/api/issueTrendPrediction/editTrendPredictionDataModal' | |
| import type { | |
| PureObj, | |
| } from '../types' | |
| import { storeToRefs } from 'pinia' | |
| import { LoginStore } from '@/store/modules/login' | |
| import { useIssueTrendPredictionPageInfoStore } from '../store/modules/pageInfo' | |
| const loginStore = LoginStore() | |
| const issueTrendPredictionPageInfoStore = useIssueTrendPredictionPageInfoStore() | |
| const { planCode } = storeToRefs(issueTrendPredictionPageInfoStore) | |
| const { height } = useWindowSize() | |
| const isModifiedKey = '_isInput' | |
| const props = defineProps<{ | |
| cols: PureObj[] | |
| data: PureObj[] | |
| }>() | |
| const data = computed(() => { | |
| return Object.freeze(props.data) | |
| }) | |
| const cols = computed(() => { | |
| return Object.freeze(props.cols) | |
| }) | |
| const maxHeight = computed(() => { | |
| return height.value - 50 - 25 - 50 - 50 - 50 | |
| }) | |
| const showInputLay = ref<boolean>(false) | |
| const numInput = ref<HTMLInputElement>() | |
| const editRow = ref<PureObj | null>(null) | |
| const editColProp = ref<string>('') | |
| const changeDataMap = new Map() | |
| interface StylePotions { | |
| top: string | |
| left: string | |
| } | |
| const inputPosition: StylePotions = reactive({ | |
| top: '0px', | |
| left: '0px', | |
| }) | |
| const calcClass = (row: PureObj, weekDate: string) => { | |
| if (row.level) { | |
| return 'can-edit' | |
| } else { | |
| return 'sys-span' + row.code + weekDate | |
| } | |
| } | |
| let $editSpan: HTMLSpanElement | |
| let editOldValue: number | |
| async function cellClick(row: PureObj, column: PureObj, cell: HTMLTableCellElement) { | |
| if (column.property === 'name' || row.level === 0) { | |
| return | |
| } | |
| showInputLay.value = true | |
| editRow.value = row | |
| editColProp.value = column.property | |
| const rect = cell.getBoundingClientRect() | |
| const $input = numInput.value as HTMLInputElement | |
| inputPosition.top = rect.top + 'px' | |
| inputPosition.left = rect.left + 'px' | |
| await nextTick() | |
| $input.focus() | |
| $editSpan = cell.querySelector('.can-edit') as HTMLSpanElement | |
| $input.value = $editSpan.textContent ?? '' | |
| editOldValue = $editSpan.textContent ? Number($editSpan.textContent) : 0 | |
| } | |
| const numberKeyDown = async (e: KeyboardEvent) => { | |
| const pattern = /^[^\d]$/ | |
| if (pattern.test(e.key)) { | |
| e.preventDefault() | |
| } | |
| } | |
| const numberSubmit = (e: Event) => { | |
| e.preventDefault() | |
| showInputLay.value = false | |
| } | |
| const numberBlur = () => { | |
| showInputLay.value = false | |
| } | |
| const numberChange = (e: Event) => { | |
| const $this = e.target as HTMLInputElement | |
| if ($this.value) { | |
| $this.stepUp() | |
| $this.stepDown() | |
| } | |
| calcResult() | |
| } | |
| const calcResult = () => { | |
| const $this = numInput.value as HTMLInputElement | |
| $editSpan.textContent = $this.value | |
| const editValue = $this.value ? Number($this.value) : 0 | |
| const editChangedValue = editValue - editOldValue | |
| const $sysSpan = document.querySelector(`.sys-span${editRow.value?.parentCode}${editColProp.value}`) as HTMLSpanElement | |
| const sysOldVal = $sysSpan.textContent ? Number($sysSpan.textContent) : 0 | |
| $sysSpan.textContent = sysOldVal + editChangedValue + '' | |
| const $summarySpan = document.querySelector(`.summary-cell${editColProp.value}`) as HTMLSpanElement | |
| const summaryOldVal = $summarySpan.textContent ? Number($summarySpan.textContent) : 0 | |
| $summarySpan.textContent = summaryOldVal + editChangedValue + '' | |
| const mapKey = editRow.value?.parentCode + editRow.value?.code + editColProp.value | |
| const mapVal = { | |
| projectSelfCode: loginStore.projectSelfCode, | |
| groupCode: planCode.value, | |
| subSystem: editRow.value?.parentCode, | |
| module: editRow.value?.code, | |
| weekTime: editColProp.value, | |
| inputData: $this.value ? Number($this.value) : null, | |
| } | |
| changeDataMap.set(mapKey, mapVal) | |
| } | |
| const saveData = async () => { | |
| if (changeDataMap.size === 0) { | |
| return | |
| } | |
| const res = await httpEditPredictionData([...changeDataMap.values()]) | |
| const { data } = res | |
| if (data?.success) { | |
| ElMessage.success('success') | |
| } else { | |
| ElMessage.error('出错了,请重试,或联系支撑人员') | |
| } | |
| } | |
| defineExpose({ | |
| saveData, | |
| }) | |
| interface SummaryMethodProps<T = PureObj> { | |
| columns: TableColumnCtx<T>[] | |
| data: T[] | |
| } | |
| const getSummaries = (param: SummaryMethodProps) => { | |
| const { columns, data } = param | |
| const sums: (number | string | VNode)[] = [] | |
| columns.forEach((column, index) => { | |
| if (index === 0) { | |
| sums[index] = '总计' | |
| return | |
| } | |
| const values = data.map((item: PureObj) => { | |
| let sum = 0 | |
| item.children.forEach((child: PureObj) => { | |
| sum += Number(child[column.property] || 0) | |
| }) | |
| item[column.property] = sum | |
| return sum | |
| }) | |
| if (!values.every((value) => Number.isNaN(value))) { | |
| sums[index] = values.reduce((prev, curr) => { | |
| const value = Number(curr) | |
| if (!Number.isNaN(value)) { | |
| return prev + curr | |
| } else { | |
| return prev | |
| } | |
| }, 0) | |
| } else { | |
| sums[index] = 'N/A' | |
| } | |
| }) | |
| return sums.map((val, index) => { | |
| if (index > 0) { | |
| return h('span', { class: 'summary-cell' + columns[index].property }, val) | |
| } else { | |
| return val | |
| } | |
| }) | |
| } | |
| </script> | |
| <style lang="scss" scoped> | |
| .input-lay { | |
| position: fixed; | |
| z-index: 99999; | |
| inset: 0; | |
| .number-input-wrapper { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| display: inline-flex; | |
| align-items: center; | |
| width: 100px; | |
| height: 30px; | |
| padding: 0 10px; | |
| font-size: 12px; | |
| } | |
| input[type=number] { | |
| width: 100%; | |
| border: 1px dashed #00aaf2; | |
| border-radius: 2px; | |
| outline: none; | |
| height: 24px; | |
| padding: 0 10px; | |
| text-align: center; | |
| } | |
| } | |
| .el-table { | |
| .col-name { | |
| display: flex; | |
| flex-direction: column; | |
| line-height: normal; | |
| } | |
| :deep(.el-table__cell:has(.is-modified)) { | |
| position: relative; | |
| } | |
| .is-modified { | |
| position: absolute; | |
| top: 0; | |
| right: 0; | |
| width: 14px; | |
| height: 10px; | |
| background-color: #00aaf2; | |
| clip-path: polygon(0 0, 100% 0, 100% 100%); | |
| pointer-events: none; | |
| } | |
| :deep(.el-table__cell:has(.can-edit)), | |
| :deep(.el-table__cell:has(.can-edit) .cell) { | |
| padding: 0; | |
| } | |
| :deep(.el-table__cell:has(.can-edit)) { | |
| cursor: pointer; | |
| &:hover .can-edit { | |
| border-color: #00aaf2; | |
| } | |
| } | |
| .can-edit { | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| height: 24px; | |
| margin: 0 10px; | |
| border: 1px dashed transparent; | |
| border-radius: 2px; | |
| } | |
| } | |
| </style> | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment