Henry Henry
  • JavaScript
  • TypeScript
  • Vue
  • ElementUI
  • React
  • HTML
  • CSS
  • 技术文档
  • GitHub 技巧
  • Nodejs
  • Chrome
  • VSCode
  • Other
  • Mac
  • Windows
  • Linux
  • Vim
  • VSCode
  • Chrome
  • iTerm
  • Mac
  • Obsidian
  • lazygit
  • Vim 技巧
  • 分类
  • 标签
  • 归档
  • 网站
  • 资源
  • Vue 资源
GitHub (opens new window)

Henry

小学生中的前端大佬
  • JavaScript
  • TypeScript
  • Vue
  • ElementUI
  • React
  • HTML
  • CSS
  • 技术文档
  • GitHub 技巧
  • Nodejs
  • Chrome
  • VSCode
  • Other
  • Mac
  • Windows
  • Linux
  • Vim
  • VSCode
  • Chrome
  • iTerm
  • Mac
  • Obsidian
  • lazygit
  • Vim 技巧
  • 分类
  • 标签
  • 归档
  • 网站
  • 资源
  • Vue 资源
GitHub (opens new window)
  • JavaScript

  • TypeScript

  • Vue

  • ElementUI

    • 基于 ElementUI 封装的 TextEllipsis
    • 基于 ElementUI 封装的 Tree
    • 基于 ElementUI 封装的 Tree2
    • 基于 ElementUI 封装的 SelectTree
    • 基于 ElementUI 封装的 NumberInput
    • 基于 ElementUI 封装的基础 table 和 form
      • 分析现有代码
        • table
        • form
      • 初稿
        • table
        • form
      • 优化
      • 封装可编辑组件
      • 终稿
        • form
        • table
        • editable-elements
      • 增加功能
        • BaseTable 增加 defaultCheckedKeys 和 currentNodeKey
      • 实际工作中发现的问题
        • 改变 editable 后页面没变化
        • 可编辑 table 有时候没有滚动条
        • table 设置 reserve-selection 无效
        • BaseTable column 如何获取 row
    • 基于 ElementUI 封装的 TreeTable
    • 基于 VuePress 搭建一个类似 ElementUI 的说明文档
    • el-tree 节点过滤加载对应子节点
    • 简单调试 node_modules 源码
    • ElementUI 问题集合
    • 树形表格更新后保持折叠状态
    • 开始和完成时间互相限制
  • React

  • AntD

  • 前端
  • ElementUI
Henry
2020-05-16
目录

基于 ElementUI 封装的基础 table 和 form

一般做后台管理系统的项目, 用到最多的 UI 组件应该就是表格和表单吧.

一个最基础的例子就是上面一排操作按钮: 增删改查; 下面一个表格, 展示数据; 增改的时候弹出表单; 而且大部分情况下表格和表单的字段都是一样的, 但却要写两套代码, 作为一个崇尚简洁的程序员, 越简洁越好(才不是懒得写呢)

所以就想封装一下这两个组件, 最好一行代码就搞定了

# 分析现有代码

# table

先看 table 例子:

<el-table :data="tableData" style="width: 100%">
  <el-table-column prop="date" label="日期" width="180"> </el-table-column>
  <el-table-column prop="name" label="姓名" width="180"> </el-table-column>
  <el-table-column prop="address" label="地址"> </el-table-column>
</el-table>
1
2
3
4
5

我们可以把 column 这部分抽出一个数组, 数组中包含所有属性; 和 data 似的传入封装好的组件中, 组件自己遍历生成 el-table-column , 那咱们调用组件的时候是不是就一行代码就搞定了呀:

<base-table :data="tableData" :columns="columns" style="width: 100%"></base-table>
1

# form

再看 form 例子:

<el-form ref="form" :model="form" label-width="80px">
  <el-form-item label="活动名称">
    <el-input v-model="form.name"></el-input>
  </el-form-item>
  <el-form-item label="活动区域">
    <el-select v-model="form.region" placeholder="请选择活动区域">
      <el-option label="区域一" value="shanghai"></el-option>
      <el-option label="区域二" value="beijing"></el-option>
    </el-select>
  </el-form-item>
  <el-form-item label="活动时间">
    <el-col :span="11">
      <el-date-picker
        type="date"
        placeholder="选择日期"
        v-model="form.date1"
        style="width: 100%;"
      ></el-date-picker>
    </el-col>
    <el-col class="line" :span="2">-</el-col>
    <el-col :span="11">
      <el-time-picker
        placeholder="选择时间"
        v-model="form.date2"
        style="width: 100%;"
      ></el-time-picker>
    </el-col>
  </el-form-item>
  <el-form-item label="即时配送">
    <el-switch v-model="form.delivery"></el-switch>
  </el-form-item>
  <el-form-item label="活动性质">
    <el-checkbox-group v-model="form.type">
      <el-checkbox label="美食/餐厅线上活动" name="type"></el-checkbox>
      <el-checkbox label="地推活动" name="type"></el-checkbox>
      <el-checkbox label="线下主题活动" name="type"></el-checkbox>
      <el-checkbox label="单纯品牌曝光" name="type"></el-checkbox>
    </el-checkbox-group>
  </el-form-item>
  <el-form-item label="特殊资源">
    <el-radio-group v-model="form.resource">
      <el-radio label="线上品牌商赞助"></el-radio>
      <el-radio label="线下场地免费"></el-radio>
    </el-radio-group>
  </el-form-item>
  <el-form-item label="活动形式">
    <el-input type="textarea" v-model="form.desc"></el-input>
  </el-form-item>
  <el-form-item>
    <el-button type="primary" @click="onSubmit">立即创建</el-button>
    <el-button>取消</el-button>
  </el-form-item>
</el-form>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53

form 就比较难一些了: 不仅要判断表单类型( input , select , textarea 等等), select 等表单还需要 option 数组

为了实现一行代码就搞定, 所以我们最终调用的方式应该是这样的:

<base-form ref="form" :model="form" :form-items="formItems" label-width="80px"></base-form>
1

那就需要在 formItems 中做处理了: 首先需要区分表单类型; 对于 select 等这类表单, 还需要传入 option 选择数组

# 初稿

# table

table 就比较简单了

组件源码:

<el-table v-bind="$attrs">
  <el-table-column
    v-for="column in columns"
    :key="column.prop"
    v-bind="column"
  >
  </el-table-column>
</el-table>
1
2
3
4
5
6
7
8
name: 'BaseTable',
props: {
  columns: {
    type: Array,
    default () {
      return []
    }
  }
}
1
2
3
4
5
6
7
8
9

调用:

<base-table :data="tableData" :columns="columns" style="width: 100%"></base-table>
1
data() {
  return {
    tableData: [
      {
        date: '2016-05-02',
        name: '王小虎',
        address: '上海市普陀区金沙江路 1518 弄'
      }
    ],
    columns: [
      {
        label: '日期',
        prop: 'date',
        width: 180
      },
      {
        label: '姓名',
        prop: 'name',
        width: 180
      },
      {
        label: '地址',
        prop: 'address'
      }
    ]
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

# form

form 需要做的事情比 table 多一些

组件源码:

<el-form v-bind="$attrs" :model="model">
  <el-form-item v-for="item in formItems" :key="item.prop">
    <el-input
      v-if="item.type === 'input'"
      v-model="model[item.prop]"
      v-bind="item"
    >
    </el-input>
    <el-select v-if="item.type === 'select'" v-model="model[item.prop]" v-bind="item">
      <el-option
        v-for="option in item.select"
        :key="option.value"
        :label="option.label"
        :value="option.value">
      </el-option>
    </el-select>
    <el-switch
      v-if="item.type === 'switch'"
      v-model="model[item.prop]"
      v-bind="item"
    >
    </el-switch>
  </el-form-item>
</el-form>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

其余的表单就不写了, 都差不多的样子

name: 'BaseForm',
props: {
  modle: {
    type: Object,
    default() {
      return {}
    }
  },
  formItems: {
    type: Array,
    default() {
      return []
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

调用:

<base-form ref="form" :model="form" :form-items="formItems" label-width="80px"></base-form>
1
data() {
  return {
    form: {
      name: '',
      region: ''
    },
    formItems: [
      {
        type: 'input',
        label: '活动名称',
        prop: 'name'
      },
      {
        type: 'select',
        label: '活动区域',
        prop: 'region',
        select: [
          {
            label: '区域一',
            value: 'shanghai'
          },
          {
            label: '区域二',
            value: 'beijing'
          }
        ]
      }
    ]
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

# 优化

目前使用我们封装好的组件, 确实可以实现一行代码就搞定的效果, 而且如果 form 和 table 字段一样的话, formItems 和 columns 完全可以合并为一个数组. 但还有几个问题:

  1. BaseTable 功能过于单一, 只能展示数据, 我们实际工作中还有可编辑的表格
  2. BaseForm 封装的组件重复代码太多, 判断太多, 很多时候只是组件名字不同, 里面内容完全一致, 比如 el-input 和 el-switch; 现在把 ElementUI 所有表单元素都写进去了, 但实际上就 el-input 用的比较多, 而且可扩展性不好, 想要再加表单, 就需要修改封装的组件的源码

综合以上两个问题我们不难看出目前的主要问题就是这些可编辑的表单元素, 那我们解决了这个问题就可以了呀

只是组件名字不同, 里面内容完全一致

从这句话想到了什么吗? 对! 就是这个 Vue 内置组件: component (opens new window)

我们完全可以去掉那些 v-if 判断, 而只使用 component 即可, 这样可扩展性问题也解决了:

<el-form v-bind="$attrs" :model="model">
  <el-form-item v-for="item in formItems" :key="item.prop">
    <component :is="item.type" v-model="model[item.prop]" v-bind="item">
      <el-option
        v-for="option in item.select"
        :key="option.value"
        :label="option.label"
        :value="option.value"
      >
      </el-option>
      <el-radio
        v-for="radio in item.radio"
        :key="radio[item.value]"
        :label="radio[item.value]"
      >
        {{ radio[item.label] }}
      </el-radio>
    </component>
  </el-form-item>
</el-form>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

是不是一下就简单多了?

那么现在 item.type 就是组件的名字了, 比如 el-input; 关于 el-select 这类还需要有选择数据的组件(el-option, el-radio 等), 我目前是全部放到 component 里面的, 一来是不传这些选择数组的时候, 就不会渲染, 二来是就算渲染了, 组件内部 slot 如果不接收, 也不会显示出来, 三呢就是这些组件已经在项目中使用了, 改动的话有点麻烦, 所以如果是新项目, 这些也可以使用 component

那 form 的改完了, table 是不是也可以使用这个呢?

<el-table v-bind="$attrs" v-on="$listeners">
  <template v-for="(column, index) in columns">
    <el-table-column v-if="column.editable" :key="column.prop" v-bind="column">
      <component
        slot-scope="{ row }"
        :is="column.type"
        v-model="row[column.prop]"
        v-bind="column"
      >
        <el-option
          v-for="option in column.select"
          :key="option.value"
          :label="option.label"
          :value="option.value"
        >
        </el-option>
        <el-radio
          v-for="radio in column.radio"
          :key="radio[column.value]"
          :label="radio[column.value]"
        >
          {{ radio[column.label] }}
        </el-radio>
      </component>
    </el-table-column>
    <el-table-column v-else :key="column.prop" v-bind="column"> </el-table-column>
  </template>
</el-table>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

一想到这个组件可能被很多地方使用, 那咱们是不是也需要将这个可编辑的组件也封装为一个组件呢?

# 封装可编辑组件

这个就没啥好说的了, 就是把上面的拿下来即可

<component :is="item.type" v-model="model[item.prop]" v-bind="item">
  <el-option
    v-for="option in item.select"
    :key="option.value"
    :label="option.label"
    :value="option.value"
  >
  </el-option>
  <el-radio
    v-for="radio in item.radio"
    :key="radio[item.value]"
    :label="radio[item.value]"
  >
    {{ radio[item.label] }}
  </el-radio>
</component>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
name: 'EditableElements',
props: {
  item: {
    type: Object,
    default() {
      return {}
    }
  },
  model: {
    type: Object,
    default() {
      return {}
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

优化一下:

上面只有 radio, 其实 checkbox 也是类似的, 所以对于这类组件应该也使用 component 处理, 而且 value 和 label 也需要做一下映射:

<template>
  <component
    :is="item.component"
    v-model="model[item.prop]"
    :key="item.prop"
    v-bind="item"
    v-focus="item.focus"
    :placeholder="item.placeholder || `${handlePlaceholder(item.type)}${item.label}`"
    v-on="{ ...$listeners, ...item.events }"
  >
    <!-- 只展示 -->
    <text-ellipsis v-if="item.type === 'info'" :content="model[item.prop]"></text-ellipsis>
    <!-- 这里由于当初设计的时候以为 el-select 和 el-radio 等会同时存在当前组件中,
    所以规定 el-select 的下拉数据放在 select 中, el-radio 的放在 radio 中,
    其实 el-select 和 el-radio 等不会同时存在,
    为了 兼容以前写法 和 统一数据结构
    现在增加一个参数: options, 存放供选择的数据, 所有通用
    当然如果是新项目, 可以去掉 || 后面的代码, 统一使用 options -->
    <template v-if="item.type === 'select'">
      <el-option
        v-for="option in item.options || item.select"
        :key="option[listProps.value]"
        :value="option[listProps.value]"
        :label="option[listProps.label]"
        :disabled="option.disabled"
      ></el-option>
    </template>
    <!-- radio / checkbox 等 -->
    <template v-if="list.includes(item.type)">
      <component
        :is="`el-${item.type}`"
        v-for="option in item.options || item[item.type]"
        :key="option[listProps.value]"
        :label="option[listProps.value]"
        :disabled="option.disabled"
      >
        {{ ele[listProps.label] }}
      </component>
    </template>
    <slot v-for="(value, key) in item.slots" :name="key" :slot="key">{{ value }}</slot>
  </component>
</template>

<script>
import { handlePlaceholder } from 'utils'

export default {
  name: 'EditableElements',
  inheritAttrs: false,
  props: {
    item: {
      type: Object,
      default() {
        return {}
      }
    },
    model: {
      type: Object,
      default() {
        return {}
      }
    }
  },
  data() {
    return {
      list: ['radio', 'checkbox']
    }
  },
  computed: {
    listProps() {
      // 这里的 props 和上面的 options 是相同的原因
      const props = this.item.props || this.item[`${this.item.type}Props`]
      if (!props) return { label: 'label', value: 'value' }
      const { label = 'label', value = 'value', ...rest } = props
      return {
        label,
        value,
        ...rest
      }
    }
  },
  methods: {
    handlePlaceholder
  },
  directives: {
    focus: {
      // [vue v-focus v-show控制input的显示聚焦,第二次不生效问题_JavaScript_宣城-CSDN博客](https://blog.csdn.net/qq_37361812/article/details/93782340)
      // [页面一刷新让文本框自动获取焦点-- 和自定义v-focus指令 - 明月人倚楼 - 博客园](https://www.cnblogs.com/IwishIcould/p/12006378.html)
      update(el, { value, oldValue }) {
        if (value && value !== oldValue) {
          // 重点注意这里 当前元素是 div  所以要查到子元素中的 input
          const dom = el.querySelector('input') || el.querySelector('textarea')
          dom && dom.focus()
        }
      },
      inserted(el, { value }) {
        if (value) {
          // 重点注意这里 当前元素是 div  所以要查到子元素中的 input
          const dom = el.querySelector('input') || el.querySelector('textarea')
          dom && dom.focus()
        }
      }
    }
  }
}
</script>

<style lang="less" scoped>
.editable-elements {
  .el-select {
    width: 100%;
  }
}
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114

# 终稿

到这里本次封装之旅差不多就结束了, 想想还有点小激动呢!

闲话不多说, 加了亿点点小细节, enjoy

这里是源码: BaseForm (opens new window), BaseTable (opens new window), EditableElements (opens new window)

以下为简略代码:

# form

<el-form v-bind="$attrs" :model="model" ref="elForm" class="base-form">
  <slot name="prev"></slot>
  <el-form-item
    v-for="item in items"
    :key="item.prop"
    v-bind="item"
    :rules="[
      {
        required: !item.noRequired,
        message: item.ruleMessage || `${handlePlaceholder(item.type)}${item.label}`,
        trigger: item.type === 'select' ? 'change' : 'blur'
      },
      ...(rules[item.prop] || [])
    ]"
  >
    <editable-elements :model="model" :item="item" v-on="$listeners"></editable-elements>
  </el-form-item>
  <slot></slot>
</el-form>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
props: {
  keyProps: {
    type: Object,
    default() {
      return null
    }
  },
  model: {
    type: Object,
    default() {
      return {}
    }
  },
  formItems: {
    type: Array,
    default() {
      return []
    }
  },
  rules: {
    type: Object,
    default() {
      return {}
    }
  }
},
computed: {
  items() {
    return this.keyProps
      ? this.formItems.map(item => ({
          ...item,
          prop: item[this.keyProps.prop || 'prop'],
          label: item[this.keyProps.label || 'label']
        }))
      : this.formItems
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

# table

<el-table ref="elTable" class="base-table" v-bind="$attrs" v-on="$listeners">
  <slot name="prev"></slot>
  <template v-for="(column, index) in cols">
    <el-table-column v-if="column.editable" :key="column.prop" v-bind="column">
      <editable-elements
        slot-scope="{ row, $index }"
        :model="row"
        :item="{ ...column, focus: index === focusCol && $index === focusRow }"
        @change="change(row, $event, column)"
      ></editable-elements>
    </el-table-column>
    <el-table-column v-else :key="column.prop" v-bind="column"> </el-table-column>
  </template>
  <slot></slot>
</el-table>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
props: {
  keyProps: {
    type: Object,
    default() {
      return null
    }
  },
  columns: {
    type: Array,
    default() {
      return []
    }
  },
  focusRow: {
    type: Number,
    default: 0
  },
  focusCol: {
    type: Number,
    default: 0
  }
},
computed: {
  cols() {
    return this.keyProps
      ? this.columns.map(column => ({
          ...column,
          prop: column[this.keyProps.prop || 'prop'],
          label: column[this.keyProps.label || 'label']
        }))
      : this.columns
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

# editable-elements

<template>
  <component
    :is="item.component"
    v-model="model[item.prop]"
    :key="item.prop"
    v-bind="item"
    v-focus="item.focus"
    :placeholder="item.placeholder || `${handlePlaceholder(item.type)}${item.label}`"
    v-on="{ ...$listeners, ...item.events }"
  >
    <!-- 只展示 -->
    <text-ellipsis v-if="item.type === 'info'" :content="model[item.prop]"></text-ellipsis>
    <template v-if="item.type === 'select'">
      <el-option
        v-for="option in item.options || item.select"
        :key="option[listProps.value]"
        :value="option[listProps.value]"
        :label="option[listProps.label]"
        :disabled="option.disabled"
      ></el-option>
    </template>
    <!-- radio / checkbox 等 -->
    <template v-if="list.includes(item.type)">
      <component
        :is="`el-${item.type}`"
        v-for="option in item.options || item[item.type]"
        :key="option[listProps.value]"
        :label="option[listProps.value]"
        :disabled="option.disabled"
      >
        {{ option[listProps.label] }}
      </component>
    </template>
    <slot v-for="(value, key) in item.slots" :name="key" :slot="key">{{ value }}</slot>
  </component>
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

# 增加功能

# BaseTable 增加 defaultCheckedKeys 和 currentNodeKey

最近做项目的时候遇到 table 初始化的时候要有默认选中和默认高亮行的需求, 那就参照 el-tree 的方式做一下

本以为和 el-tree 一样, 传入 key 后, el-table 内部会自己处理, 结果 el-table 只能通过传入的 row 来实现默认选中和默认高亮行, 这也不能理解, 毕竟 el-table 中很少用到 row-key

那咱们就通过传入的 key 遍历找到对应的 row, 当然这种方式就必须传入 row-key 啦:

props: {
  data: {
    type: Array,
    default() {
      return []
    }
  },
  defaultCheckedKeys: {
    type: Array,
    default() {
      return []
    }
  },
  currentNodeKey: {
    type: [String, Number],
    default: ''
  }
},
watch: {
  data: {
    handler() {
      this.setDefaultCheckedKeys()
      this.setCurrentNodeKey()
    },
    immediate: true
  },
  rowKey: {
    type: [String, Function],
    default: 'id'
  },
  defaultCheckedKeys: {
    handler: 'setDefaultCheckedKeys',
    immediate: true
  },
  currentNodeKey: {
    handler: 'setCurrentNodeKey',
    immediate: true
  }
},
methods: {
  // 设置默认选中
  setDefaultCheckedKeys() {
    this.$nextTick(() => {
      if (this.defaultCheckedKeys.length) {
        const rows = this.data.filter(item => this.defaultCheckedKeys.includes(item[this.rowKey]))
        rows.forEach(row => {
          this.$refs.elTable.toggleRowSelection(row, true)
        })
      } else {
        this.$refs.elTable.clearSelection()
      }
    })
  },
  setCurrentNodeKey() {
    this.$nextTick(() => {
      if (this.currentNodeKey) {
        const row = this.data.find(item => this.currentNodeKey === item[this.rowKey])
        if (row) {
          this.$refs.elTable.setCurrentRow(row)
        }
      } else {
        this.$refs.elTable.setCurrentRow(null)
      }
    })
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66

# 实际工作中发现的问题

# 改变 editable 后页面没变化

最近项目增加了权限控制, 一些可编辑的表格需要先判断权限, 有权限才可以编辑.

所以需要动态修改 editable 的值, 初始为 false, 后续根据权限改变值.

但发现明明有权限, 却还是不可编辑状态, 而且跟踪代码也发现值确实改变了, 但页面却没有改变

只有触发页面重绘后才会变成可编辑状态, 就好像是状态其实已经改变了, 就差最后的绘制了

这时候突然想到是不是表格复用了, 仍然用的是不可编辑状态的表格: 利用 v-if 动态渲染表格时, 在 el-table-column 中添加 key 属性防止表格复用

查看代码发现果然是复用了: 可编辑和不可编辑用的相同的 key

<el-table-column
  v-if="column.editable"
  :key="column.prop"
  v-bind="column">
</el-table-column>
<el-table-column
  v-else
  :key="column.prop"
  v-bind="column">
</el-table-column>
1
2
3
4
5
6
7
8
9
10

当初封装组件的时候想着他俩肯定只能存在一个, 用相同的 key 应该没什么问题, 却忘了切换 editable 后, 相同的 key 会复用的问题了

那稍微修改一下吧:

<el-table-column
  v-if="column.editable"
  :key="`${column.prop}-edit`"
  v-bind="column">
</el-table-column>
<el-table-column
  v-else
  :key="column.prop"
  v-bind="column">
</el-table-column>
1
2
3
4
5
6
7
8
9
10

所以以后尽量还是用不同的 key 吧, 即使是 if else.

# 可编辑 table 有时候没有滚动条

引起这个问题主要有两个条件:

  1. table 中有可编辑组件
  2. 内容高度刚好超出设置高度一点点

当有可编辑组件时, tr 的高度比不可编辑时高了一点, 而 el-table 还是按不可编辑状态计算高度, 从而认为当前内容并没有超出设置高度, 所以没有出现滚动条

所以解决方法有两种:

要么将 tr 的高度设死, 这样可编辑和不可编辑高度一样, 就可以了; 不过样式可能不算美观

要么让 el-table 重新计算一下高度:

computed: {
  isEditable() {
    return this.columns.some(item => item.editable || item.editableMethod)
  }
},
watch: {
  data: {
    handler() {
      this.setDefaultCheckedKeys()
      this.setCurrentNodeKey()
      this.refreshLayout()
    },
    immediate: true
  }
},
methods: {
  refreshLayout() {
    if (!this.isEditable) return
    this.$nextTick(() => {
      setTimeout(() => {
        this.$refs.elTable.doLayout()
      }, 200)
    })
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

# table 设置 reserve-selection 无效

复现路径: 设置了 reserve-selection, 没有传入 defaultCheckedKeys, 选中一个, 上下移并重新获取数据后, 没有选中了

是由于 else 代码引起的:

methods: {
  // 设置默认选中
  setDefaultCheckedKeys() {
    this.$nextTick(() => {
      if (this.defaultCheckedKeys.length) {
        const rows = this.data.filter(item => this.defaultCheckedKeys.includes(item[this.rowKey]))
        rows.forEach(row => {
          this.$refs.elTable.toggleRowSelection(row, true)
        })
      } else {
        this.$refs.elTable.clearSelection()
      }
    })
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

由于没有传入 defaultCheckedKeys, 更新 data 数据后, 调用 setDefaultCheckedKeys 方法, 进入 else 判断, 清除所有选中, 导致 reserve-selection 失效

那把 else 干掉就好了

但有时候我确实想清除全部呢? 传入空数组后, 不进 if 判断, 不执行后面的逻辑, 相当于还是维持原样, 并没有达到清除全部的效果

那我们可以定义一个清空的关键字: clear, 如果 defaultCheckedKeys 等于这个关键字, 就清空

methods: {
  // 设置默认选中
  setDefaultCheckedKeys() {
    this.$nextTick(() => {
      if (this.defaultCheckedKeys === 'clear') {
        this.$refs.elTable.clearSelection()
        return
      }
      if (this.defaultCheckedKeys.length) {
        const rows = this.data.filter(item => this.defaultCheckedKeys.includes(item[this.rowKey]))
        rows.forEach(row => {
          this.$refs.elTable.toggleRowSelection(row, true)
        })
      }
    })
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

同理, setCurrentNodeKey 也需要这样处理一下. 不过这个可能会有很小的概率和 row-key 的值相同, 这个就需要酌情处理了. 目前本项目暂无此问题

# BaseTable column 如何获取 row

使用 el-table 我们知道可以通过 slot-scope 获取 row, 如下所示:

<el-table-column label="日期" width="180">
  <template slot-scope="scope">
    <i class="el-icon-time"></i>
    <span style="margin-left: 10px">{{ scope.row.date }}</span>
  </template>
</el-table-column>
1
2
3
4
5
6

那使用 BaseTable 如何获取 row 呢?

一开始想的是在 EditableElements 里处理, 这里以 disabled 为例:

EditableElements 简略代码:

<component
  :is="item.component"
  v-model="model[item.prop]"
  :key="item.prop"
  :disabled="typeof item.disabled === 'function' ? item.disabled(model) : !!item.disabled"
  v-bind="item"
  v-focus="item.focus"
  :placeholder="item.placeholder || `${handlePlaceholder(item.type)}${item.label}`"
  v-on="{ ...$listeners, ...item.events }"
></component>
1
2
3
4
5
6
7
8
9
10

使用方式:

columns: [
  {
    label: 'label',
    prop: 'prop',
    component: 'el-switch',
    disabled({ canSet }) {
      return !canSet
    }
  }
]
1
2
3
4
5
6
7
8
9
10

确实是可以了, 但这只是解决了一个 disabled, 如果后面还有很多需要 row 的字段, 如何处理呢? 总不能挨个加吧

这时突然想到以前遇到的一个 this 问题:

{
  label: 'label',
  prop: 'prop',
  component: 'el-switch',
  disabled({ canSet }) {
    console.log(this)
    return !canSet
  }
}
1
2
3
4
5
6
7
8
9

这里的 this 其实就是上面代码这整个对象, 也就是某一个 column, 而不是 Vue, 要想是 Vue 必须用箭头函数, 而不是这种方法简写方式

那咱们把这个 column 和 row 合并一下, this 是不是就都可以取到了呢?

而哪里有这两个数据呢? 必须是 EditableElements 里呀:

props: {
  item: {
    type: Object,
    default() {
      return {}
    }
  },
  model: {
    type: Object,
    default() {
      return {}
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

这里 item 就是 column, model 就是 row, 那合并一下是不是就可以啦

EditableElements 简略代码:

computed: {
  assignItem() {
    const assignItem = {}
    for (const key in this.model) {
      if (Object.hasOwnProperty.call(this.model, key)) {
        assignItem[`_${key}`] = this.model[key]
      }
    }
    return {...this.item, ...assignItem}
  }
}
1
2
3
4
5
6
7
8
9
10
11
<component
  :is="item.component"
  v-model="model[item.prop]"
  :key="item.prop"
  v-bind="assignItem"
  v-focus="item.focus"
  :placeholder="item.placeholder || `${handlePlaceholder(item.type)}${item.label}`"
  v-on="{ ...$listeners, ...item.events }"
></component>
1
2
3
4
5
6
7
8
9

使用方式:

columns: [
  {
    label: 'label',
    prop: 'prop',
    component: 'el-switch',
    disabled() {
      console.log(this)
      return !this._canSet
    }
  }
]
1
2
3
4
5
6
7
8
9
10
11

这里发现了两个问题:

问题一: this 还是 column, 而没有 row, 其实看上面代码也可以发现, 合并只是创建了一个新对象, 而没有对 this.item 改变, 所以没有合并成功

需要改成这样:

computed: {
  assignItem() {
    const assignItem = {}
    for (const key in this.model) {
      if (Object.hasOwnProperty.call(this.model, key)) {
        assignItem[`_${key}`] = this.model[key]
      }
    }
    return Object.assign(this.item, assignItem)
  }
}
1
2
3
4
5
6
7
8
9
10
11

或这样:

computed: {
  assignItem() {
    for (const key in this.model) {
      if (Object.hasOwnProperty.call(this.model, key)) {
        this.item[`_${key}`] = this.model[key]
      }
    }
    return this.item
  }
}
1
2
3
4
5
6
7
8
9
10

结果却出现一个有意思的现象: disabled 中打印的 this 有 _canSet, 而实际上却是 undefined.

其实这个问题是老生常谈了, 由于控制台输出的对象当我们点击时, 会取最新值, 但 disabled 调用的时候还没有合并(computed 只有在用到这个值时才执行), 所以还是取的合并前的值, 使用 JSON.stringify() 输出也可以看到是合并前的值

那我们就放到 created 中:

created() {
  for (const key in this.model) {
    if (Object.hasOwnProperty.call(this.model, key)) {
      this.item[`_${key}`] = this.model[key]
    }
  }
}
1
2
3
4
5
6
7

这样就可以取得了

问题二: 报错: Invalid prop: type check failed for prop "disabled". Expected Boolean, got Function

人家要布尔值, 咱们却给人传了一个方法, 类型不符了, 那只能将方法变成布尔了

created() {
  for (const key in this.model) {
    if (Object.hasOwnProperty.call(this.model, key)) {
      this.item[`_${key}`] = this.model[key]
    }
  }
  for (const key in this.item) {
    if (Object.hasOwnProperty.call(this.item, key)) {
      if (typeof this.item[key] === 'function') {
        this.item[key] = this.item[key]()
      }
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

这样就可以了, 但其实还有很多隐藏的问题: 万一 column 中还有别的方法, 而且这个方法不是立即执行, 或者有参数咋办?

那咱们是不是还要加一个字段, 定义要调用哪些方法

created() {
  for (const key in this.model) {
    if (Object.hasOwnProperty.call(this.model, key)) {
      this.item[`_${key}`] = this.model[key]
    }
  }
  for (const key in this.item) {
    if (Object.hasOwnProperty.call(this.item, key)) {
      if (
        typeof this.item[key] === 'function' &&
        this.item.callFun &&
        this.item.callFun.includes(key)
      ) {
        this.item[key] = this.item[key]()
      }
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

突然发现, 咱们做了这么多, 不就是要在 column 中拿到 row 吗?

那我们直接在 created 中把 row 传进去不就可以了吗?

created() {
  for (const key in this.item) {
    if (Object.hasOwnProperty.call(this.item, key)) {
      if (
        typeof this.item[key] === 'function' &&
        this.item.callFun &&
        this.item.callFun.includes(key)
      ) {
        this.item[key] = this.item[key](this.model)
      }
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

使用方式:

columns: [
  {
    label: 'label',
    prop: 'prop',
    component: 'el-switch',
    disabled(row) {
      return !row.canSet
    }
  }
]
1
2
3
4
5
6
7
8
9
10

这样也不用把 row 挂在 column 上了.

但这只是在初始化的时候处理了, 如果 this.model 改变了, 仍要处理一下, 那只能在 watch 中处理了, 这样也不用在 created 中处理了, watch 设置 immediate: true 即可

但还是会报错: Invalid prop: type check failed for prop "disabled". Expected Boolean, got Function, 应该是 this.model 改变后, 组件会立刻检查 disabled, 发现是一个方法, 报错, 同时监听方法触发, disabled 又成了一个布尔值

虽然功能是可以了, 但报错忍受不了, 而且个人感觉这种方式也不太好

目前暂无好方法

编辑 (opens new window)
#Js#ElementUI
上次更新: 5/27/2023, 1:02:05 PM
基于 ElementUI 封装的 NumberInput
基于 ElementUI 封装的 TreeTable

← 基于 ElementUI 封装的 NumberInput 基于 ElementUI 封装的 TreeTable→

最近更新
01
version 1.15
07-01
02
version 1.14
06-27
03
version 1.13
06-27
更多文章>
Theme by Vdoing | Copyright © 2017-2023 HenryTSZ | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式