Commit 8104d35f08d3dec4ac51d9c2f604181275bfe37a
1 parent
a57cd44f
Exists in
master
and in
1 other branch
feat: 兼容旧版表格编辑器
Showing
7 changed files
with
180 additions
and
222 deletions
Show diff stats
packages/index.js
| ... | ... | @@ -2,6 +2,7 @@ import Vue from 'vue'; |
| 2 | 2 | import ZTable from './table/index.vue'; |
| 3 | 3 | import ZTableNormal from './table/normal'; |
| 4 | 4 | import ZTableEditable from './table/editable'; |
| 5 | +import ZTableEditor from './table/editor'; | |
| 5 | 6 | import ElImageViewer from './upload/image-viewer'; |
| 6 | 7 | |
| 7 | 8 | let components = {}; |
| ... | ... | @@ -17,6 +18,7 @@ const install = function(Vue, opts = {}) { |
| 17 | 18 | components[ZTable.name] = ZTable; |
| 18 | 19 | components[ZTableNormal.name] = ZTableNormal; |
| 19 | 20 | components[ZTableEditable.name] = ZTableEditable; |
| 21 | + components[ZTableEditor.name] = ZTableEditor; | |
| 20 | 22 | Object.values(components).forEach(component => { |
| 21 | 23 | // 组件前缀 |
| 22 | 24 | const prefix = opts.name || 'z'; | ... | ... |
packages/table/editable copy.vue
| ... | ... | @@ -1,217 +0,0 @@ |
| 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/editable.vue
| ... | ... | @@ -61,14 +61,21 @@ |
| 61 | 61 | </template> |
| 62 | 62 | |
| 63 | 63 | <script> |
| 64 | -import TableNormal from './normal'; | |
| 65 | 64 | import tableProps from './props'; |
| 66 | 65 | import { cloneDeep, get, set } from '../utils'; |
| 67 | 66 | |
| 68 | 67 | export default { |
| 69 | 68 | name: 'TableEditable', |
| 70 | - extends: TableNormal, | |
| 71 | 69 | components: { |
| 70 | + CellRender: { | |
| 71 | + functional: true, | |
| 72 | + render(h, context) { | |
| 73 | + const props = context.props; | |
| 74 | + const item = props.item || {}; | |
| 75 | + const content = item.render(props.value, props.row, h, props.index); | |
| 76 | + return typeof content === 'string' ? h('span', {}, [content]) : content; | |
| 77 | + }, | |
| 78 | + }, | |
| 72 | 79 | cellEditor: { |
| 73 | 80 | props: { |
| 74 | 81 | value: [String, Number, Array, Object, Boolean], |
| ... | ... | @@ -119,6 +126,14 @@ export default { |
| 119 | 126 | }, |
| 120 | 127 | }, |
| 121 | 128 | }, |
| 129 | + inject: { | |
| 130 | + elForm: { | |
| 131 | + default: '', | |
| 132 | + }, | |
| 133 | + elFormItem: { | |
| 134 | + default: '', | |
| 135 | + }, | |
| 136 | + }, | |
| 122 | 137 | props: { |
| 123 | 138 | value: { |
| 124 | 139 | type: Array, |
| ... | ... | @@ -153,12 +168,31 @@ export default { |
| 153 | 168 | tableData: this.value, |
| 154 | 169 | }; |
| 155 | 170 | }, |
| 171 | + computed: { | |
| 172 | + _elFormItemSize() { | |
| 173 | + return (this.elFormItem || {}).elFormItemSize; | |
| 174 | + }, | |
| 175 | + _elSize() { | |
| 176 | + return this.size || this._elFormItemSize || (this.elForm || {}).size || (this.$ELEMENT || {}).size; | |
| 177 | + }, | |
| 178 | + bindProps() { | |
| 179 | + const tablePropsKeys = Object.keys(tableProps); | |
| 180 | + let props = {}; | |
| 181 | + Object.keys(this._props).forEach(key => { | |
| 182 | + if (tablePropsKeys.includes(key)) { | |
| 183 | + props[key] = this._props[key]; | |
| 184 | + } | |
| 185 | + }); | |
| 186 | + return props; | |
| 187 | + }, | |
| 188 | + }, | |
| 156 | 189 | filters: { |
| 157 | 190 | tableDataFilter(value) { |
| 158 | 191 | return value.map((item, index) => ({ ...item, $index: index })); |
| 159 | 192 | }, |
| 160 | 193 | }, |
| 161 | 194 | methods: { |
| 195 | + get, | |
| 162 | 196 | onHeaderClick() { |
| 163 | 197 | if (this.clickable) { |
| 164 | 198 | this.cancelEditCell(); | ... | ... |
| ... | ... | @@ -0,0 +1,130 @@ |
| 1 | +<script> | |
| 2 | +import { get, set, cloneDeep } from '../utils'; | |
| 3 | +import { renderContext } from '../utils/vnode'; | |
| 4 | + | |
| 5 | +// 标题渲染 | |
| 6 | +function headerRender(h, context, item) { | |
| 7 | + const headerSlot = context.scopedSlots[`header-${item.prop}`]; | |
| 8 | + return function(scope) { | |
| 9 | + // 自定义具名插槽 | |
| 10 | + if (headerSlot) { | |
| 11 | + return headerSlot(scope); | |
| 12 | + } | |
| 13 | + // 自定义渲染函数 | |
| 14 | + if (item.header) { | |
| 15 | + return item.header(item, h, scope); | |
| 16 | + } | |
| 17 | + // 默认取值 | |
| 18 | + return item.label; | |
| 19 | + }; | |
| 20 | +} | |
| 21 | + | |
| 22 | +// 单元格渲染 | |
| 23 | +function cellRender(h, context, item) { | |
| 24 | + const cellSlot = context.scopedSlots[`cell-${item.prop}`]; | |
| 25 | + return function(scope) { | |
| 26 | + const value = get(scope.row, item.prop); | |
| 27 | + // 自定义具名插槽 | |
| 28 | + if (cellSlot) { | |
| 29 | + return cellSlot({ item, value, index: scope.$index, ...scope }); | |
| 30 | + } | |
| 31 | + // 自定义渲染函数 | |
| 32 | + if (item.render) { | |
| 33 | + return item.render(value, scope.row, h, scope.$index); | |
| 34 | + } | |
| 35 | + // 默认取值 | |
| 36 | + return get(scope.row, item.prop); | |
| 37 | + }; | |
| 38 | +} | |
| 39 | + | |
| 40 | +// 编辑器渲染 | |
| 41 | +function editorRender(h, context, item) { | |
| 42 | + const editorSlot = context.scopedSlots[`editor-${item.prop}`]; | |
| 43 | + const contentProps = context.props || {}; | |
| 44 | + return function(scope) { | |
| 45 | + const value = get(scope.row, item.prop); | |
| 46 | + // 自定义具名插槽 | |
| 47 | + if (editorSlot) { | |
| 48 | + return editorSlot({ item, value, index: scope.$index, ...scope }); | |
| 49 | + } | |
| 50 | + // 默认 | |
| 51 | + const vnode = h(item.is, { | |
| 52 | + attrs: item.attrs, | |
| 53 | + props: { ...(item.props || {}), value }, | |
| 54 | + on: { | |
| 55 | + input(val) { | |
| 56 | + if (get(contentProps, 'editor.deep') === true) { | |
| 57 | + if (item.prop.indexOf('.') > -1 || item.prop.indexOf('[') > -1) { | |
| 58 | + let separator = ''; | |
| 59 | + if (item.prop.indexOf('.') > -1) { | |
| 60 | + separator = '.'; | |
| 61 | + } else if (item.prop.indexOf('[') > -1) { | |
| 62 | + separator = '['; | |
| 63 | + } | |
| 64 | + const path = item.prop.split(separator); | |
| 65 | + const bindProp = path[0]; | |
| 66 | + const propValue = cloneDeep(scope.row); | |
| 67 | + set(propValue, item.prop, val); | |
| 68 | + vnode.componentInstance.$set(scope.row, bindProp, propValue[bindProp]); | |
| 69 | + } else { | |
| 70 | + // set(scope.row, item.prop, val); | |
| 71 | + scope.row[item.prop] = val; | |
| 72 | + } | |
| 73 | + } else { | |
| 74 | + scope.row[item.prop] = val; | |
| 75 | + // set(contentProps.data, `[${[scope.$index]}]${item.prop}`, val); | |
| 76 | + } | |
| 77 | + if (item.on && item.on.input) { | |
| 78 | + item.on.input(val); | |
| 79 | + } | |
| 80 | + }, | |
| 81 | + }, | |
| 82 | + }); | |
| 83 | + return vnode; | |
| 84 | + }; | |
| 85 | +} | |
| 86 | + | |
| 87 | +// 跟进columns生成列 | |
| 88 | +function createElTableColumns(h, context, columns) { | |
| 89 | + const props = context.props || {}; | |
| 90 | + const editorConfig = props.editor || {}; | |
| 91 | + return columns.map((item, index) => { | |
| 92 | + const { attrs, on, ...props } = item; | |
| 93 | + const editorMatch = editorConfig.inputs.find(i => i.prop === item.prop); | |
| 94 | + // 处理插槽 | |
| 95 | + const scopedSlots = { | |
| 96 | + header: headerRender(h, context, item), | |
| 97 | + default: editorMatch ? editorRender(h, context, editorMatch) : cellRender(h, context, item), | |
| 98 | + }; | |
| 99 | + return h('el-table-column', { key: index, attrs, props, on, scopedSlots }); | |
| 100 | + }); | |
| 101 | +} | |
| 102 | + | |
| 103 | +export default { | |
| 104 | + name: 'TableEditor', | |
| 105 | + functional: true, | |
| 106 | + render(h, context) { | |
| 107 | + console.log(context); | |
| 108 | + const props = context.props || {}; | |
| 109 | + let scopedSlots = context.scopedSlots || {}; | |
| 110 | + // 如有默认插槽则相当于直接写el-table | |
| 111 | + if (scopedSlots.default) { | |
| 112 | + return h('el-table', context); | |
| 113 | + } | |
| 114 | + const columns = props.columns || []; | |
| 115 | + // 通过columns快速生成el-table-column | |
| 116 | + const elTableColumns = createElTableColumns(h, context, columns); | |
| 117 | + // 前置插槽 | |
| 118 | + const prependSlot = scopedSlots.prepend ? scopedSlots.prepend() : ''; | |
| 119 | + // 左侧插槽 | |
| 120 | + const leftSlot = scopedSlots.left ? scopedSlots.left() : ''; | |
| 121 | + // 右侧插槽 | |
| 122 | + const rightSlot = scopedSlots.right ? scopedSlots.right() : ''; | |
| 123 | + // 后置插槽 | |
| 124 | + const appendSlot = scopedSlots.append ? scopedSlots.append() : ''; | |
| 125 | + // 渲染组件时移除当前组件特有的props,避免透传不必要的参数 | |
| 126 | + delete context.columns; | |
| 127 | + return h('el-table', renderContext(context), [prependSlot, leftSlot, ...elTableColumns, rightSlot, appendSlot]); | |
| 128 | + }, | |
| 129 | +}; | |
| 130 | +</script> | ... | ... |
packages/table/index.vue
| ... | ... | @@ -6,8 +6,11 @@ export default { |
| 6 | 6 | functional: true, |
| 7 | 7 | render(h, context) { |
| 8 | 8 | const props = context.props || {}; |
| 9 | - if (props.editable) { | |
| 10 | - return ref('z-table-editable', context); | |
| 9 | + if (Object.prototype.hasOwnProperty.call(props, 'editable') && props.editable !== false) { | |
| 10 | + return h('z-table-editable', { props, scopedSlots: context.scopedSlots, on: context.listeners }); | |
| 11 | + } | |
| 12 | + if (props.editor) { | |
| 13 | + return ref('z-table-editor', context); | |
| 11 | 14 | } |
| 12 | 15 | return ref('z-table-normal', context); |
| 13 | 16 | }, | ... | ... |
packages/table/normal.vue
| ... | ... | @@ -6,9 +6,15 @@ import { renderContext } from '../utils/vnode'; |
| 6 | 6 | function headerRender(h, context, item) { |
| 7 | 7 | const headerSlot = context.scopedSlots[`header-${item.prop}`]; |
| 8 | 8 | return function(scope) { |
| 9 | + // 自定义具名插槽 | |
| 9 | 10 | if (headerSlot) { |
| 10 | 11 | return headerSlot(scope); |
| 11 | 12 | } |
| 13 | + // 自定义渲染函数 | |
| 14 | + if (item.header) { | |
| 15 | + return item.header(item, h, scope); | |
| 16 | + } | |
| 17 | + // 默认取值 | |
| 12 | 18 | return item.label; |
| 13 | 19 | }; |
| 14 | 20 | } | ... | ... |