Commit 98832d95e8a8b51d511ba28df8868fc7f5adae65
1 parent
4bdc1d1a
Exists in
master
and in
1 other branch
feat: 优化表格渲染逻辑
Showing
14 changed files
with
858 additions
and
177 deletions
Show diff stats
packages/index.js
packages/schema-page/index.vue
| ... | ... | @@ -36,36 +36,35 @@ |
| 36 | 36 | </slot> |
| 37 | 37 | </div> |
| 38 | 38 | <!-- 表格内容 --> |
| 39 | - <div v-if="schema.table || $scopedSlots.table" class="z-schema-page__table"> | |
| 39 | + <div v-if="schema.table || $scopedSlots.table" class="z-schema-page__table" v-loading="schema.loading !== false ? tableLoading : false"> | |
| 40 | 40 | <slot name="table" v-bind="_slotScope"> |
| 41 | - <z-schema-table | |
| 42 | - :size="_size" | |
| 43 | - :schema="tableSchemaDefaultProps(schema.table)" | |
| 44 | - v-model="tableData" | |
| 45 | - v-loading="schema.loading !== false ? tableLoading : false" | |
| 46 | - @selection-change="onTableSelectionChange" | |
| 47 | - > | |
| 48 | - <template #left> | |
| 49 | - <el-table-column v-if="schema.selection !== false" type="selection" width="40" align="center"></el-table-column> | |
| 41 | + <z-schema-table :size="_size" :schema="tableSchemaDefaultProps(schema.table)" :data="tableData" @selection-change="onTableSelectionChange"> | |
| 42 | + <template #prepend> | |
| 43 | + <slot name="table-prepend"> | |
| 44 | + <el-table-column v-if="schema.selection !== false" type="selection" width="40" align="center"></el-table-column> | |
| 45 | + </slot> | |
| 50 | 46 | </template> |
| 51 | 47 | <template v-for="item in getSlotKeys('table-')" #[item.name]="slotScope"> |
| 52 | 48 | <slot :name="item.slot" v-bind="{ ..._slotScope, ...slotScope }"></slot> |
| 53 | 49 | </template> |
| 54 | - <slot v-if="schema.operation !== false" name="operation" v-bind="_slotScope"> | |
| 55 | - <el-table-column v-bind="{ label: '操作', width: '90', align: 'center', ...(schema.operation || {}) }"> | |
| 56 | - <template #default="{ row, column, $index }"> | |
| 57 | - <div class="z-schema-page__table-operation"> | |
| 58 | - <slot name="operation-left" v-bind="{ ..._slotScope, row, column, $index }"></slot> | |
| 59 | - <slot name="operation-button" v-bind="{ ..._slotScope, row, column, $index }"></slot> | |
| 60 | - <el-button type="text" icon="el-icon-edit" title="编辑" @click="openEdit(row)"></el-button> | |
| 61 | - <el-popconfirm confirm-button-text="确定" cancel-button-text="取消" title="确定删除吗?" placement="top" @confirm="onDelete([row])"> | |
| 62 | - <el-button slot="reference" type="text" icon="el-icon-delete" title="删除"></el-button> | |
| 63 | - </el-popconfirm> | |
| 64 | - <slot name="operation-right" v-bind="{ ..._slotScope, row, column, $index }"></slot> | |
| 65 | - </div> | |
| 66 | - </template> | |
| 67 | - </el-table-column> | |
| 68 | - </slot> | |
| 50 | + <template #append> | |
| 51 | + <slot name="table-append" /> | |
| 52 | + <slot v-if="schema.operation !== false" name="operation" v-bind="_slotScope"> | |
| 53 | + <el-table-column v-bind="{ label: '操作', width: '90', align: 'center', ...(schema.operation || {}) }"> | |
| 54 | + <template #default="{ row, column, $index }"> | |
| 55 | + <div class="z-schema-page__table-operation"> | |
| 56 | + <slot name="operation-left" v-bind="{ ..._slotScope, row, column, $index }"></slot> | |
| 57 | + <slot name="operation-button" v-bind="{ ..._slotScope, row, column, $index }"></slot> | |
| 58 | + <el-button type="text" icon="el-icon-edit" title="编辑" @click="openEdit(row)"></el-button> | |
| 59 | + <el-popconfirm confirm-button-text="确定" cancel-button-text="取消" title="确定删除吗?" placement="top" @confirm="onDelete([row])"> | |
| 60 | + <el-button slot="reference" type="text" icon="el-icon-delete" title="删除"></el-button> | |
| 61 | + </el-popconfirm> | |
| 62 | + <slot name="operation-right" v-bind="{ ..._slotScope, row, column, $index }"></slot> | |
| 63 | + </div> | |
| 64 | + </template> | |
| 65 | + </el-table-column> | |
| 66 | + </slot> | |
| 67 | + </template> | |
| 69 | 68 | </z-schema-table> |
| 70 | 69 | </slot> |
| 71 | 70 | </div> | ... | ... |
packages/schema-table/index.vue
| 1 | 1 | <script> |
| 2 | +import { ref } from '../utils/vnode'; | |
| 3 | + | |
| 2 | 4 | export default { |
| 3 | 5 | name: 'SchemaTable', |
| 4 | - props: { | |
| 5 | - value: { | |
| 6 | - type: Array, | |
| 7 | - default() { | |
| 8 | - return []; | |
| 9 | - }, | |
| 10 | - }, | |
| 11 | - schema: { | |
| 12 | - required: true, | |
| 13 | - type: Object, | |
| 14 | - default() { | |
| 15 | - return {}; | |
| 16 | - }, | |
| 17 | - }, | |
| 18 | - size: String, | |
| 19 | - }, | |
| 20 | - data() { | |
| 21 | - return { | |
| 22 | - model: this.value, | |
| 23 | - }; | |
| 24 | - }, | |
| 25 | - watch: { | |
| 26 | - value(val = []) { | |
| 27 | - this.model = val; | |
| 28 | - }, | |
| 29 | - model: { | |
| 30 | - handler(val) { | |
| 31 | - this.$emit('input', val); | |
| 32 | - }, | |
| 33 | - deep: true, | |
| 34 | - }, | |
| 35 | - }, | |
| 36 | - render(h) { | |
| 37 | - const schema = this.schema || {}; | |
| 38 | - const _props = schema.props || {}; | |
| 39 | - const _on = schema.on || this.$listeners || {}; | |
| 40 | - return h('z-table', { props: { value: this.model, size: this.size, columns: schema.items, ..._props }, on: _on, scopedSlots: this.$scopedSlots }); | |
| 6 | + functional: true, | |
| 7 | + render(h, context) { | |
| 8 | + const props = context.props || {}; | |
| 9 | + // 当前函数式组件特有props | |
| 10 | + const schema = props.schema; | |
| 11 | + // 解析schema参数,设置到即将生成的组件上下文中 | |
| 12 | + context.props.columns = props.schema.items; | |
| 13 | + context.props = Object.assign(context.props, schema.props); | |
| 14 | + context.listeners = Object.assign(context.listeners, schema.on); | |
| 15 | + // 渲染组件时移除当前组件特有的props,避免透传不必要的参数 | |
| 16 | + delete context.props.schema; | |
| 17 | + return ref('z-table', context); | |
| 41 | 18 | }, |
| 42 | 19 | }; |
| 43 | 20 | </script> | ... | ... |
| ... | ... | @@ -0,0 +1,217 @@ |
| 1 | +<style lang="scss"> | |
| 2 | +.z-table-column__cell-editable { | |
| 3 | + display: inline-flex; | |
| 4 | + align-items: center; | |
| 5 | + justify-content: space-between; | |
| 6 | + width: 100%; | |
| 7 | + .el-icon-edit { | |
| 8 | + color: rgba(151, 151, 151, 0.5); | |
| 9 | + &:hover { | |
| 10 | + color: $primary; | |
| 11 | + } | |
| 12 | + } | |
| 13 | + .el-icon-check { | |
| 14 | + color: $green; | |
| 15 | + } | |
| 16 | + .el-icon-close { | |
| 17 | + color: $red; | |
| 18 | + } | |
| 19 | + .el-icon-edit, | |
| 20 | + .el-icon-check, | |
| 21 | + .el-icon-close { | |
| 22 | + cursor: pointer; | |
| 23 | + margin-left: 5px; | |
| 24 | + font-size: 14px; | |
| 25 | + } | |
| 26 | +} | |
| 27 | +</style> | |
| 28 | + | |
| 29 | +<template> | |
| 30 | + <el-table :data="tableData | tableDataFilter" :size="_elSize" v-bind="bindProps" v-on="$listeners" @header-click="onHeaderClick" @cell-click="onCellClick" @cell-dblclick="onCellDblclick"> | |
| 31 | + <slot name="left"></slot> | |
| 32 | + <template v-for="(item, index) in columns"> | |
| 33 | + <el-table-column v-bind="item" :key="index"> | |
| 34 | + <slot :name="`header-${item.prop}`" slot="header"></slot> | |
| 35 | + <template #default="{ row, column, $index }"> | |
| 36 | + <cell-editor | |
| 37 | + :disabled="item.editalways || editall || disabled || item.editable === false" | |
| 38 | + :editable="item.editalways || editall || (item.editable !== false && row.$editor && row.$editor.includes(item.prop))" | |
| 39 | + :component="item.component" | |
| 40 | + :value="row[column.property]" | |
| 41 | + @input="value => onCellInput(value, row, column, $index)" | |
| 42 | + @edit-click="setRowEditor(row, column, $index)" | |
| 43 | + @edit-confirm="value => onEditConfirm(value, row, column, $index)" | |
| 44 | + > | |
| 45 | + <template v-if="$scopedSlots[`editor-${item.prop}`]" slot="editor"> | |
| 46 | + <slot :name="`editor-${item.prop}`" :value="row[column.property]" :row="row" :index="$index" :onInput="value => onCellInput(value, row, column, $index)"></slot> | |
| 47 | + </template> | |
| 48 | + <template v-if="$scopedSlots[`cell-${item.prop}`]"> | |
| 49 | + <slot :name="`cell-${item.prop}`" :value="row[column.property]" :row="row" :index="$index"></slot> | |
| 50 | + </template> | |
| 51 | + <template v-else-if="item.render && typeof item.render === 'function'" #default="{ row, column, $index }"> | |
| 52 | + <cell-render :item="item" :value="get(row, item.prop)" :row="row" :column="column" :index="$index"></cell-render> | |
| 53 | + </template> | |
| 54 | + </cell-editor> | |
| 55 | + </template> | |
| 56 | + </el-table-column> | |
| 57 | + </template> | |
| 58 | + <slot></slot> | |
| 59 | + <slot name="append"></slot> | |
| 60 | + </el-table> | |
| 61 | +</template> | |
| 62 | + | |
| 63 | +<script> | |
| 64 | +import TableNormal from './normal'; | |
| 65 | +import tableProps from './props'; | |
| 66 | +import { cloneDeep, get, set } from '../utils'; | |
| 67 | + | |
| 68 | +export default { | |
| 69 | + name: 'TableEditable', | |
| 70 | + extends: TableNormal, | |
| 71 | + components: { | |
| 72 | + cellEditor: { | |
| 73 | + props: { | |
| 74 | + value: [String, Number, Array, Object, Boolean], | |
| 75 | + component: { type: String, default: 'el-input' }, | |
| 76 | + editable: Boolean, | |
| 77 | + disabled: Boolean, | |
| 78 | + }, | |
| 79 | + watch: { | |
| 80 | + editable(val) { | |
| 81 | + if (!this.disabled && val && this.component === 'el-input') { | |
| 82 | + this.$nextTick(() => { | |
| 83 | + this.$children[0] && this.$children[0].focus && this.$children[0].focus(); | |
| 84 | + }); | |
| 85 | + } | |
| 86 | + }, | |
| 87 | + }, | |
| 88 | + render(h) { | |
| 89 | + if (this.editable) { | |
| 90 | + let editorRender = [ | |
| 91 | + h(this.component, { | |
| 92 | + props: { value: this.value, size: 'mini' }, | |
| 93 | + on: { | |
| 94 | + input: value => { | |
| 95 | + this.$emit('input', value); | |
| 96 | + }, | |
| 97 | + }, | |
| 98 | + }), | |
| 99 | + ]; | |
| 100 | + if (this.$scopedSlots.editor) { | |
| 101 | + editorRender = [this.$scopedSlots.editor()]; | |
| 102 | + } | |
| 103 | + if (!this.disabled) { | |
| 104 | + const handlerItems = [h('i', { attrs: { title: '确定', class: 'el-icon-check' }, on: { click: () => this.$emit('edit-confirm', this.value) } })]; | |
| 105 | + // handlerItems.push(h('i', { attrs: { title: '取消', class: 'el-icon-close' }, on: { click: () => this.$emit('edit-confirm') } })); | |
| 106 | + const handler = h('span', handlerItems); | |
| 107 | + editorRender.push(handler); | |
| 108 | + } | |
| 109 | + return h('span', { class: 'z-table-column__cell-editable' }, editorRender); | |
| 110 | + } | |
| 111 | + let valueRender = [h('span', this.value)]; | |
| 112 | + if (this.$scopedSlots.default) { | |
| 113 | + valueRender = [this.$scopedSlots.default()]; | |
| 114 | + } | |
| 115 | + if (!this.disabled) { | |
| 116 | + valueRender.push(h('i', { attrs: { title: '编辑', class: 'el-icon-edit' }, on: { click: () => this.$emit('edit-click') } })); | |
| 117 | + } | |
| 118 | + return h('span', { class: 'z-table-column__cell-editable' }, valueRender); | |
| 119 | + }, | |
| 120 | + }, | |
| 121 | + }, | |
| 122 | + props: { | |
| 123 | + value: { | |
| 124 | + type: Array, | |
| 125 | + default() { | |
| 126 | + return []; | |
| 127 | + }, | |
| 128 | + }, | |
| 129 | + columns: { | |
| 130 | + type: Array, | |
| 131 | + default() { | |
| 132 | + return []; | |
| 133 | + }, | |
| 134 | + }, | |
| 135 | + editall: Boolean, | |
| 136 | + clickable: Boolean, | |
| 137 | + disabled: Boolean, | |
| 138 | + ...tableProps, | |
| 139 | + }, | |
| 140 | + watch: { | |
| 141 | + value(val) { | |
| 142 | + this.tableData = val || []; | |
| 143 | + }, | |
| 144 | + data(val) { | |
| 145 | + this.tableData = val || []; | |
| 146 | + }, | |
| 147 | + tableData(val) { | |
| 148 | + this.$emit('input', val || []); | |
| 149 | + }, | |
| 150 | + }, | |
| 151 | + data() { | |
| 152 | + return { | |
| 153 | + tableData: this.value, | |
| 154 | + }; | |
| 155 | + }, | |
| 156 | + filters: { | |
| 157 | + tableDataFilter(value) { | |
| 158 | + return value.map((item, index) => ({ ...item, $index: index })); | |
| 159 | + }, | |
| 160 | + }, | |
| 161 | + methods: { | |
| 162 | + onHeaderClick() { | |
| 163 | + if (this.clickable) { | |
| 164 | + this.cancelEditCell(); | |
| 165 | + } | |
| 166 | + }, | |
| 167 | + onCellClick(row, column) { | |
| 168 | + if (this.clickable) { | |
| 169 | + const prop = column.property; | |
| 170 | + let tableData = cloneDeep(this.tableData); | |
| 171 | + tableData.forEach((item, index) => { | |
| 172 | + if (!(index === row.$index && item.$editor && item.$editor.includes(prop))) { | |
| 173 | + item.$editor = []; | |
| 174 | + } | |
| 175 | + }); | |
| 176 | + this.tableData = tableData; | |
| 177 | + } | |
| 178 | + }, | |
| 179 | + onCellDblclick(row, column) { | |
| 180 | + if (this.clickable) { | |
| 181 | + this.setRowEditor(row, column, row.$index); | |
| 182 | + } | |
| 183 | + }, | |
| 184 | + setRowEditor(row, column, index) { | |
| 185 | + this.cancelEditCell(); | |
| 186 | + let tableRow = this.tableData[index]; | |
| 187 | + if (tableRow) { | |
| 188 | + if (tableRow.$editor) { | |
| 189 | + tableRow.$editor = [...tableRow.$editor, column.property]; | |
| 190 | + } else { | |
| 191 | + tableRow.$editor = [column.property]; | |
| 192 | + } | |
| 193 | + this.$set(this.tableData, index, tableRow); | |
| 194 | + } | |
| 195 | + }, | |
| 196 | + onEditConfirm(value, row, column, index) { | |
| 197 | + this.$emit('cell-edit-confirm', { row, index, prop: column.property, value }); | |
| 198 | + this.cancelEditCell(); | |
| 199 | + }, | |
| 200 | + cancelEditCell() { | |
| 201 | + this.tableData = this.tableData.map((item, index) => { | |
| 202 | + const newItem = cloneDeep(item); | |
| 203 | + delete newItem.$index; | |
| 204 | + delete newItem.$editor; | |
| 205 | + return newItem; | |
| 206 | + }); | |
| 207 | + }, | |
| 208 | + onCellInput(value, row, column, index) { | |
| 209 | + const tableData = cloneDeep(this.tableData); | |
| 210 | + const tableRow = tableData[index]; | |
| 211 | + set(tableRow, column.property, value); | |
| 212 | + tableData[index] = tableRow; | |
| 213 | + this.$set(this.tableData, index, tableRow); | |
| 214 | + }, | |
| 215 | + }, | |
| 216 | +}; | |
| 217 | +</script> | ... | ... |
| ... | ... | @@ -0,0 +1,27 @@ |
| 1 | +import tableProps from './props'; | |
| 2 | + | |
| 3 | +export default { | |
| 4 | + name: 'Table', | |
| 5 | + props: { | |
| 6 | + value: { | |
| 7 | + type: Array, | |
| 8 | + default() { | |
| 9 | + return []; | |
| 10 | + }, | |
| 11 | + }, | |
| 12 | + columns: { | |
| 13 | + type: Array, | |
| 14 | + default() { | |
| 15 | + return []; | |
| 16 | + }, | |
| 17 | + }, | |
| 18 | + editable: Boolean, | |
| 19 | + editall: Boolean, | |
| 20 | + clickable: Boolean, | |
| 21 | + disabled: Boolean, | |
| 22 | + ...tableProps, | |
| 23 | + }, | |
| 24 | + render(h) { | |
| 25 | + return h(`z-table-${this.editable ? 'editable' : 'normal'}`, { props: { ...this._props }, scopedSlots: this.$scopedSlots, on: this.$listeners }); | |
| 26 | + }, | |
| 27 | +}; | ... | ... |
| ... | ... | @@ -0,0 +1,95 @@ |
| 1 | +<template> | |
| 2 | + <el-table :data="tableData" :size="_elSize" v-bind="bindProps" v-on="$listeners"> | |
| 3 | + <slot name="left"></slot> | |
| 4 | + <template v-for="(item, index) in columns"> | |
| 5 | + <el-table-column v-bind="item" :key="index"> | |
| 6 | + <slot :name="`header-${item.prop}`" slot="header"></slot> | |
| 7 | + <template v-if="$scopedSlots[`cell-${item.prop}`]" #default="{ row, column, $index }"> | |
| 8 | + <slot :name="`cell-${item.prop}`" :value="get(row, item.prop)" :row="row" :column="column" :index="$index"></slot> | |
| 9 | + </template> | |
| 10 | + <template v-else-if="item.render && typeof item.render === 'function'" #default="{ row, column, $index }"> | |
| 11 | + <cell-render :item="item" :value="get(row, item.prop)" :row="row" :column="column" :index="$index"></cell-render> | |
| 12 | + </template> | |
| 13 | + </el-table-column> | |
| 14 | + </template> | |
| 15 | + <slot></slot> | |
| 16 | + <slot name="append"></slot> | |
| 17 | + </el-table> | |
| 18 | +</template> | |
| 19 | + | |
| 20 | +<script> | |
| 21 | +import tableProps from './props'; | |
| 22 | +import { get } from '../utils'; | |
| 23 | + | |
| 24 | +export default { | |
| 25 | + name: 'TableNormal', | |
| 26 | + components: { | |
| 27 | + CellRender: { | |
| 28 | + functional: true, | |
| 29 | + render(h, context) { | |
| 30 | + const props = context.props; | |
| 31 | + const item = props.item || {}; | |
| 32 | + const content = item.render(props.value, props.row, h, props.index); | |
| 33 | + return typeof content === 'string' ? h('span', {}, [content]) : content; | |
| 34 | + }, | |
| 35 | + }, | |
| 36 | + }, | |
| 37 | + inject: { | |
| 38 | + elForm: { | |
| 39 | + default: '', | |
| 40 | + }, | |
| 41 | + elFormItem: { | |
| 42 | + default: '', | |
| 43 | + }, | |
| 44 | + }, | |
| 45 | + props: { | |
| 46 | + value: { | |
| 47 | + type: Array, | |
| 48 | + default() { | |
| 49 | + return []; | |
| 50 | + }, | |
| 51 | + }, | |
| 52 | + columns: { | |
| 53 | + type: Array, | |
| 54 | + default() { | |
| 55 | + return []; | |
| 56 | + }, | |
| 57 | + }, | |
| 58 | + ...tableProps, | |
| 59 | + }, | |
| 60 | + data() { | |
| 61 | + return { | |
| 62 | + tableData: this.value.length > 0 ? this.value : this.data, | |
| 63 | + }; | |
| 64 | + }, | |
| 65 | + watch: { | |
| 66 | + value(val) { | |
| 67 | + this.tableData = val || []; | |
| 68 | + }, | |
| 69 | + data(val) { | |
| 70 | + this.tableData = val || []; | |
| 71 | + }, | |
| 72 | + }, | |
| 73 | + computed: { | |
| 74 | + _elFormItemSize() { | |
| 75 | + return (this.elFormItem || {}).elFormItemSize; | |
| 76 | + }, | |
| 77 | + _elSize() { | |
| 78 | + return this.size || this._elFormItemSize || (this.elForm || {}).size || (this.$ELEMENT || {}).size; | |
| 79 | + }, | |
| 80 | + bindProps() { | |
| 81 | + const tablePropsKeys = Object.keys(tableProps); | |
| 82 | + let props = {}; | |
| 83 | + Object.keys(this._props).forEach(key => { | |
| 84 | + if (tablePropsKeys.includes(key)) { | |
| 85 | + props[key] = this._props[key]; | |
| 86 | + } | |
| 87 | + }); | |
| 88 | + return props; | |
| 89 | + }, | |
| 90 | + }, | |
| 91 | + methods: { | |
| 92 | + get, | |
| 93 | + }, | |
| 94 | +}; | |
| 95 | +</script> | ... | ... |
| ... | ... | @@ -0,0 +1,62 @@ |
| 1 | +export default { | |
| 2 | + data: { | |
| 3 | + type: Array, | |
| 4 | + default: function() { | |
| 5 | + return []; | |
| 6 | + }, | |
| 7 | + }, | |
| 8 | + size: String, | |
| 9 | + width: [String, Number], | |
| 10 | + height: [String, Number], | |
| 11 | + maxHeight: [String, Number], | |
| 12 | + fit: { | |
| 13 | + type: Boolean, | |
| 14 | + default: true, | |
| 15 | + }, | |
| 16 | + stripe: Boolean, | |
| 17 | + border: Boolean, | |
| 18 | + rowKey: [String, Function], | |
| 19 | + context: {}, | |
| 20 | + showHeader: { | |
| 21 | + type: Boolean, | |
| 22 | + default: true, | |
| 23 | + }, | |
| 24 | + showSummary: Boolean, | |
| 25 | + sumText: String, | |
| 26 | + summaryMethod: Function, | |
| 27 | + rowClassName: [String, Function], | |
| 28 | + rowStyle: [Object, Function], | |
| 29 | + cellClassName: [String, Function], | |
| 30 | + cellStyle: [Object, Function], | |
| 31 | + headerRowClassName: [String, Function], | |
| 32 | + headerRowStyle: [Object, Function], | |
| 33 | + headerCellClassName: [String, Function], | |
| 34 | + headerCellStyle: [Object, Function], | |
| 35 | + highlightCurrentRow: Boolean, | |
| 36 | + currentRowKey: [String, Number], | |
| 37 | + emptyText: String, | |
| 38 | + expandRowKeys: Array, | |
| 39 | + defaultExpandAll: Boolean, | |
| 40 | + defaultSort: Object, | |
| 41 | + tooltipEffect: String, | |
| 42 | + spanMethod: Function, | |
| 43 | + selectOnIndeterminate: { | |
| 44 | + type: Boolean, | |
| 45 | + default: true, | |
| 46 | + }, | |
| 47 | + indent: { | |
| 48 | + type: Number, | |
| 49 | + default: 16, | |
| 50 | + }, | |
| 51 | + treeProps: { | |
| 52 | + type: Object, | |
| 53 | + default() { | |
| 54 | + return { | |
| 55 | + hasChildren: 'hasChildren', | |
| 56 | + children: 'children', | |
| 57 | + }; | |
| 58 | + }, | |
| 59 | + }, | |
| 60 | + lazy: Boolean, | |
| 61 | + load: Function, | |
| 62 | +}; | ... | ... |
| ... | ... | @@ -0,0 +1,217 @@ |
| 1 | +<style lang="scss"> | |
| 2 | +.z-table-column__cell-editable { | |
| 3 | + display: inline-flex; | |
| 4 | + align-items: center; | |
| 5 | + justify-content: space-between; | |
| 6 | + width: 100%; | |
| 7 | + .el-icon-edit { | |
| 8 | + color: rgba(151, 151, 151, 0.5); | |
| 9 | + &:hover { | |
| 10 | + color: $primary; | |
| 11 | + } | |
| 12 | + } | |
| 13 | + .el-icon-check { | |
| 14 | + color: $green; | |
| 15 | + } | |
| 16 | + .el-icon-close { | |
| 17 | + color: $red; | |
| 18 | + } | |
| 19 | + .el-icon-edit, | |
| 20 | + .el-icon-check, | |
| 21 | + .el-icon-close { | |
| 22 | + cursor: pointer; | |
| 23 | + margin-left: 5px; | |
| 24 | + font-size: 14px; | |
| 25 | + } | |
| 26 | +} | |
| 27 | +</style> | |
| 28 | + | |
| 29 | +<template> | |
| 30 | + <el-table :data="tableData | tableDataFilter" :size="_elSize" v-bind="bindProps" v-on="$listeners" @header-click="onHeaderClick" @cell-click="onCellClick" @cell-dblclick="onCellDblclick"> | |
| 31 | + <slot name="left"></slot> | |
| 32 | + <template v-for="(item, index) in columns"> | |
| 33 | + <el-table-column v-bind="item" :key="index"> | |
| 34 | + <slot :name="`header-${item.prop}`" slot="header"></slot> | |
| 35 | + <template #default="{ row, column, $index }"> | |
| 36 | + <cell-editor | |
| 37 | + :disabled="item.editalways || editall || disabled || item.editable === false" | |
| 38 | + :editable="item.editalways || editall || (item.editable !== false && row.$editor && row.$editor.includes(item.prop))" | |
| 39 | + :component="item.component" | |
| 40 | + :value="row[column.property]" | |
| 41 | + @input="value => onCellInput(value, row, column, $index)" | |
| 42 | + @edit-click="setRowEditor(row, column, $index)" | |
| 43 | + @edit-confirm="value => onEditConfirm(value, row, column, $index)" | |
| 44 | + > | |
| 45 | + <template v-if="$scopedSlots[`editor-${item.prop}`]" slot="editor"> | |
| 46 | + <slot :name="`editor-${item.prop}`" :value="row[column.property]" :row="row" :index="$index" :onInput="value => onCellInput(value, row, column, $index)"></slot> | |
| 47 | + </template> | |
| 48 | + <template v-if="$scopedSlots[`cell-${item.prop}`]"> | |
| 49 | + <slot :name="`cell-${item.prop}`" :value="row[column.property]" :row="row" :index="$index"></slot> | |
| 50 | + </template> | |
| 51 | + <template v-else-if="item.render && typeof item.render === 'function'" #default="{ row, column, $index }"> | |
| 52 | + <cell-render :item="item" :value="get(row, item.prop)" :row="row" :column="column" :index="$index"></cell-render> | |
| 53 | + </template> | |
| 54 | + </cell-editor> | |
| 55 | + </template> | |
| 56 | + </el-table-column> | |
| 57 | + </template> | |
| 58 | + <slot></slot> | |
| 59 | + <slot name="append"></slot> | |
| 60 | + </el-table> | |
| 61 | +</template> | |
| 62 | + | |
| 63 | +<script> | |
| 64 | +import TableNormal from './normal'; | |
| 65 | +import tableProps from './props'; | |
| 66 | +import { cloneDeep, get, set } from '../utils'; | |
| 67 | + | |
| 68 | +export default { | |
| 69 | + name: 'TableEditable', | |
| 70 | + extends: TableNormal, | |
| 71 | + components: { | |
| 72 | + cellEditor: { | |
| 73 | + props: { | |
| 74 | + value: [String, Number, Array, Object, Boolean], | |
| 75 | + component: { type: String, default: 'el-input' }, | |
| 76 | + editable: Boolean, | |
| 77 | + disabled: Boolean, | |
| 78 | + }, | |
| 79 | + watch: { | |
| 80 | + editable(val) { | |
| 81 | + if (!this.disabled && val && this.component === 'el-input') { | |
| 82 | + this.$nextTick(() => { | |
| 83 | + this.$children[0] && this.$children[0].focus && this.$children[0].focus(); | |
| 84 | + }); | |
| 85 | + } | |
| 86 | + }, | |
| 87 | + }, | |
| 88 | + render(h) { | |
| 89 | + if (this.editable) { | |
| 90 | + let editorRender = [ | |
| 91 | + h(this.component, { | |
| 92 | + props: { value: this.value, size: 'mini' }, | |
| 93 | + on: { | |
| 94 | + input: value => { | |
| 95 | + this.$emit('input', value); | |
| 96 | + }, | |
| 97 | + }, | |
| 98 | + }), | |
| 99 | + ]; | |
| 100 | + if (this.$scopedSlots.editor) { | |
| 101 | + editorRender = [this.$scopedSlots.editor()]; | |
| 102 | + } | |
| 103 | + if (!this.disabled) { | |
| 104 | + const handlerItems = [h('i', { attrs: { title: '确定', class: 'el-icon-check' }, on: { click: () => this.$emit('edit-confirm', this.value) } })]; | |
| 105 | + // handlerItems.push(h('i', { attrs: { title: '取消', class: 'el-icon-close' }, on: { click: () => this.$emit('edit-confirm') } })); | |
| 106 | + const handler = h('span', handlerItems); | |
| 107 | + editorRender.push(handler); | |
| 108 | + } | |
| 109 | + return h('span', { class: 'z-table-column__cell-editable' }, editorRender); | |
| 110 | + } | |
| 111 | + let valueRender = [h('span', this.value)]; | |
| 112 | + if (this.$scopedSlots.default) { | |
| 113 | + valueRender = [this.$scopedSlots.default()]; | |
| 114 | + } | |
| 115 | + if (!this.disabled) { | |
| 116 | + valueRender.push(h('i', { attrs: { title: '编辑', class: 'el-icon-edit' }, on: { click: () => this.$emit('edit-click') } })); | |
| 117 | + } | |
| 118 | + return h('span', { class: 'z-table-column__cell-editable' }, valueRender); | |
| 119 | + }, | |
| 120 | + }, | |
| 121 | + }, | |
| 122 | + props: { | |
| 123 | + value: { | |
| 124 | + type: Array, | |
| 125 | + default() { | |
| 126 | + return []; | |
| 127 | + }, | |
| 128 | + }, | |
| 129 | + columns: { | |
| 130 | + type: Array, | |
| 131 | + default() { | |
| 132 | + return []; | |
| 133 | + }, | |
| 134 | + }, | |
| 135 | + editall: Boolean, | |
| 136 | + clickable: Boolean, | |
| 137 | + disabled: Boolean, | |
| 138 | + ...tableProps, | |
| 139 | + }, | |
| 140 | + watch: { | |
| 141 | + value(val) { | |
| 142 | + this.tableData = val || []; | |
| 143 | + }, | |
| 144 | + data(val) { | |
| 145 | + this.tableData = val || []; | |
| 146 | + }, | |
| 147 | + tableData(val) { | |
| 148 | + this.$emit('input', val || []); | |
| 149 | + }, | |
| 150 | + }, | |
| 151 | + data() { | |
| 152 | + return { | |
| 153 | + tableData: this.value, | |
| 154 | + }; | |
| 155 | + }, | |
| 156 | + filters: { | |
| 157 | + tableDataFilter(value) { | |
| 158 | + return value.map((item, index) => ({ ...item, $index: index })); | |
| 159 | + }, | |
| 160 | + }, | |
| 161 | + methods: { | |
| 162 | + onHeaderClick() { | |
| 163 | + if (this.clickable) { | |
| 164 | + this.cancelEditCell(); | |
| 165 | + } | |
| 166 | + }, | |
| 167 | + onCellClick(row, column) { | |
| 168 | + if (this.clickable) { | |
| 169 | + const prop = column.property; | |
| 170 | + let tableData = cloneDeep(this.tableData); | |
| 171 | + tableData.forEach((item, index) => { | |
| 172 | + if (!(index === row.$index && item.$editor && item.$editor.includes(prop))) { | |
| 173 | + item.$editor = []; | |
| 174 | + } | |
| 175 | + }); | |
| 176 | + this.tableData = tableData; | |
| 177 | + } | |
| 178 | + }, | |
| 179 | + onCellDblclick(row, column) { | |
| 180 | + if (this.clickable) { | |
| 181 | + this.setRowEditor(row, column, row.$index); | |
| 182 | + } | |
| 183 | + }, | |
| 184 | + setRowEditor(row, column, index) { | |
| 185 | + this.cancelEditCell(); | |
| 186 | + let tableRow = this.tableData[index]; | |
| 187 | + if (tableRow) { | |
| 188 | + if (tableRow.$editor) { | |
| 189 | + tableRow.$editor = [...tableRow.$editor, column.property]; | |
| 190 | + } else { | |
| 191 | + tableRow.$editor = [column.property]; | |
| 192 | + } | |
| 193 | + this.$set(this.tableData, index, tableRow); | |
| 194 | + } | |
| 195 | + }, | |
| 196 | + onEditConfirm(value, row, column, index) { | |
| 197 | + this.$emit('cell-edit-confirm', { row, index, prop: column.property, value }); | |
| 198 | + this.cancelEditCell(); | |
| 199 | + }, | |
| 200 | + cancelEditCell() { | |
| 201 | + this.tableData = this.tableData.map((item, index) => { | |
| 202 | + const newItem = cloneDeep(item); | |
| 203 | + delete newItem.$index; | |
| 204 | + delete newItem.$editor; | |
| 205 | + return newItem; | |
| 206 | + }); | |
| 207 | + }, | |
| 208 | + onCellInput(value, row, column, index) { | |
| 209 | + const tableData = cloneDeep(this.tableData); | |
| 210 | + const tableRow = tableData[index]; | |
| 211 | + set(tableRow, column.property, value); | |
| 212 | + tableData[index] = tableRow; | |
| 213 | + this.$set(this.tableData, index, tableRow); | |
| 214 | + }, | |
| 215 | + }, | |
| 216 | +}; | |
| 217 | +</script> | ... | ... |
packages/table/index.js
| ... | ... | @@ -1,27 +0,0 @@ |
| 1 | -import tableProps from './props'; | |
| 2 | - | |
| 3 | -export default { | |
| 4 | - name: 'Table', | |
| 5 | - props: { | |
| 6 | - value: { | |
| 7 | - type: Array, | |
| 8 | - default() { | |
| 9 | - return []; | |
| 10 | - }, | |
| 11 | - }, | |
| 12 | - columns: { | |
| 13 | - type: Array, | |
| 14 | - default() { | |
| 15 | - return []; | |
| 16 | - }, | |
| 17 | - }, | |
| 18 | - editable: Boolean, | |
| 19 | - editall: Boolean, | |
| 20 | - clickable: Boolean, | |
| 21 | - disabled: Boolean, | |
| 22 | - ...tableProps, | |
| 23 | - }, | |
| 24 | - render(h) { | |
| 25 | - return h(`z-table-${this.editable ? 'editable' : 'normal'}`, { props: { ...this._props }, scopedSlots: this.$scopedSlots, on: this.$listeners }); | |
| 26 | - }, | |
| 27 | -}; |
| ... | ... | @@ -0,0 +1,15 @@ |
| 1 | +<script> | |
| 2 | +import { ref } from '../utils/vnode'; | |
| 3 | + | |
| 4 | +export default { | |
| 5 | + name: 'Table', | |
| 6 | + functional: true, | |
| 7 | + render(h, context) { | |
| 8 | + const props = context.props || {}; | |
| 9 | + if (props.editable) { | |
| 10 | + return ref('z-table-editable', context); | |
| 11 | + } | |
| 12 | + return ref('z-table-normal', context); | |
| 13 | + }, | |
| 14 | +}; | |
| 15 | +</script> | ... | ... |
| ... | ... | @@ -0,0 +1,95 @@ |
| 1 | +<template> | |
| 2 | + <el-table :data="tableData" :size="_elSize" v-bind="bindProps" v-on="$listeners"> | |
| 3 | + <slot name="left"></slot> | |
| 4 | + <template v-for="(item, index) in columns"> | |
| 5 | + <el-table-column v-bind="item" :key="index"> | |
| 6 | + <slot :name="`header-${item.prop}`" slot="header"></slot> | |
| 7 | + <template v-if="$scopedSlots[`cell-${item.prop}`]" #default="{ row, column, $index }"> | |
| 8 | + <slot :name="`cell-${item.prop}`" :value="get(row, item.prop)" :row="row" :column="column" :index="$index"></slot> | |
| 9 | + </template> | |
| 10 | + <template v-else-if="item.render && typeof item.render === 'function'" #default="{ row, column, $index }"> | |
| 11 | + <cell-render :item="item" :value="get(row, item.prop)" :row="row" :column="column" :index="$index"></cell-render> | |
| 12 | + </template> | |
| 13 | + </el-table-column> | |
| 14 | + </template> | |
| 15 | + <slot></slot> | |
| 16 | + <slot name="append"></slot> | |
| 17 | + </el-table> | |
| 18 | +</template> | |
| 19 | + | |
| 20 | +<script> | |
| 21 | +import tableProps from './props'; | |
| 22 | +import { get } from '../utils'; | |
| 23 | + | |
| 24 | +export default { | |
| 25 | + name: 'TableNormal', | |
| 26 | + components: { | |
| 27 | + CellRender: { | |
| 28 | + functional: true, | |
| 29 | + render(h, context) { | |
| 30 | + const props = context.props; | |
| 31 | + const item = props.item || {}; | |
| 32 | + const content = item.render(props.value, props.row, h, props.index); | |
| 33 | + return typeof content === 'string' ? h('span', {}, [content]) : content; | |
| 34 | + }, | |
| 35 | + }, | |
| 36 | + }, | |
| 37 | + inject: { | |
| 38 | + elForm: { | |
| 39 | + default: '', | |
| 40 | + }, | |
| 41 | + elFormItem: { | |
| 42 | + default: '', | |
| 43 | + }, | |
| 44 | + }, | |
| 45 | + props: { | |
| 46 | + value: { | |
| 47 | + type: Array, | |
| 48 | + default() { | |
| 49 | + return []; | |
| 50 | + }, | |
| 51 | + }, | |
| 52 | + columns: { | |
| 53 | + type: Array, | |
| 54 | + default() { | |
| 55 | + return []; | |
| 56 | + }, | |
| 57 | + }, | |
| 58 | + ...tableProps, | |
| 59 | + }, | |
| 60 | + data() { | |
| 61 | + return { | |
| 62 | + tableData: this.value.length > 0 ? this.value : this.data, | |
| 63 | + }; | |
| 64 | + }, | |
| 65 | + watch: { | |
| 66 | + value(val) { | |
| 67 | + this.tableData = val || []; | |
| 68 | + }, | |
| 69 | + data(val) { | |
| 70 | + this.tableData = val || []; | |
| 71 | + }, | |
| 72 | + }, | |
| 73 | + computed: { | |
| 74 | + _elFormItemSize() { | |
| 75 | + return (this.elFormItem || {}).elFormItemSize; | |
| 76 | + }, | |
| 77 | + _elSize() { | |
| 78 | + return this.size || this._elFormItemSize || (this.elForm || {}).size || (this.$ELEMENT || {}).size; | |
| 79 | + }, | |
| 80 | + bindProps() { | |
| 81 | + const tablePropsKeys = Object.keys(tableProps); | |
| 82 | + let props = {}; | |
| 83 | + Object.keys(this._props).forEach(key => { | |
| 84 | + if (tablePropsKeys.includes(key)) { | |
| 85 | + props[key] = this._props[key]; | |
| 86 | + } | |
| 87 | + }); | |
| 88 | + return props; | |
| 89 | + }, | |
| 90 | + }, | |
| 91 | + methods: { | |
| 92 | + get, | |
| 93 | + }, | |
| 94 | +}; | |
| 95 | +</script> | ... | ... |
packages/table/normal.vue
| 1 | -<template> | |
| 2 | - <el-table :data="tableData" :size="_elSize" v-bind="bindProps" v-on="$listeners"> | |
| 3 | - <slot name="left"></slot> | |
| 4 | - <template v-for="(item, index) in columns"> | |
| 5 | - <el-table-column v-bind="item" :key="index"> | |
| 6 | - <slot :name="`header-${item.prop}`" slot="header"></slot> | |
| 7 | - <template v-if="$scopedSlots[`cell-${item.prop}`]" #default="{ row, column, $index }"> | |
| 8 | - <slot :name="`cell-${item.prop}`" :value="get(row, item.prop)" :row="row" :column="column" :index="$index"></slot> | |
| 9 | - </template> | |
| 10 | - <template v-else-if="item.render && typeof item.render === 'function'" #default="{ row, column, $index }"> | |
| 11 | - <cell-render :item="item" :value="get(row, item.prop)" :row="row" :column="column" :index="$index"></cell-render> | |
| 12 | - </template> | |
| 13 | - </el-table-column> | |
| 14 | - </template> | |
| 15 | - <slot></slot> | |
| 16 | - <slot name="append"></slot> | |
| 17 | - </el-table> | |
| 18 | -</template> | |
| 19 | - | |
| 20 | 1 | <script> |
| 21 | -import tableProps from './props'; | |
| 22 | 2 | import { get } from '../utils'; |
| 23 | 3 | |
| 4 | +// 标题渲染 | |
| 5 | +function headerRender(h, context, item) { | |
| 6 | + const headerSlot = context.scopedSlots[`header-${item.prop}`]; | |
| 7 | + return function(scope) { | |
| 8 | + if (headerSlot) { | |
| 9 | + return headerSlot(scope); | |
| 10 | + } | |
| 11 | + return item.label; | |
| 12 | + }; | |
| 13 | +} | |
| 14 | + | |
| 15 | +// 单元格渲染 | |
| 16 | +function cellRender(h, context, item) { | |
| 17 | + const cellSlot = context.scopedSlots[`cell-${item.prop}`]; | |
| 18 | + return function(scope) { | |
| 19 | + const value = get(scope.row, item.prop); | |
| 20 | + // 自定义具名插槽 | |
| 21 | + if (cellSlot) { | |
| 22 | + return cellSlot({ | |
| 23 | + item, | |
| 24 | + value, | |
| 25 | + index: scope.$index, | |
| 26 | + ...scope, | |
| 27 | + }); | |
| 28 | + } | |
| 29 | + // 自定义渲染函数 | |
| 30 | + if (item.render) { | |
| 31 | + return item.render(value, scope.row, h, scope.$index); | |
| 32 | + } | |
| 33 | + // 默认取值 | |
| 34 | + return get(scope.row, item.prop); | |
| 35 | + }; | |
| 36 | +} | |
| 37 | + | |
| 38 | +// 跟进columns生成列 | |
| 39 | +function createElTableColumns(h, context, columns) { | |
| 40 | + return columns.map((item, index) => { | |
| 41 | + const { attrs, on, ...props } = item; | |
| 42 | + // 处理插槽 | |
| 43 | + const scopedSlots = { | |
| 44 | + header: headerRender(h, context, item), | |
| 45 | + default: cellRender(h, context, item), | |
| 46 | + }; | |
| 47 | + return h('el-table-column', { key: index, attrs, props, on, scopedSlots }); | |
| 48 | + }); | |
| 49 | +} | |
| 50 | + | |
| 24 | 51 | export default { |
| 25 | 52 | name: 'TableNormal', |
| 26 | - components: { | |
| 27 | - CellRender: { | |
| 28 | - functional: true, | |
| 29 | - render(h, context) { | |
| 30 | - const props = context.props; | |
| 31 | - const item = props.item || {}; | |
| 32 | - const content = item.render(props.value, props.row, h, props.index); | |
| 33 | - return typeof content === 'string' ? h('span', {}, [content]) : content; | |
| 34 | - }, | |
| 35 | - }, | |
| 36 | - }, | |
| 37 | - inject: { | |
| 38 | - elForm: { | |
| 39 | - default: '', | |
| 40 | - }, | |
| 41 | - elFormItem: { | |
| 42 | - default: '', | |
| 43 | - }, | |
| 44 | - }, | |
| 45 | - props: { | |
| 46 | - value: { | |
| 47 | - type: Array, | |
| 48 | - default() { | |
| 49 | - return []; | |
| 50 | - }, | |
| 51 | - }, | |
| 52 | - columns: { | |
| 53 | - type: Array, | |
| 54 | - default() { | |
| 55 | - return []; | |
| 56 | - }, | |
| 57 | - }, | |
| 58 | - ...tableProps, | |
| 59 | - }, | |
| 60 | - data() { | |
| 61 | - return { | |
| 62 | - tableData: this.value.length > 0 ? this.value : this.data, | |
| 63 | - }; | |
| 64 | - }, | |
| 65 | - watch: { | |
| 66 | - value(val) { | |
| 67 | - this.tableData = val || []; | |
| 68 | - }, | |
| 69 | - data(val) { | |
| 70 | - this.tableData = val || []; | |
| 71 | - }, | |
| 72 | - }, | |
| 73 | - computed: { | |
| 74 | - _elFormItemSize() { | |
| 75 | - return (this.elFormItem || {}).elFormItemSize; | |
| 76 | - }, | |
| 77 | - _elSize() { | |
| 78 | - return this.size || this._elFormItemSize || (this.elForm || {}).size || (this.$ELEMENT || {}).size; | |
| 79 | - }, | |
| 80 | - bindProps() { | |
| 81 | - const tablePropsKeys = Object.keys(tableProps); | |
| 82 | - let props = {}; | |
| 83 | - Object.keys(this._props).forEach(key => { | |
| 84 | - if (tablePropsKeys.includes(key)) { | |
| 85 | - props[key] = this._props[key]; | |
| 86 | - } | |
| 87 | - }); | |
| 88 | - return props; | |
| 89 | - }, | |
| 90 | - }, | |
| 91 | - methods: { | |
| 92 | - get, | |
| 53 | + functional: true, | |
| 54 | + render(h, context) { | |
| 55 | + const props = context.props || {}; | |
| 56 | + let scopedSlots = context.scopedSlots || {}; | |
| 57 | + // 如有默认插槽则相当于直接写el-table | |
| 58 | + if (scopedSlots.default) { | |
| 59 | + return h('el-table', context); | |
| 60 | + } | |
| 61 | + const columns = props.columns || []; | |
| 62 | + // 通过columns快速生成el-table-column | |
| 63 | + const elTableColumns = createElTableColumns(h, context, columns); | |
| 64 | + // 前置插槽 | |
| 65 | + const prependSlot = scopedSlots.prepend ? scopedSlots.prepend() : ''; | |
| 66 | + // 后置插槽 | |
| 67 | + const appendSlot = scopedSlots.append ? scopedSlots.append() : ''; | |
| 68 | + // 渲染组件时移除当前组件特有的props,避免透传不必要的参数 | |
| 69 | + delete context.columns; | |
| 70 | + return h('el-table', context, [prependSlot, ...elTableColumns, appendSlot]); | |
| 93 | 71 | }, |
| 94 | 72 | }; |
| 95 | 73 | </script> | ... | ... |
packages/table/props.js
| ... | ... | @@ -0,0 +1,25 @@ |
| 1 | +// 注册函数式组件ref | |
| 2 | +export function registerRef(vnode, context) { | |
| 3 | + if (!context.data.ref) { | |
| 4 | + return vnode; | |
| 5 | + } | |
| 6 | + // 备份vnode原有的insert周期函数 | |
| 7 | + const hackInsert = vnode.data.hook.insert; | |
| 8 | + // 新的vnode的insert周期函数 | |
| 9 | + vnode.data.hook.insert = function(config) { | |
| 10 | + hackInsert(config); | |
| 11 | + // 当vnode生成实例后,通过上下文反写入父组件的refs; | |
| 12 | + context.parent.$refs[context.data.ref] = config.componentInstance || config.elm; // ref本身就有组件实例和dom节点两种情况,优先取实例 | |
| 13 | + }; | |
| 14 | + return vnode; | |
| 15 | +} | |
| 16 | + | |
| 17 | +// 简写注册ref | |
| 18 | +export function ref(name, context) { | |
| 19 | + return registerRef(context._c(name, context), context); | |
| 20 | +} | |
| 21 | + | |
| 22 | +export default { | |
| 23 | + registerRef, | |
| 24 | + ref, | |
| 25 | +}; | ... | ... |