UNPKG

vxe-table

Version:

一个基于 vue 的 PC 端表格组件,支持增删改查、虚拟滚动、懒加载、快捷菜单、数据校验、树形结构、打印导出、表单渲染、数据分页、虚拟列表、模态窗口、自定义模板、渲染器、贼灵活的配置项、扩展接口等...

818 lines (806 loc) 27.8 kB
import Table from '../../table' import XEUtils from 'xe-utils/ctor' import GlobalConfig from '../../conf' import vSize from '../../mixins/size' import VXETable from '../../v-x-e-table' import { UtilTools, DomTools, GlobalEvent } from '../../tools' import { clearTableDefaultStatus, clearTableAllStatus } from '../../table/src/util' const methods = {} const propKeys = Object.keys(Table.props) function getOffsetHeight (elem) { return elem ? elem.offsetHeight : 0 } function getPaddingTopBottomSize (elem) { const computedStyle = getComputedStyle(elem) const paddingTop = XEUtils.toNumber(computedStyle.paddingTop) const paddingBottom = XEUtils.toNumber(computedStyle.paddingBottom) return paddingTop + paddingBottom } function renderDefaultForm (h, _vm) { const { proxyConfig, proxyOpts, formData, formConfig, formOpts } = _vm if (formConfig && formOpts.items && formOpts.items.length) { if (!formOpts.inited) { formOpts.inited = true const beforeItem = proxyOpts.beforeItem if (proxyOpts && beforeItem) { formOpts.items.forEach(item => { beforeItem.call(_vm, { $grid: _vm, item }) }) } } return [ h('vxe-form', { props: Object.assign({}, formOpts, { data: proxyConfig && proxyOpts.form ? formData : formOpts.data }), on: { submit: _vm.submitEvent, reset: _vm.resetEvent, 'submit-invalid': _vm.submitInvalidEvent, 'toggle-collapse': _vm.togglCollapseEvent } }) ] } return [] } function getToolbarSlots (_vm) { const { $scopedSlots, toolbarOpts } = _vm const toolbarOptSlots = toolbarOpts.slots let $buttons let $tools const slots = {} if (toolbarOptSlots) { $buttons = toolbarOptSlots.buttons $tools = toolbarOptSlots.tools if ($buttons && $scopedSlots[$buttons]) { $buttons = $scopedSlots[$buttons] } if ($tools && $scopedSlots[$tools]) { $tools = $scopedSlots[$tools] } } if ($buttons) { slots.buttons = $buttons } if ($tools) { slots.tools = $tools } return slots } function getPagerSlots (_vm) { const { $scopedSlots, pagerOpts } = _vm const pagerOptSlots = pagerOpts.slots const slots = {} let $left let $right if (pagerOptSlots) { $left = pagerOptSlots.left $right = pagerOptSlots.right if ($left && $scopedSlots[$left]) { $left = $scopedSlots[$left] } if ($right && $scopedSlots[$right]) { $right = $scopedSlots[$right] } } if ($left) { slots.left = $left } if ($right) { slots.right = $right } return slots } function getTableOns (_vm) { const { $listeners, proxyConfig, proxyOpts } = _vm const ons = {} XEUtils.each($listeners, (cb, type) => { ons[type] = (...args) => { _vm.$emit(type, ...args) } }) if (proxyConfig) { if (proxyOpts.sort) { ons['sort-change'] = _vm.sortChangeEvent } if (proxyOpts.filter) { ons['filter-change'] = _vm.filterChangeEvent } } return ons } Object.keys(Table.methods).forEach(name => { methods[name] = function (...args) { return this.$refs.xTable && this.$refs.xTable[name](...args) } }) export default { name: 'VxeGrid', mixins: [vSize], props: { ...Table.props, columns: Array, pagerConfig: [Boolean, Object], proxyConfig: Object, toolbar: [Boolean, Object], toolbarConfig: [Boolean, Object], formConfig: [Boolean, Object], zoomConfig: Object, size: { type: String, default: () => GlobalConfig.grid.size || GlobalConfig.size } }, provide () { return { $xegrid: this } }, data () { return { tableLoading: false, isZMax: false, tableData: [], pendingRecords: [], filterData: [], formData: {}, sortData: [], tZindex: 0, tablePage: { total: 0, pageSize: 10, currentPage: 1 } } }, computed: { isMsg () { return this.proxyOpts.message !== false }, proxyOpts () { return Object.assign({}, GlobalConfig.grid.proxyConfig, this.proxyConfig) }, pagerOpts () { return Object.assign({}, GlobalConfig.grid.pagerConfig, this.pagerConfig) }, formOpts () { return Object.assign({}, GlobalConfig.grid.formConfig, this.formConfig) }, toolbarOpts () { return Object.assign({}, GlobalConfig.grid.toolbarConfig, this.toolbarConfig || this.toolbar) }, zoomOpts () { return Object.assign({}, GlobalConfig.grid.zoomConfig, this.zoomConfig) }, renderStyle () { return this.isZMax ? { zIndex: this.tZindex } : null }, tableExtendProps () { const rest = {} propKeys.forEach(key => { rest[key] = this[key] }) return rest }, tableProps () { const { isZMax, seqConfig, pagerConfig, loading, editConfig, proxyConfig, proxyOpts, tableExtendProps, tableLoading, tablePage, tableData } = this const tableProps = Object.assign({}, tableExtendProps) if (isZMax) { if (tableExtendProps.maxHeight) { tableProps.maxHeight = 'auto' } else { tableProps.height = 'auto' } } if (proxyConfig) { tableProps.loading = loading || tableLoading tableProps.data = tableData tableProps.rowClassName = this.handleRowClassName if ((proxyOpts.seq || proxyOpts.index) && pagerConfig) { tableProps.seqConfig = Object.assign({}, seqConfig, { startIndex: (tablePage.currentPage - 1) * tablePage.pageSize }) } } if (editConfig) { tableProps.editConfig = Object.assign({}, editConfig, { activeMethod: this.handleActiveMethod }) } return tableProps }, pagerProps () { return Object.assign({}, this.pagerOpts, this.proxyConfig ? this.tablePage : {}) } }, watch: { columns (value) { this.$nextTick(() => this.loadColumn(value)) }, toolbar (value) { if (value) { this.initToolbar() } }, toolbarConfig (value) { if (value) { this.initToolbar() } }, proxyConfig () { this.initProxy() }, pagerConfig () { this.initPages() } }, created () { const { data, formOpts, proxyOpts, proxyConfig } = this if (proxyConfig && (data || (proxyOpts.form && formOpts.data))) { console.error('[vxe-grid] There is a conflict between the props proxy-config and data.') } if (process.env.VUE_APP_VXE_TABLE_ENV === 'development') { if (this.toolbar) { UtilTools.warn('vxe.error.delProp', ['grid.toolbar', 'grid.toolbar-config']) } if (this.toolbarConfig && !XEUtils.isObject(this.toolbarConfig)) { UtilTools.warn('vxe.error.errProp', [`grid.toolbar-config=${this.toolbarConfig}`, 'grid.toolbar-config={}']) } } GlobalEvent.on(this, 'keydown', this.handleGlobalKeydownEvent) }, mounted () { if (this.columns && this.columns.length) { this.loadColumn(this.columns) } this.initToolbar() this.initPages() this.initProxy() }, destroyed () { GlobalEvent.off(this, 'keydown') }, render (h) { const { $scopedSlots, vSize, isZMax } = this const hasForm = !!($scopedSlots.form || this.formConfig) const hasToolbar = !!($scopedSlots.toolbar || this.toolbarConfig || this.toolbar) const hasPager = !!($scopedSlots.pager || this.pagerConfig) return h('div', { class: ['vxe-grid', { [`size--${vSize}`]: vSize, 't--animat': !!this.animat, 'is--round': this.round, 'is--maximize': isZMax, 'is--loading': this.loading || this.tableLoading }], style: this.renderStyle }, [ /** * 渲染表单 */ hasForm ? h('div', { ref: 'formWrapper', class: 'vxe-grid--form-wrapper' }, $scopedSlots.form ? $scopedSlots.form.call(this, { $grid: this }, h) : renderDefaultForm(h, this) ) : null, /** * 渲染工具栏 */ hasToolbar ? h('div', { ref: 'toolbarWrapper', class: 'vxe-grid--toolbar-wrapper' }, $scopedSlots.toolbar ? $scopedSlots.toolbar.call(this, { $grid: this }, h) : [ h('vxe-toolbar', { props: this.toolbarOpts, ref: 'xToolbar', scopedSlots: getToolbarSlots(this) }) ] ) : null, /** * 渲染表格顶部区域 */ $scopedSlots.top ? h('div', { ref: 'topWrapper', class: 'vxe-grid--top-wrapper' }, $scopedSlots.top.call(this, { $grid: this }, h)) : null, /** * 渲染表格 */ h('vxe-table', { props: this.tableProps, on: getTableOns(this), scopedSlots: $scopedSlots, ref: 'xTable' }), /** * 渲染表格底部区域 */ $scopedSlots.bottom ? h('div', { ref: 'bottomWrapper', class: 'vxe-grid--bottom-wrapper' }, $scopedSlots.bottom.call(this, { $grid: this }, h)) : null, /** * 渲染分页 */ hasPager ? h('div', { ref: 'pagerWrapper', class: 'vxe-grid--pager-wrapper' }, $scopedSlots.pager ? $scopedSlots.pager.call(this, { $grid: this }, h) : [ h('vxe-pager', { props: this.pagerProps, on: { 'page-change': this.pageChangeEvent }, scopedSlots: getPagerSlots(this) }) ] ) : null ]) }, methods: { ...methods, getParentHeight () { return (this.isZMax ? DomTools.getDomNode().visibleHeight : this.$el.parentNode.clientHeight) - this.getExcludeHeight() }, /** * 获取需要排除的高度 */ getExcludeHeight () { const { $refs, $el, isZMax } = this const { formWrapper, toolbarWrapper, topWrapper, bottomWrapper, pagerWrapper } = $refs const parentPaddingSize = isZMax ? 0 : getPaddingTopBottomSize($el.parentNode) return parentPaddingSize + getPaddingTopBottomSize($el) + getOffsetHeight(formWrapper) + getOffsetHeight(toolbarWrapper) + getOffsetHeight(topWrapper) + getOffsetHeight(bottomWrapper) + getOffsetHeight(pagerWrapper) }, handleRowClassName (params) { const rowClassName = this.rowClassName const clss = [] if (this.pendingRecords.some(item => item === params.row)) { clss.push('row--pending') } return clss.push(rowClassName ? XEUtils.isFunction(rowClassName) ? rowClassName(params) : rowClassName : '') }, handleActiveMethod (params) { const { editConfig } = this const activeMethod = editConfig ? editConfig.activeMethod : null return this.pendingRecords.indexOf(params.row) === -1 && (!activeMethod || activeMethod(params)) }, loadColumn (columns) { const { $scopedSlots } = this XEUtils.eachTree(columns, column => { if (column.slots) { XEUtils.each(column.slots, (func, name, colSlots) => { if (!XEUtils.isFunction(func)) { if ($scopedSlots[func]) { colSlots[name] = $scopedSlots[func] } else { colSlots[name] = null UtilTools.error('vxe.error.notSlot', [func]) } } }) } }) this.$refs.xTable.loadColumn(columns) }, reloadColumn (columns) { this.clearAll() return this.loadColumn(columns) }, initToolbar () { this.$nextTick(() => { const { xTable, xToolbar } = this.$refs if (xTable && xToolbar) { xTable.connect(xToolbar) } }) }, initPages () { const { tablePage, pagerConfig, pagerOpts } = this const { currentPage, pageSize } = pagerOpts if (pagerConfig) { if (currentPage) { tablePage.currentPage = currentPage } if (pageSize) { tablePage.pageSize = pageSize } } }, initProxy () { const { proxyInited, proxyConfig, proxyOpts, formConfig, formOpts } = this if (proxyConfig) { if (formConfig && proxyOpts.form && formOpts.items) { const formData = {} formOpts.items.forEach(({ field, itemRender }) => { if (field) { formData[field] = itemRender && !XEUtils.isUndefined(itemRender.defaultValue) ? itemRender.defaultValue : undefined } }) this.formData = formData } if (!proxyInited && proxyOpts.autoLoad !== false) { this.proxyInited = true this.$nextTick(() => this.commitProxy('init')) } } }, handleGlobalKeydownEvent (evnt) { const isEsc = evnt.keyCode === 27 if (isEsc && this.isZMax && this.zoomOpts.escRestore !== false) { this.triggerZoomEvent(evnt) } }, /** * 提交指令,支持 code 或 button * @param {String/Object} code 字符串或对象 */ commitProxy (proxyTarget, ...args) { const { $refs, toolbar, toolbarConfig, toolbarOpts, proxyOpts, tablePage, pagerConfig, sortData, filterData, formData, isMsg } = this const { beforeQuery, afterQuery, beforeDelete, afterDelete, beforeSave, afterSave, ajax = {}, props: proxyProps = {} } = proxyOpts const $xetable = $refs.xTable let button let code if (XEUtils.isString(proxyTarget)) { const matchObj = toolbarConfig || toolbar ? XEUtils.findTree(toolbarOpts.buttons, item => item.code === proxyTarget, { children: 'dropdowns' }) : null code = proxyTarget button = matchObj ? matchObj.item : null } else { button = proxyTarget code = button.code } const btnParams = button ? button.params : null switch (code) { case 'insert': this.insert() break case 'insert_actived': this.insert().then(({ row }) => this.setActiveRow(row)) break case 'mark_cancel': this.triggerPendingEvent(code) break case 'remove': return this.handleDeleteRow(code, 'vxe.grid.removeSelectRecord', () => this.removeCheckboxRow()) case 'import': this.importData(btnParams) break case 'open_import': this.openImport(btnParams) break case 'export': this.exportData(btnParams) break case 'open_export': this.openExport(btnParams) break case 'reset_custom': this.resetColumn(true) break case 'init': case 'reload': case 'query': { const isInited = code === 'init' const isReload = code === 'reload' const ajaxMethods = ajax.query if (ajaxMethods) { const params = { code, button, $grid: this, sort: sortData.length ? sortData[0] : {}, sorts: sortData, filters: filterData, form: formData, options: ajaxMethods } if (pagerConfig) { if (isReload) { tablePage.currentPage = 1 } params.page = tablePage } if (isInited || isReload) { const checkedFilters = isInited ? this.getCheckedFilters() : [] let defaultSort = $xetable.sortOpts.defaultSort let sortParams = [] // 如果使用默认排序 if (defaultSort) { if (!XEUtils.isArray(defaultSort)) { defaultSort = [defaultSort] } sortParams = defaultSort } this.sortData = params.sorts = sortParams this.filterData = params.filters = isInited ? checkedFilters : [] this.pendingRecords = [] params.sort = params.sorts.length ? params.sorts[0] : {} this.$nextTick(() => { if (isInited) { clearTableDefaultStatus($xetable) } else { clearTableAllStatus($xetable) } }) } const applyArgs = [params].concat(args) this.tableLoading = true return Promise.resolve((beforeQuery || ajaxMethods)(...applyArgs)) .catch(e => e) .then(rest => { this.tableLoading = false if (rest) { if (pagerConfig) { tablePage.total = XEUtils.get(rest, proxyProps.total || 'page.total') || 0 this.tableData = XEUtils.get(rest, proxyProps.result || 'result') || [] } else { this.tableData = (proxyProps.list ? XEUtils.get(rest, proxyProps.list) : rest) || [] } } else { this.tableData = [] } if (afterQuery) { afterQuery(...applyArgs) } }) } else { UtilTools.error('vxe.error.notFunc', ['query']) } break } case 'delete': { const ajaxMethods = ajax.delete if (ajaxMethods) { const removeRecords = this.getCheckboxRecords() const body = { removeRecords } const applyArgs = [{ $grid: this, code, button, body, options: ajaxMethods }].concat(args) if (removeRecords.length) { return this.handleDeleteRow(code, 'vxe.grid.deleteSelectRecord', () => { this.tableLoading = true return Promise.resolve((beforeDelete || ajaxMethods)(...applyArgs)) .then(rest => { this.tableLoading = false this.pendingRecords = this.pendingRecords.filter(row => removeRecords.indexOf(row) === -1) if (isMsg) { VXETable.modal.message({ message: this.getRespMsg(rest, 'vxe.grid.delSuccess'), status: 'success' }) } if (afterDelete) { afterDelete(...applyArgs) } else { this.commitProxy('query') } }) .catch(rest => { this.tableLoading = false if (isMsg) { VXETable.modal.message({ id: code, message: this.getRespMsg(rest, 'vxe.grid.operError'), status: 'error' }) } }) }) } else { if (isMsg) { VXETable.modal.message({ id: code, message: GlobalConfig.i18n('vxe.grid.selectOneRecord'), status: 'warning' }) } } } else { UtilTools.error('vxe.error.notFunc', [code]) } break } case 'save': { const ajaxMethods = ajax.save if (ajaxMethods) { const body = Object.assign({ pendingRecords: this.pendingRecords }, this.getRecordset()) const { insertRecords, removeRecords, updateRecords, pendingRecords } = body const applyArgs = [{ $grid: this, code, button, body, options: ajaxMethods }].concat(args) // 排除掉新增且标记为删除的数据 if (insertRecords.length) { body.pendingRecords = pendingRecords.filter(row => insertRecords.indexOf(row) === -1) } // 排除已标记为删除的数据 if (pendingRecords.length) { body.insertRecords = insertRecords.filter(row => pendingRecords.indexOf(row) === -1) } // 只校验新增和修改的数据 return this.validate(body.insertRecords.concat(updateRecords)).then(() => { if (body.insertRecords.length || removeRecords.length || updateRecords.length || body.pendingRecords.length) { this.tableLoading = true return Promise.resolve((beforeSave || ajaxMethods)(...applyArgs)) .then(rest => { this.tableLoading = false this.pendingRecords = [] if (isMsg) { VXETable.modal.message({ message: this.getRespMsg(rest, 'vxe.grid.saveSuccess'), status: 'success' }) } if (afterSave) { afterSave(...applyArgs) } else { this.commitProxy('query') } }) .catch(rest => { this.tableLoading = false if (isMsg) { VXETable.modal.message({ id: code, message: this.getRespMsg(rest, 'vxe.grid.operError'), status: 'error' }) } }) } else { if (isMsg) { VXETable.modal.message({ id: code, message: GlobalConfig.i18n('vxe.grid.dataUnchanged'), status: 'info' }) } } }).catch(errMap => errMap) } else { UtilTools.error('vxe.error.notFunc', [code]) } break } default: { const btnMethod = VXETable.commands.get(code) if (btnMethod) { btnMethod({ code, button, $grid: this, $table: $xetable }, ...args) } } } return this.$nextTick() }, getRespMsg (rest, defaultMsg) { const { props: proxyProps = {} } = this.proxyOpts let msg if (rest && proxyProps.message) { msg = XEUtils.get(rest, proxyProps.message) } return msg || GlobalConfig.i18n(defaultMsg) }, handleDeleteRow (code, alertKey, callback) { const selectRecords = this.getCheckboxRecords() if (this.isMsg) { if (selectRecords.length) { return VXETable.modal.confirm({ id: `cfm_${code}`, message: GlobalConfig.i18n(alertKey), escClosable: true }).then(type => { if (type === 'confirm') { callback() } }) } else { VXETable.modal.message({ id: `msg_${code}`, message: GlobalConfig.i18n('vxe.grid.selectOneRecord'), status: 'warning' }) } } else { if (selectRecords.length) { callback() } } return Promise.resolve() }, getFormItems (index) { const { formConfig, formOpts } = this const items = formConfig && formOpts.items ? formOpts.items : [] return arguments.length ? items[index] : items }, getPendingRecords () { return this.pendingRecords }, triggerToolbarBtnEvent (button, evnt) { this.commitProxy(button, evnt) this.$emit('toolbar-button-click', { code: button.code, button, $grid: this, $event: evnt }) }, triggerPendingEvent (code) { const { pendingRecords, isMsg } = this const selectRecords = this.getCheckboxRecords() if (selectRecords.length) { const plus = [] const minus = [] selectRecords.forEach(data => { if (pendingRecords.some(item => data === item)) { minus.push(data) } else { plus.push(data) } }) if (minus.length) { this.pendingRecords = pendingRecords.filter(item => minus.indexOf(item) === -1).concat(plus) } else if (plus.length) { this.pendingRecords = pendingRecords.concat(plus) } this.clearCheckboxRow() } else { if (isMsg) { VXETable.modal.message({ id: code, message: GlobalConfig.i18n('vxe.grid.selectOneRecord'), status: 'warning' }) } } }, pageChangeEvent (params) { const { proxyConfig, tablePage } = this const { currentPage, pageSize } = params tablePage.currentPage = currentPage tablePage.pageSize = pageSize this.$emit('page-change', Object.assign({ $grid: this }, params)) if (proxyConfig) { this.commitProxy('query') } }, sortChangeEvent (params) { const { $table, column, sortList } = params const isRemote = XEUtils.isBoolean(column.remoteSort) ? column.remoteSort : $table.sortOpts.remote // 如果是服务端排序 if (isRemote) { this.sortData = sortList if (this.proxyConfig) { this.tablePage.currentPage = 1 this.commitProxy('query') } } this.$emit('sort-change', Object.assign({ $grid: this }, params)) }, filterChangeEvent (params) { const { $table, filters } = params // 如果是服务端过滤 if ($table.filterOpts.remote) { this.filterData = filters if (this.proxyConfig) { this.tablePage.currentPage = 1 this.commitProxy('query') } } this.$emit('filter-change', Object.assign({ $grid: this }, params)) }, submitEvent (params) { const { proxyConfig } = this if (proxyConfig) { this.commitProxy('reload') } this.$emit('form-submit', Object.assign({ $grid: this }, params)) }, resetEvent (params) { const { proxyConfig } = this if (proxyConfig) { this.commitProxy('reload') } this.$emit('form-reset', Object.assign({ $grid: this }, params)) }, submitInvalidEvent (params) { this.$emit('form-submit-invalid', Object.assign({ $grid: this }, params)) }, togglCollapseEvent (params) { this.$nextTick(() => this.recalculate(true)) this.$emit('form-toggle-collapse', Object.assign({ $grid: this }, params)) }, triggerZoomEvent (evnt) { this.zoom() this.$emit('zoom', { $grid: this, type: this.isZMax ? 'max' : 'revert', $event: evnt }) }, zoom () { return this[this.isZMax ? 'revert' : 'maximize']() }, isMaximized () { return this.isZMax }, maximize () { return this.handleZoom(true) }, revert () { return this.handleZoom() }, handleZoom (isMax) { const { isZMax } = this if (isMax ? !isZMax : isZMax) { this.isZMax = !isZMax if (this.tZindex < UtilTools.getLastZIndex()) { this.tZindex = UtilTools.nextZIndex() } } return this.$nextTick().then(() => this.recalculate(true)).then(() => this.isZMax) }, getProxyInfo () { const { sortData } = this return this.proxyConfig ? { data: this.tableData, filter: this.filterData, form: this.formData, sort: sortData.length ? sortData[0] : {}, sorts: sortData, pager: this.tablePage, pendingRecords: this.pendingRecords } : null } } }