ant-design-vue合并列表(二)

31 阅读10分钟

若只需要实现跨行表格可以查看此文章

话不多说,先上效果,用户选中不同选项时,表格就行不同的更迭,其中进价与售价为固定项,每一行的进价可以自定义填写

recording.gif

先准备如下数据(为了方便识别这里用枚举类型EItem来表示form的类型)

export enum EItem {
  scheme = 'scheme',
  attr1 = 'attr1',
  attr2 = 'attr2',
  attr3 = 'attr3',
}
  // form表单
export const formGroup: { label: string, key: EItem }[] = [  { label: '套餐', key: EItem.scheme },  { label: '内存', key: EItem.attr1 },  { label: '颜色', key: EItem.attr2 },  { label: '运行内存', key: EItem.attr3 },]  
// 选项内容
export const selectOptions: Partial<Record<EItem, { label: string, value: number }[]>> = {
  [EItem.scheme]: [
    { label: '套餐一', value: 11 },
    { label: '套餐二', value: 12 },
  ],
  [EItem.attr1]: [
    { label: '32G', value: 4 },
    { label: '64G', value: 5 },
    { label: '128G', value: 6 },
  ],
  [EItem.attr2]: [
    { label: '红色', value: 1 },
    { label: '黄色', value: 2 },
    { label: '绿色', value: 3 },
  ],
  [EItem.attr3]: [
    { label: '32G', value: 7 },
    { label: '64G', value: 8 },
    { label: '128G', value: 9 },
  ],
}

接着简单写一个form表单数据如下

const form = reactive<Partial<Record<EItem, number[]>>>({
  [EItem.scheme]: [11],
  [EItem.attr1]: [4, 5],
  [EItem.attr2]: [],
  [EItem.attr3]: [7, 9],
})

筛选要展示的列

首先通过form筛选出要展示的列,至少选中其中一项才会展示对应的列(如上方的颜色没有选中,故而不会展示),这里逻辑比较简单 就是要筛选出有选中项的数据

function getDynamicColumn(): TColumnProps {
  return formGroup
    .filter((item) => form[item.key]!.length > 0)
    .map((item) => {
      return {
        dataIndex: item.key,
        title: item.label,
      }
    }) as TColumnProps
}

form的value与label映射,以便后续通过选中值找到中文名字

// 设置form的value与label映射
const formValueLabelMap: Record<number, string> = {}
const selecKeys = Object.keys(selectOptions) as EItem[]
selecKeys.forEach((key: EItem) => {
  const valueList = selectOptions[key]
  valueList!.forEach((item) => {
    formValueLabelMap[item.value as number] = item.label
  })
})

然后监听form表单做如下操作,

watch(
  () => form,
  () => {
    dynamicColumn = getDynamicColumn()
    resultList.value = permuteForm()
    resultColumns.value = getColumns()
  },
  {
    deep: true,
    immediate: true,
  },
)

permuteForm 生成表格数据

先拿到上一步动态列中的所有key值

  const keys: EItem[] = dynamicColumn.map((item) => item.dataIndex as EItem)

接下来

将选中的key进行排列组合得到我们需要的所有组合类型

let result: number[][] = [[]]
​
  for (const key of keys) {
    const values = form[key]
    const newResult: number[][] = []
    for (const prevCombination of result) {
      for (const value of values!) {
        newResult.push([...prevCombination, value])
      }
    }
    result = newResult
  }

比如我们这个选项就会生成如下结果

const form = reactive<Partial<Record<EItem, number[]>>>({
  [EItem.scheme]: [11],
  [EItem.attr1]: [4, 5],
  [EItem.attr2]: [],
  [EItem.attr3]: [7, 9],
})
[
    [11, 4, 7],
    [11, 4, 9],
    [11, 5, 7],
    [11, 5, 9]
]

最后我们把产生的result整合设置成表格数据

 return result.map((combination, index) => {
    const obj: { [key: string]: unknown } = { price: 0, price2: 0 }
    keys.forEach((key, keyIndex) => {
      obj[key] = formValueLabelMap[combination[keyIndex]]
    })
    return obj
  })

result形成出来的数组中的项就会如下

{
  attr1: '32G',
  attr3: '32G',
  price: 0,
  price2: 0,
  scheme: '套餐一',
}

存在的问题

但是这个过程存在一个问题,就是加入我填好了商品的价格,然后修改form的时候就会导致填好的价格被清除掉,这个情况我们需要斟酌一下

分两个情况

  • 修改form然后不改变行数
  • 修改form然后不改变行数

如下

加了个颜色--不会影响结果

加了个内存--会影响结果

对于行数不改的情况 我们直接从行数据中拿到对于的索引赋值即可,

如下

 return result.map((combination, index) => {
    const obj: { [key: string]: unknown } = { price: 0, price2: 0 }
    obj.price = resultList.value[index].price as number
    obj.price2 = resultList.value[index].price2 as number
    keys.forEach((key, keyIndex) => {
      obj[key] = formValueLabelMap[combination[keyIndex]]
    })
    return obj
  })

对于会改变的数据,我们需要找到之前这一行数据的值,然后给原原本本还原回去

事实上换个角度分析一下,是另外两种情况

  • 修改行
  • 修改列

且两者不会同时存在,

我们知道之前每一列排列组合出来的result的每一个数组都是唯一的,如下

[
    [11, 4, 7],
    [11, 4, 9],
    [11, 5, 7],
    [11, 5, 9]
]

于是我们可以将这些属性的key值通过join合并起来,接下来用来映射每一条数据,从而将其缓存,如下

let colsTableMap: Record<string, Record<string, unknown>> = {}
function permuteForm(): Record<string, unknown>[] {
  ...
  let type: 'add' | 'del' | 'udpate' = 'add'
  if (resultList.value.length === result.length) {
    type = 'udpate'
  }
  // 需要重置原有的值
  const colsTableMapCpoy = colsTableMap
  colsTableMap = {}
  return result.map((combination, index) => {
    const obj: { [key: string]: unknown } = { price: 0, price2: 0 }
    const colsValue = combination.join('')
    // 行数不变属于update,赋值原有的数据
    if (type === 'udpate') {
      obj.price = resultList.value[index].price as number
      obj.price2 = resultList.value[index].price2 as number
    } else if (type === 'add') {
      // 这一行若是完全对应之前的行则直接返回
      if (colsTableMapCpoy[colsValue]) {
        colsTableMap[colsValue] = colsTableMapCpoy[colsValue]
        return colsTableMap[colsValue]
      }
    }
    keys.forEach((key, keyIndex) => {
      obj[key] = formValueLabelMap[combination[keyIndex]]
    })
    colsTableMap[colsValue] = obj
    return obj
  })
}

这一部分的代码抽离如下

// 设置form的value与label映射
const formValueLabelMap: Record<number, string> = {}
const selecKeys = Object.keys(selectOptions) as EItem[]
selecKeys.forEach((key: EItem) => {
  const valueList = selectOptions[key]
  valueList!.forEach((item) => {
    formValueLabelMap[item.value as number] = item.label
  })
})
​
// 需要动态设置的列
dynamicColumn = getDynamicColumn()
// 缓存表格数据
let colsTableMap: Record<string, Record<string, unknown>> = {}
​
function permuteForm(): Record<string, unknown>[] {
  if (dynamicColumn.length === 0) {
    return []
  }
  const keys: EItem[] = dynamicColumn.map((item) => item.dataIndex as EItem)
  let result: number[][] = [[]]
  // 将选中的数据就行排列组合
  for (const key of keys) {
    const values = form[key]
    const newResult: number[][] = []
    for (const prevCombination of result) {
      for (const value of values!) {
        newResult.push([...prevCombination, value])
      }
    }
    result = newResult
  }
  //方便后续复用用户修改时就行的判断
  let type: 'add' | 'del' | 'udpate' = 'add'
  if (resultList.value.length === result.length) {
    type = 'udpate'
  }
  const colsTableMapCpoy = colsTableMap
  colsTableMap = {}
  return result.map((combination, index) => {
    const obj: { [key: string]: unknown } = { price: 0, price2: 0 }
    const colsValue = combination.join('')
    if (type === 'udpate') {
      obj.price = resultList.value[index].price as number
      obj.price2 = resultList.value[index].price2 as number
    } else if (type === 'add') {
      if (colsTableMapCpoy[colsValue]) {
        colsTableMap[colsValue] = colsTableMapCpoy[colsValue]
        return colsTableMap[colsValue]
      }
    }
    keys.forEach((key, keyIndex) => {
      obj[key] = formValueLabelMap[combination[keyIndex]]
    })
    // 通过选型构成的值缓存数据
    colsTableMap[colsValue] = obj
    return obj
  })
}

getColumns整合columns

如下

function getColumns() {
  const columnSpans = getSpansColumn()
  const columnConfig = calculateRowSpans(columnSpans)
  const column = generateMergedColumns(columnConfig)
  return column
}

最后将计算出来的数据用上一版本提到的思路就行行的合并即可,文章链接

进阶

上一讲的方法还是很暴力的

只要修改了了form表单,整个column重新计算,然后所有要创建表单,虽然用数据进行了缓存,但也是要遍历所有单元格。下面分享一下分情况讨论来进行优化

首先是第一次进入的时候,这里的行和列是要完整生成,这个无可避免

关键是再form的变化选择上进行的改变,即update时

我们将情况归类为以下类型

  • 删除行数据,即某个属性有多项,然后删掉了其中一项
  • 添加行数据,即某个属性有多项,然后增加了其中一项
  • 只存在列更新,即某个属性原先没有,然后增加了一个选项;或者原先只有一项,现在给删除没了

只存在列更新这个可能不好理解,下面给个图说明一下

于是在修改时我们做如下操作,在数据更新时分三种情况就行判断

let oldForm = JSON.parse(JSON.stringify(form)) // 进入页面时就把oldForm保存  // 以下时更新时的操作
    let dynamicIndex = 0
    // 查看每个formitem的选择情况
    for (let i = 0; i < formGroup.length; i++) {
      const { key } = formGroup[i]
      const oldLength = oldForm[key].length
      const newLength = form[key].length
      //记录旧值中的位置 
      if (oldLength > 0) {
        dynamicIndex++
      }
      // 增加或删除一个form选项都是更行列
      if (newLength + oldLength === 1) {
        return onlyColumnUpdate(key, oldLength, dynamicIndex)
      } else if (oldLength !== newLength) {
        // 否则就是删除或增加行
        return oldLength > newLength
          ? delRow(key, dynamicIndex)
          : addSomeRow(formGroup[i - 1]?.key, key, newLength, dynamicIndex)
      }
    }                       
​
​

delRow

删除行数据的情况我们只需要对比form表单的变化情况,将被删除的那一项的key与对应选项的value找出,找到record[key]===value的项去除即可。

例如开头gif中删除了颜色的红色选项,则对比前后form,找到被删除的是颜色选项,在找到被删除的是红色,接着便利数组,表格数据中存在红色的行进行删除。

m:list.length

之前:时间复杂度a * b * c * d (排列组合所有行)abcd为每一个选项的下拉个数+m+构造列

现在: 时间复杂度a+b+c+d + 构造列+m

/**
 * 删除行
 */
function delRow(key: EItem, dynamicIndex: number): TColumnUPdateArg {
  const delValue = findDelValue(oldForm[key], form[key])
  resultList.value = resultList.value.filter((item) => {
    return item[key] !== formValueLabelMap[delValue]
  })
  updateOldForms()
  return {
    handlerType: 'ListUpdate',
    key,
    index: dynamicIndex,
  }
}
// list1长 list2短
function findDelValue(list1: number[], list2: number[]) {
  let l = 0,
    r = 0
  while (l < list1.length && r < list2.length) {
    if (list1[l] === list2[r]) {
      l++
      r++
    } else {
      return list1[l]
    }
  }
  return list1[l]
}

返回值是用在后续统一处理行的合并时使用的的,这里只处理了行,但对于column中的rowspan还未做处理

addSomeRow

以前 时间复杂度a * b * c * d (排列组合所有行)abcd为每一个选项的下拉个数+m+构造列

现在 a+b+c+d+m+构造列

添加行数据

先说一下原理

把表单中每一项拉出来比较,其中某一项的选中值在新旧表单中不一致,且旧表单选中数量较小则触发addSomeRow

需要如下参数

/**
 * form改变导致添加多行
 * @param {EItem|undefined} keyBefore 前一列的key ,
 * @param key 影响到的key
 * @param newLength 修改后from长度
 * @param dynamicIndex 影响到的列索引
 */

我们要在原有的基础上构造出新的列表数组

如下 我们在只有红色属性的基础上增加一个黄色时,红色所在行的数据和新增的黄色行数据是一致的,也就是我们可以将红色的拷贝之后直接插入到对应数据的下方,然后将红色换成黄色

规则是一路查阅下来,同属性的上一个份数据的值所在的行直接拷贝,除此之外前一列的值必须相同,若二者有一个不同则停止拷贝,开始插入,具体代码如下,同样,返回值在后面会用到

/**
 * form改变导致添加多行
 * @param {EItem|undefined} keyBefore 前一列的key ,
 * @param key 影响到的key
 * @param newLength 修改后from长度
 * @param dynamicIndex 影响到的列索引
 */
function addSomeRow(
  keyBefore: EItem | undefined,
  key: EItem,
  newLength: number,
  dynamicIndex: number,
): TColumnUPdateArg {
  const newArr = [...resultList.value]
  // 新插入之后数据会有偏移量,需要记录下来
  let offSet = 0,
    count = 0
  let lastColumnValue: string = resultList.value[0]?.[keyBefore!]
​
  resultList.value.forEach((item, index) => {
    // 与当前新增选项所属同个属性 说明除当前选项外都相同
    const isSameAttr = item[key] === formValueLabelMap[form[key][newLength - 2]]
    // 用于判断前一列的值是否相同
    const isSameKeyBefore = item[keyBefore!] === lastColumnValue
    if (isSameAttr && isSameKeyBefore) {
      count++
    } else if (count > 0) {
      // 将具有同等属性的列拷贝下来
      const insertOption = resultList.value.slice(index - count, index)
      insertOption.forEach((insertItem) => {
        // 最好deepclone
        newArr.splice(index + offSet, 0, {
          ...insertItem,
          price: 0,
          price2: 0,
          [key]: formValueLabelMap[form[key][newLength - 1]],
        })
        offSet++
      })
      count = isSameAttr ? 1 : 0
      lastColumnValue = item[keyBefore!]
    }
  })
  // 若遍历完之后发现还有未插入的值直接push到末尾
  if (count > 0) {
    const insertOption = [...newArr.slice(-count)]
    insertOption.map((item) => {
      newArr.push({
        ...item,
        price: 0,
        price2: 0,
        [key]: formValueLabelMap[form[key][newLength - 1]],
      })
    })
  }
  resultList.value = newArr
  updateOldForms()
  return {
    handlerType: 'ListUpdate',
    key,
    index: dynamicIndex,
  }
}

onlyColumnUpdate

只存在列修改

新增列时我们可以做到精准添加行属性中对应的列,并且赋值前一列的customCell一比一复制(只操作列的话当前列和前一列的customCell值是相同的)

更新列的时候不需要对行就行增减,我们只需要在对应的行将其属性删除或者新增对应的值即可

/**
 * 找到被影响的列信息,同时修改行
 * @param key 影响到的key
 * @param oldLength 修改前from长度
 * @param dynamicIndex 影响到的列索引
 */
function onlyColumnUpdate(
  key: EItem,
  oldLength: number,
  dynamicIndex: number,
): TColumnUPdateArg {
  resultList.value = resultList.value.map((item) => {
    if (oldForm[key].length === 0) {
      item[key] = formValueLabelMap[form[key][0]]
    } else {
      delete item[key]
    }
    return item
  })
  const handlerType = oldLength === 0 ? 'addColumn' : 'delColumn'
  updateOldForms()
  return {
    handlerType,
    key,
    index: dynamicIndex,
  }
}

接下来根据上面返回值判断继续操作

    const handlerColumnArgs = updateList()
    if (handlerColumnArgs) {
      updateColumns(handlerColumnArgs)
    }

新增列则直接找到对应的列并就行插入,

1、但如果新增的是第一列则需要创建数据,第一列的话新增的列跨所有行

2、若类型是删除列,则直接找到对应列的索引就行删除

3、若是新增或删除行的操作,则需要对所有的列做处理,要调用原有的getColumns

此步骤中主要是第一步和第二部节省了大量运算

代码如下

function updateColumns({
  handlerType,
  key: handlerKey,
  index: handlerIndex,
}: {
  handlerType: 'addColumn' | 'delColumn' | 'ListUpdate' | 'update'
  key?: EItem
  index?: number
}) {
  if (handlerType === 'addColumn' && typeof handlerIndex === 'number') {
    const customCell =
      handlerIndex > 0
        ? resultColumns.value[handlerIndex - 1].customCell
        : (_, index?: number) => ({
            // 第一列的话新增的列跨所有行
            rowSpan: index! > 0 ? 0 : resultList.value.length, 
          })
    // @ts-expect-error 忽略一下报错
    resultColumns.value.splice(handlerIndex, 0, {
      dataIndex: handlerKey!,
      title: formGroup[handlerIndex].label,
      customCell,
    })
  } else if (handlerType === 'delColumn') {
    resultColumns.value.splice(handlerIndex! - 1, 1)
  } else {
    resultColumns.value = getColumns()
  }
}