├── .eslintrc.js ├── .gitee ├── ISSUE_TEMPLATE.en.md ├── ISSUE_TEMPLATE.md └── ISSUE_TEMPLATE.zh-TW.md ├── .github └── ISSUE_TEMPLATE │ ├── bug_report.yml │ └── feature_request.yml ├── .gitignore ├── LICENSE ├── README.md ├── gulpfile.js ├── index.ts ├── package.json ├── style.scss └── tsconfig.json /.eslintrc.js: -------------------------------------------------------------------------------- 1 | // https://eslint.org/docs/user-guide/configuring 2 | 3 | module.exports = { 4 | root: true, 5 | parser: '@typescript-eslint/parser', 6 | parserOptions: { 7 | ecmaVersion: 6, 8 | sourceType: 'module', 9 | ecmaFeatures: { 10 | impliedStrict: true, 11 | objectLiteralDuplicateProperties: false 12 | } 13 | }, 14 | env: { 15 | amd: true, 16 | browser: true, 17 | node: true 18 | }, 19 | plugins: [ 20 | '@typescript-eslint' 21 | ], 22 | extends: [ 23 | 'prettier/@typescript-eslint', 24 | 'standard' 25 | ], 26 | rules: { 27 | 'array-bracket-spacing': ['error', 'never'], 28 | 'no-debugger': ['error'], 29 | 'keyword-spacing': ['error'] 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /.gitee/ISSUE_TEMPLATE.en.md: -------------------------------------------------------------------------------- 1 | **(Required) Describe the bug or screenshots:** 2 | ? 3 | 4 | **(Required) Reproduction link:** 5 | ? 6 | 7 | **(Required) Expected behavior:** 8 | ? 9 | 10 | **(Required) Please fill in the version information:** 11 | 12 | - OS: ? 13 | - Browser: ? 14 | - vue: ? 15 | - vxe-table: ? 16 | -------------------------------------------------------------------------------- /.gitee/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | **(必填)请填写问题描述或截图:** 2 | ? 3 | 4 | **(必填)请填重在线链接:** 5 | ? 6 | 7 | **(必填)请填写期望的结果:** 8 | ? 9 | 10 | **(必填)请填写以下信息:** 11 | 12 | - OS: ? 13 | - Browser: ? 14 | - vue: ? 15 | - vxe-table: ? 16 | -------------------------------------------------------------------------------- /.gitee/ISSUE_TEMPLATE.zh-TW.md: -------------------------------------------------------------------------------- 1 | **(必填)請填寫問題描述或截圖:** 2 | ? 3 | 4 | **(必填)請填重線上連結:** 5 | ? 6 | 7 | **(必填)請填寫期望的結果:** 8 | ? 9 | 10 | **(必填)請填寫以下資訊:** 11 | 12 | - OS: ? 13 | - Browser: ? 14 | - vue: ? 15 | - vxe-table: ? 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug report 2 | description: 反馈问题 3 | labels: bug 4 | body: 5 | - type: input 6 | id: issue_link 7 | attributes: 8 | label: "可复现的链接:" 9 | description: "一个最小化的重现示例能让我们精确地定位问题,从而快速解决问题。如何创建,点击 v3:[codesandbox](https://codesandbox.io/s/vue-template-916h0)、[jsfiddle](https://jsfiddle.net/86p7Ltny/)、[jsrun](https://jsrun.net/vIyKp/edit) 或 v4:[codesandbox](https://codesandbox.io/s/vxe-table-wentiyanshi-forked-54v2j)、[jsfiddle](https://jsfiddle.net/9qoghkbj/)、[jsrun](https://jsrun.net/K5IKp/edit),将代码示例编辑后保存。" 10 | validations: 11 | required: true 12 | - type: textarea 13 | id: issue_describe 14 | attributes: 15 | label: "问题描述与截图:" 16 | validations: 17 | required: true 18 | - type: markdown 19 | attributes: 20 | value: "在发布问题之前,请仔细阅读所填写的步骤,以确保是详细和清晰的。" 21 | - type: input 22 | id: issue_expect 23 | attributes: 24 | label: "期望的结果:" 25 | - type: input 26 | id: issue_os_version 27 | attributes: 28 | label: "操作系统:" 29 | placeholder: "例如:window10" 30 | validations: 31 | required: true 32 | - type: input 33 | id: issue_browser_version 34 | attributes: 35 | label: "浏览器版本:" 36 | placeholder: "例如:chrome 95.0.4638.69" 37 | validations: 38 | required: true 39 | - type: input 40 | id: issue_vue_version 41 | attributes: 42 | label: "vue 版本:" 43 | placeholder: "例如:2.6.0" 44 | validations: 45 | required: true 46 | - type: input 47 | id: issue_vxe_version 48 | attributes: 49 | label: "vxe-table 版本:" 50 | placeholder: "例如:3.4.0" 51 | validations: 52 | required: true 53 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature request 2 | description: 功能需求 3 | labels: enhancement 4 | body: 5 | - type: textarea 6 | id: issue_describe 7 | attributes: 8 | label: "这个需求解决了什么问题:" 9 | validations: 10 | required: true 11 | - type: markdown 12 | attributes: 13 | value: "请先查看[最新文档](https://xuliangzhan_admin.gitee.io/vxe-table/#/table/api),确定该功能是否已有实现" 14 | - type: textarea 15 | id: issue_api_describe 16 | attributes: 17 | label: "建议的 API 是什么样的:" 18 | - type: markdown 19 | attributes: 20 | value: "描述一下希望该功能如何调用" 21 | - type: textarea 22 | id: issue_alternative_solution 23 | attributes: 24 | label: "是否已有其他不错的替代方案:" 25 | - type: markdown 26 | attributes: 27 | value: "如果有其他已实现的方案,可以通过链接或截图描述一下" 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | package-lock.json 5 | yarn.lock 6 | 7 | # local env files 8 | .env.local 9 | .env.*.local 10 | 11 | # Log files 12 | npm-debug.log* 13 | yarn-debug.log* 14 | yarn-error.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Xu Liangzhan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 由于 vxe-table 已经支持虚拟树,该插件将不再维护了 2 | 3 | [点击查看虚拟树文档](https://xuliangzhan_admin.gitee.io/vxe-table/#/table/scroll/tree) 4 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp') 2 | const del = require('del') 3 | const uglify = require('gulp-uglify') 4 | const babel = require('gulp-babel') 5 | const rename = require('gulp-rename') 6 | const replace = require('gulp-replace') 7 | const sass = require('gulp-sass') 8 | const cleanCSS = require('gulp-clean-css') 9 | const prefixer = require('gulp-autoprefixer') 10 | const sourcemaps = require('gulp-sourcemaps') 11 | const ts = require('gulp-typescript') 12 | const pack = require('./package.json') 13 | const tsconfig = require('./tsconfig.json') 14 | 15 | const exportModuleName = 'VXETablePluginVirtualTree' 16 | 17 | gulp.task('build_style', function () { 18 | return gulp.src('style.scss') 19 | .pipe(sass()) 20 | .pipe(prefixer({ 21 | borwsers: ['last 1 version', '> 1%', 'not ie <= 8'], 22 | cascade: true, 23 | remove: true 24 | })) 25 | .pipe(rename({ 26 | basename: 'style', 27 | extname: '.css' 28 | })) 29 | .pipe(gulp.dest('dist')) 30 | .pipe(cleanCSS()) 31 | .pipe(rename({ 32 | basename: 'style', 33 | suffix: '.min', 34 | extname: '.css' 35 | })) 36 | .pipe(gulp.dest('dist')) 37 | }) 38 | 39 | gulp.task('build_commonjs', function () { 40 | return gulp.src(['index.ts']) 41 | .pipe(sourcemaps.init()) 42 | .pipe(ts(tsconfig.compilerOptions)) 43 | .pipe(babel({ 44 | presets: ['@babel/env'] 45 | })) 46 | .pipe(rename({ 47 | basename: 'index', 48 | suffix: '.common', 49 | extname: '.js' 50 | })) 51 | .pipe(sourcemaps.write()) 52 | .pipe(gulp.dest('dist')) 53 | }) 54 | 55 | gulp.task('build_umd', function () { 56 | return gulp.src(['index.ts']) 57 | .pipe(ts(tsconfig.compilerOptions)) 58 | .pipe(babel({ 59 | moduleId: pack.name, 60 | presets: ['@babel/env'], 61 | plugins: [['@babel/transform-modules-umd', { 62 | globals: { 63 | [pack.name]: exportModuleName, 64 | 'vxe-table': 'VXETable', 65 | 'xe-utils': 'XEUtils' 66 | }, 67 | exactGlobals: true 68 | }]] 69 | })) 70 | .pipe(rename({ 71 | basename: 'index', 72 | suffix: '.umd', 73 | extname: '.js' 74 | })) 75 | .pipe(gulp.dest('dist')) 76 | .pipe(uglify()) 77 | .pipe(rename({ 78 | basename: 'index', 79 | suffix: '.umd.min', 80 | extname: '.js' 81 | })) 82 | .pipe(gulp.dest('dist')) 83 | }) 84 | 85 | gulp.task('clear', () => { 86 | return del([ 87 | 'dist/depend.*' 88 | ]) 89 | }) 90 | 91 | gulp.task('build', gulp.series(gulp.parallel('build_commonjs', 'build_umd', 'build_style'), 'clear')) 92 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | import Vue, { CreateElement, VNodeChildren, VNode } from 'vue' 3 | import XEUtils from 'xe-utils' 4 | import { 5 | VXETable, 6 | Table, 7 | Grid, 8 | RowInfo, 9 | ColumnOptions, 10 | ColumnCellRenderParams 11 | } from 'vxe-table' 12 | /* eslint-enable no-unused-vars */ 13 | 14 | function hasChilds (_vm: VirtualTree, row: RowInfo) { 15 | const childList = row[_vm.treeOpts.children] 16 | return childList && childList.length 17 | } 18 | 19 | function renderDefaultForm (h: CreateElement, _vm: VirtualTree) { 20 | const { proxyConfig, proxyOpts, formData, formConfig, formOpts } = _vm 21 | if (formConfig && formOpts.items && formOpts.items.length) { 22 | if (!formOpts.inited) { 23 | formOpts.inited = true 24 | const beforeItem = proxyOpts.beforeItem 25 | if (proxyOpts && beforeItem) { 26 | formOpts.items.forEach((item: any) => { 27 | beforeItem.call(_vm, { $grid: _vm, item }) 28 | }) 29 | } 30 | } 31 | return [ 32 | h('vxe-form', { 33 | props: Object.assign({}, formOpts, { 34 | data: proxyConfig && proxyOpts.form ? formData : formOpts.data 35 | }), 36 | on: { 37 | submit: _vm.submitEvent, 38 | reset: _vm.resetEvent, 39 | 'submit-invalid': _vm.submitInvalidEvent, 40 | 'toggle-collapse': _vm.togglCollapseEvent 41 | }, 42 | ref: 'form' 43 | }) 44 | ] 45 | } 46 | return [] 47 | } 48 | 49 | function getToolbarSlots (_vm: VirtualTree) { 50 | const { $scopedSlots, toolbarOpts } = _vm 51 | const toolbarOptSlots = toolbarOpts.slots 52 | let $buttons 53 | let $tools 54 | const slots: { [key: string]: any } = {} 55 | if (toolbarOptSlots) { 56 | $buttons = toolbarOptSlots.buttons 57 | $tools = toolbarOptSlots.tools 58 | if ($buttons && $scopedSlots[$buttons]) { 59 | $buttons = $scopedSlots[$buttons] 60 | } 61 | if ($tools && $scopedSlots[$tools]) { 62 | $tools = $scopedSlots[$tools] 63 | } 64 | } 65 | if ($buttons) { 66 | slots.buttons = $buttons 67 | } 68 | if ($tools) { 69 | slots.tools = $tools 70 | } 71 | return slots 72 | } 73 | 74 | function getPagerSlots (_vm: VirtualTree) { 75 | const { $scopedSlots, pagerOpts } = _vm 76 | const pagerOptSlots = pagerOpts.slots 77 | const slots: { [key: string]: any } = {} 78 | let $left 79 | let $right 80 | if (pagerOptSlots) { 81 | $left = pagerOptSlots.left 82 | $right = pagerOptSlots.right 83 | if ($left && $scopedSlots[$left]) { 84 | $left = $scopedSlots[$left] 85 | } 86 | if ($right && $scopedSlots[$right]) { 87 | $right = $scopedSlots[$right] 88 | } 89 | } 90 | if ($left) { 91 | slots.left = $left 92 | } 93 | if ($right) { 94 | slots.right = $right 95 | } 96 | return slots 97 | } 98 | 99 | function getTableOns (_vm: VirtualTree) { 100 | const { $listeners, proxyConfig, proxyOpts } = _vm 101 | const ons: { [key: string]: Function } = {} 102 | XEUtils.each($listeners, (cb, type) => { 103 | ons[type] = (...args: any[]) => { 104 | _vm.$emit(type, ...args) 105 | } 106 | }) 107 | ons['checkbox-all'] = _vm.checkboxAllEvent 108 | ons['checkbox-change'] = _vm.checkboxChangeEvent 109 | if (proxyConfig) { 110 | if (proxyOpts.sort) { 111 | ons['sort-change'] = _vm.sortChangeEvent 112 | } 113 | if (proxyOpts.filter) { 114 | ons['filter-change'] = _vm.filterChangeEvent 115 | } 116 | } 117 | return ons 118 | } 119 | 120 | function errorLog (log: string, args?: any) { 121 | console.error(args ? XEUtils.template(log, args) : log) 122 | } 123 | 124 | declare module 'vxe-table/lib/vxe-table' { 125 | interface VXETableStatic { 126 | Vue: typeof Vue; 127 | Grid: Grid; 128 | Table: Table; 129 | } 130 | } 131 | 132 | interface VirtualTree extends Grid { 133 | $refs: { 134 | xTable: Table; 135 | [key: string]: any; 136 | }; 137 | _loadTreeData(data: RowInfo[]): Promise; 138 | handleColumns(columns: ColumnOptions[]): ColumnOptions[]; 139 | toVirtualTree(treeData: RowInfo[]): RowInfo[]; 140 | handleExpanding(row: RowInfo): RowInfo[]; 141 | handleCollapsing(row: RowInfo): RowInfo[]; 142 | [key: string]: any; 143 | } 144 | 145 | interface VirtualTreeOptions { 146 | data?: (this: VirtualTree) => any; 147 | computed?: { [key: string]: (this: VirtualTree) => any } 148 | watch?: { [key: string]: (this: VirtualTree, ...args: any[]) => any } 149 | created?: (this: VirtualTree) => any; 150 | render?: (this: VirtualTree, h: CreateElement) => VNode; 151 | methods?: { [key: string]: (this: VirtualTree, ...args: any[]) => any } 152 | [key: string]: any; 153 | } 154 | 155 | function registerComponent (vxetable: typeof VXETable) { 156 | const { setup, t } = vxetable 157 | const GlobalConfig = setup() 158 | const propKeys = Object.keys(vxetable.Table.props).filter(name => ['data', 'treeConfig'].indexOf(name) === -1) 159 | 160 | const options: VirtualTreeOptions = { 161 | name: 'VxeVirtualTree', 162 | extends: vxetable.Grid, 163 | data () { 164 | return { 165 | removeList: [], 166 | treeLazyLoadeds: [] 167 | } 168 | }, 169 | computed: { 170 | treeOpts () { 171 | return Object.assign({}, GlobalConfig.table.treeConfig, this.treeConfig) 172 | }, 173 | checkboxOpts () { 174 | return Object.assign({}, GlobalConfig.table.checkboxConfig, this.checkboxConfig) 175 | }, 176 | tableExtendProps () { 177 | let rest: { [key: string]: any } = {} 178 | propKeys.forEach(key => { 179 | rest[key] = this[key] 180 | }) 181 | if (rest.checkboxConfig) { 182 | rest.checkboxConfig = this.checkboxOpts 183 | } 184 | return rest 185 | } 186 | }, 187 | watch: { 188 | columns (value: ColumnOptions[]) { 189 | this.handleColumns(value) 190 | }, 191 | data (value: any[]) { 192 | this.loadData(value) 193 | } 194 | }, 195 | created () { 196 | const { $vxe, treeOpts, data, columns } = this 197 | Object.assign(this, { 198 | fullTreeData: [], 199 | treeTableData: [], 200 | fullTreeRowMap: new Map() 201 | }) 202 | if (this.keepSource) { 203 | errorLog($vxe.t('vxe.error.notProp'), ['keep-source']) 204 | } 205 | if (treeOpts.line) { 206 | errorLog($vxe.t('vxe.error.notProp'), ['checkbox-config.line']) 207 | } 208 | if (columns) { 209 | this.handleColumns(columns) 210 | } 211 | if (data) { 212 | this.reloadData(data) 213 | } 214 | }, 215 | render (h: CreateElement) { 216 | const { vSize, isZMax } = this 217 | const $scopedSlots: any = this.$scopedSlots 218 | const hasForm = !!($scopedSlots.form || this.formConfig) 219 | const hasToolbar = !!($scopedSlots.toolbar || this.toolbarConfig || this.toolbar) 220 | const hasPager = !!($scopedSlots.pager || this.pagerConfig) 221 | return h('div', { 222 | class: ['vxe-grid', 'vxe-virtual-tree', { 223 | [`size--${vSize}`]: vSize, 224 | 't--animat': !!this.animat, 225 | 'is--round': this.round, 226 | 'is--maximize': isZMax, 227 | 'is--loading': this.loading || this.tableLoading 228 | }], 229 | style: this.renderStyle 230 | }, [ 231 | /** 232 | * 渲染表单 233 | */ 234 | hasForm ? h('div', { 235 | ref: 'formWrapper', 236 | staticClass: 'vxe-grid--form-wrapper' 237 | }, $scopedSlots.form 238 | ? $scopedSlots.form.call(this, { $grid: this }, h) 239 | : renderDefaultForm(h, this) 240 | ) : null, 241 | /** 242 | * 渲染工具栏 243 | */ 244 | hasToolbar ? h('div', { 245 | ref: 'toolbarWrapper', 246 | class: 'vxe-grid--toolbar-wrapper' 247 | }, $scopedSlots.toolbar 248 | ? $scopedSlots.toolbar.call(this, { $grid: this }, h) 249 | : [ 250 | h('vxe-toolbar', { 251 | props: this.toolbarOpts, 252 | ref: 'xToolbar', 253 | scopedSlots: getToolbarSlots(this) 254 | }) 255 | ] 256 | ) : null, 257 | /** 258 | * 渲染表格顶部区域 259 | */ 260 | $scopedSlots.top ? h('div', { 261 | ref: 'topWrapper', 262 | staticClass: 'vxe-grid--top-wrapper' 263 | }, $scopedSlots.top.call(this, { $grid: this }, h)) : null, 264 | /** 265 | * 渲染表格 266 | */ 267 | h('vxe-table', { 268 | props: this.tableProps, 269 | on: getTableOns(this), 270 | scopedSlots: $scopedSlots, 271 | ref: 'xTable' 272 | }, this.$slots.default), 273 | /** 274 | * 渲染表格底部区域 275 | */ 276 | $scopedSlots.bottom ? h('div', { 277 | ref: 'bottomWrapper', 278 | staticClass: 'vxe-grid--bottom-wrapper' 279 | }, $scopedSlots.bottom.call(this, { $grid: this }, h)) : null, 280 | /** 281 | * 渲染分页 282 | */ 283 | hasPager ? h('div', { 284 | ref: 'pagerWrapper', 285 | staticClass: 'vxe-grid--pager-wrapper' 286 | }, $scopedSlots.pager 287 | ? $scopedSlots.pager.call(this, { $grid: this }, h) 288 | : [ 289 | h('vxe-pager', { 290 | props: this.pagerProps, 291 | on: { 292 | 'page-change': this.pageChangeEvent 293 | }, 294 | scopedSlots: getPagerSlots(this) 295 | }) 296 | ] 297 | ) : null 298 | ]) 299 | }, 300 | methods: { 301 | loadColumn (columns: ColumnOptions[]) { 302 | return this.$nextTick().then(() => { 303 | const { $vxe, $scopedSlots, renderTreeIcon, treeOpts } = this 304 | XEUtils.eachTree(columns, column => { 305 | if (column.treeNode) { 306 | if (!column.slots) { 307 | column.slots = {} 308 | } 309 | column.slots.icon = renderTreeIcon 310 | } 311 | if (column.slots) { 312 | XEUtils.each(column.slots, (func, name, slots) => { 313 | if (!XEUtils.isFunction(func)) { 314 | if ($scopedSlots[func]) { 315 | slots[name] = $scopedSlots[func] 316 | } else { 317 | slots[name] = null 318 | errorLog($vxe.t('vxe.error.notSlot'), [func]) 319 | } 320 | } 321 | }) 322 | } 323 | }, treeOpts) 324 | this.$refs.xTable.loadColumn(columns) 325 | }) 326 | }, 327 | renderTreeIcon (params: ColumnCellRenderParams, h: CreateElement, cellVNodes: VNodeChildren) { 328 | const { treeLazyLoadeds, treeOpts } = this 329 | let { isHidden, row } = params 330 | const { children, hasChild, indent, lazy, trigger, iconLoaded, showIcon, iconOpen, iconClose } = treeOpts 331 | let rowChilds = row[children] 332 | let hasLazyChilds = false 333 | let isAceived = false 334 | let isLazyLoaded = false 335 | let on: { [key: string]: Function } = {} 336 | if (!isHidden) { 337 | isAceived = row._X_EXPAND 338 | if (lazy) { 339 | isLazyLoaded = treeLazyLoadeds.indexOf(row) > -1 340 | hasLazyChilds = row[hasChild] 341 | } 342 | } 343 | if (!trigger || trigger === 'default') { 344 | on.click = (evnt: Event) => this.triggerTreeExpandEvent(evnt, params) 345 | } 346 | return [ 347 | h('div', { 348 | class: ['vxe-cell--tree-node', { 349 | 'is--active': isAceived 350 | }], 351 | style: { 352 | paddingLeft: `${row._X_LEVEL * indent}px` 353 | } 354 | }, [ 355 | showIcon && ((rowChilds && rowChilds.length) || hasLazyChilds) ? [ 356 | h('div', { 357 | class: 'vxe-tree--btn-wrapper', 358 | on 359 | }, [ 360 | h('i', { 361 | class: ['vxe-tree--node-btn', isLazyLoaded ? (iconLoaded || GlobalConfig.icon.TABLE_TREE_LOADED) : (isAceived ? (iconOpen || GlobalConfig.icon.TABLE_TREE_OPEN) : (iconClose || GlobalConfig.icon.TABLE_TREE_CLOSE))] 362 | }) 363 | ]) 364 | ] : null, 365 | h('div', { 366 | class: 'vxe-tree-cell' 367 | }, cellVNodes) 368 | ]) 369 | ] 370 | }, 371 | _loadTreeData (data: RowInfo[]) { 372 | const selectRow = this.getRadioRecord() 373 | return this.$nextTick() 374 | .then(() => this.$refs.xTable.loadData(data)) 375 | .then(() => { 376 | if (selectRow) { 377 | this.setRadioRow(selectRow) 378 | } 379 | }) 380 | }, 381 | loadData (data: any[]) { 382 | return this._loadTreeData(this.toVirtualTree(data)) 383 | }, 384 | reloadData (data: any[]) { 385 | return this.$nextTick() 386 | .then(() => this.$refs.xTable.reloadData(this.toVirtualTree(data))) 387 | .then(() => this.handleDefaultTreeExpand()) 388 | }, 389 | isTreeExpandByRow (row: RowInfo) { 390 | return !!row._X_EXPAND 391 | }, 392 | setTreeExpansion (rows: RowInfo | RowInfo[], expanded: boolean) { 393 | return this.setTreeExpand(rows, expanded) 394 | }, 395 | handleAsyncTreeExpandChilds (row: RowInfo) { 396 | const { treeLazyLoadeds, treeOpts, checkboxOpts } = this 397 | const { loadMethod, children } = treeOpts 398 | const { checkStrictly } = checkboxOpts 399 | return new Promise(resolve => { 400 | if (loadMethod) { 401 | treeLazyLoadeds.push(row) 402 | loadMethod({ row }).catch(() => []).then((childs: any[]) => { 403 | row._X_LOADED = true 404 | XEUtils.remove(treeLazyLoadeds, item => item === row) 405 | if (!XEUtils.isArray(childs)) { 406 | childs = [] 407 | } 408 | if (childs) { 409 | row[children] = childs.map(item => { 410 | item._X_LOADED = false 411 | item._X_EXPAND = false 412 | item._X_INSERT = false 413 | item._X_LEVEL = row._X_LEVEL + 1 414 | return item 415 | }) 416 | if (childs.length && !row._X_EXPAND) { 417 | this.virtualExpand(row, true) 418 | } 419 | // 如果当前节点已选中,则展开后子节点也被选中 420 | if (!checkStrictly && this.isCheckedByCheckboxRow(row)) { 421 | this.setCheckboxRow(childs, true) 422 | } 423 | } 424 | resolve(this.$nextTick().then(() => this.recalculate())) 425 | }) 426 | } else { 427 | resolve() 428 | } 429 | }) 430 | }, 431 | setTreeExpand (rows: any, expanded: boolean) { 432 | const { treeLazyLoadeds, treeOpts, tableFullData, treeNodeColumn } = this 433 | const { lazy, hasChild, accordion, toggleMethod } = treeOpts 434 | const result: any[] = [] 435 | if (rows) { 436 | if (!XEUtils.isArray(rows)) { 437 | rows = [rows] 438 | } 439 | const columnIndex = this.getColumnIndex(treeNodeColumn) 440 | const $columnIndex = this.getVMColumnIndex(treeNodeColumn) 441 | let validRows = toggleMethod ? rows.filter((row: RowInfo) => toggleMethod({ expanded, column: treeNodeColumn, row, columnIndex, $columnIndex })) : rows 442 | if (accordion) { 443 | validRows = validRows.length ? [validRows[validRows.length - 1]] : [] 444 | // 同一级只能展开一个 445 | const matchObj = XEUtils.findTree(tableFullData, item => item === rows[0], treeOpts) 446 | if (matchObj) { 447 | matchObj.items.forEach(row => { 448 | row._X_EXPAND = false 449 | }) 450 | } 451 | } 452 | validRows.forEach((row: any) => { 453 | const isLoad = lazy && row[hasChild] && !row._X_LOADED && treeLazyLoadeds.indexOf(row) === -1 454 | // 是否使用懒加载 455 | if (expanded && isLoad) { 456 | result.push(this.handleAsyncTreeExpandChilds(row)) 457 | } else { 458 | if (hasChilds(this, row)) { 459 | this.virtualExpand(row, !!expanded) 460 | } 461 | } 462 | }) 463 | return Promise.all(result).then(() => { 464 | this._loadTreeData(this.treeTableData) 465 | return this.recalculate() 466 | }) 467 | } 468 | return this.$nextTick() 469 | }, 470 | setAllTreeExpansion (expanded: boolean) { 471 | return this.setAllTreeExpand(expanded) 472 | }, 473 | setAllTreeExpand (expanded: boolean) { 474 | return this._loadTreeData(this.virtualAllExpand(expanded)) 475 | }, 476 | toggleTreeExpansion (row: RowInfo) { 477 | return this.toggleTreeExpand(row) 478 | }, 479 | triggerTreeExpandEvent (evnt: Event, params: ColumnCellRenderParams) { 480 | const { treeOpts, treeLazyLoadeds } = this 481 | const { row, column } = params 482 | const { lazy } = treeOpts 483 | if (!lazy || treeLazyLoadeds.indexOf(row) === -1) { 484 | const expanded = !this.isTreeExpandByRow(row) 485 | this.setTreeExpand(row, expanded) 486 | this.$emit('toggle-tree-expand', { expanded, column, row, $event: evnt }) 487 | } 488 | }, 489 | toggleTreeExpand (row: RowInfo) { 490 | return this._loadTreeData(this.virtualExpand(row, !row._X_EXPAND)) 491 | }, 492 | getTreeExpandRecords () { 493 | const { fullTreeData, treeOpts } = this 494 | const treeExpandRecords: RowInfo[] = [] 495 | XEUtils.eachTree(fullTreeData, row => { 496 | if (row._X_EXPAND && hasChilds(this, row)) { 497 | treeExpandRecords.push(row) 498 | } 499 | }, treeOpts) 500 | return treeExpandRecords 501 | }, 502 | clearTreeExpand () { 503 | return this.setAllTreeExpand(false) 504 | }, 505 | handleColumns (columns: ColumnOptions[]) { 506 | const { $vxe, renderTreeIcon, checkboxOpts } = this 507 | if (columns) { 508 | if ((!checkboxOpts.checkField || !checkboxOpts.halfField) && columns.some(conf => conf.type === 'checkbox')) { 509 | errorLog($vxe.t('vxe.error.reqProp'), ['table.checkbox-config.checkField | table.checkbox-config.halfField']) 510 | return [] 511 | } 512 | const treeNodeColumn = columns.find(conf => conf.treeNode) 513 | if (treeNodeColumn) { 514 | let slots = treeNodeColumn.slots || {} 515 | slots.icon = renderTreeIcon 516 | treeNodeColumn.slots = slots 517 | this.treeNodeColumn = treeNodeColumn 518 | } 519 | return columns 520 | } 521 | return [] 522 | }, 523 | /** 524 | * 获取表格数据集,包含新增、删除 525 | * 不支持修改 526 | */ 527 | getRecordset () { 528 | return { 529 | insertRecords: this.getInsertRecords(), 530 | removeRecords: this.getRemoveRecords(), 531 | updateRecords: [] 532 | } 533 | }, 534 | isInsertByRow (row: RowInfo) { 535 | return !!row._X_INSERT 536 | }, 537 | getInsertRecords () { 538 | const { treeOpts } = this 539 | const insertRecords: RowInfo[] = [] 540 | XEUtils.eachTree(this.fullTreeData, row => { 541 | if (row._X_INSERT) { 542 | insertRecords.push(row) 543 | } 544 | }, treeOpts) 545 | return insertRecords 546 | }, 547 | insert (records: RowInfo | RowInfo[]) { 548 | return this.insertAt(records, null) 549 | }, 550 | /** 551 | * 支持任意层级插入与删除 552 | */ 553 | insertAt (records: any, row: number | RowInfo | null) { 554 | const { fullTreeData, treeTableData, treeOpts } = this 555 | if (!XEUtils.isArray(records)) { 556 | records = [records] 557 | } 558 | let newRecords = records.map((record: any) => this.defineField(Object.assign({ 559 | _X_LOADED: false, 560 | _X_EXPAND: false, 561 | _X_INSERT: true, 562 | _X_LEVEL: 0 563 | }, record))) 564 | if (!row) { 565 | fullTreeData.unshift(...newRecords) 566 | treeTableData.unshift(...newRecords) 567 | } else { 568 | if (row === -1) { 569 | fullTreeData.push(...newRecords) 570 | treeTableData.push(...newRecords) 571 | } else { 572 | let matchObj = XEUtils.findTree(fullTreeData, item => item === row, treeOpts) 573 | if (!matchObj || matchObj.index === -1) { 574 | throw new Error(t('vxe.error.unableInsert')) 575 | } 576 | let { items, index, nodes } = matchObj 577 | let rowIndex = treeTableData.indexOf(row) 578 | if (rowIndex > -1) { 579 | treeTableData.splice(rowIndex, 0, ...newRecords) 580 | } 581 | items.splice(index, 0, ...newRecords) 582 | newRecords.forEach((item: any) => { 583 | item._X_LEVEL = nodes.length - 1 584 | }) 585 | } 586 | } 587 | return this._loadTreeData(treeTableData).then(() => { 588 | return { 589 | row: newRecords.length ? newRecords[newRecords.length - 1] : null, 590 | rows: newRecords 591 | } 592 | }) 593 | }, 594 | /** 595 | * 获取已删除的数据 596 | */ 597 | getRemoveRecords () { 598 | return this.removeList 599 | }, 600 | removeSelecteds () { 601 | return this.removeCheckboxRow() 602 | }, 603 | /** 604 | * 删除选中数据 605 | */ 606 | removeCheckboxRow () { 607 | return this.remove(this.getCheckboxRecords()).then((params: any) => { 608 | this.clearSelection() 609 | return params 610 | }) 611 | }, 612 | remove (rows: any) { 613 | const { removeList, fullTreeData, treeOpts } = this 614 | let rest: RowInfo[] = [] 615 | if (!rows) { 616 | rows = fullTreeData 617 | } else if (!XEUtils.isArray(rows)) { 618 | rows = [rows] 619 | } 620 | rows.forEach((row: any) => { 621 | let matchObj = XEUtils.findTree(fullTreeData, item => item === row, treeOpts) 622 | if (matchObj) { 623 | const { item, items, index, parent }: any = matchObj 624 | if (!this.isInsertByRow(row)) { 625 | removeList.push(row) 626 | } 627 | if (parent) { 628 | let isExpand = this.isTreeExpandByRow(parent) 629 | if (isExpand) { 630 | this.handleCollapsing(parent) 631 | } 632 | items.splice(index, 1) 633 | if (isExpand) { 634 | this.handleExpanding(parent) 635 | } 636 | } else { 637 | this.handleCollapsing(item) 638 | items.splice(index, 1) 639 | this.treeTableData.splice(this.treeTableData.indexOf(item), 1) 640 | } 641 | rest.push(item) 642 | } 643 | }) 644 | return this._loadTreeData(this.treeTableData).then(() => { 645 | return { row: rest.length ? rest[rest.length - 1] : null, rows: rest } 646 | }) 647 | }, 648 | /** 649 | * 处理默认展开树节点 650 | */ 651 | handleDefaultTreeExpand () { 652 | let { treeConfig, treeOpts, tableFullData } = this 653 | if (treeConfig) { 654 | let { children, expandAll, expandRowKeys } = treeOpts 655 | if (expandAll) { 656 | this.setAllTreeExpand(true) 657 | } else if (expandRowKeys && this.rowId) { 658 | let rowkey = this.rowId 659 | expandRowKeys.forEach((rowid: any) => { 660 | let matchObj = XEUtils.findTree(tableFullData, item => rowid === XEUtils.get(item, rowkey), treeOpts) 661 | let rowChildren = matchObj ? matchObj.item[children] : 0 662 | if (rowChildren && rowChildren.length) { 663 | this.setTreeExpand(matchObj.item, true) 664 | } 665 | }) 666 | } 667 | } 668 | }, 669 | /** 670 | * 定义树属性 671 | */ 672 | toVirtualTree (treeData: RowInfo[]) { 673 | const { treeOpts } = this 674 | let fullTreeRowMap = this.fullTreeRowMap 675 | fullTreeRowMap.clear() 676 | XEUtils.eachTree(treeData, (item, index, items, paths, parent, nodes) => { 677 | item._X_LOADED = false 678 | item._X_EXPAND = false 679 | item._X_INSERT = false 680 | item._X_LEVEL = nodes.length - 1 681 | fullTreeRowMap.set(item, { item, index, items, paths, parent, nodes }) 682 | }, treeOpts) 683 | this.fullTreeData = treeData.slice(0) 684 | this.treeTableData = treeData.slice(0) 685 | return treeData 686 | }, 687 | /** 688 | * 展开/收起树节点 689 | */ 690 | virtualExpand (row: RowInfo, expanded: boolean) { 691 | const { treeOpts, treeNodeColumn } = this 692 | const { toggleMethod } = treeOpts 693 | const columnIndex = this.getColumnIndex(treeNodeColumn) 694 | const $columnIndex = this.getVMColumnIndex(treeNodeColumn) 695 | if (!toggleMethod || toggleMethod({ expanded, row, column: treeNodeColumn, columnIndex, $columnIndex })) { 696 | if (row._X_EXPAND !== expanded) { 697 | if (row._X_EXPAND) { 698 | this.handleCollapsing(row) 699 | } else { 700 | this.handleExpanding(row) 701 | } 702 | } 703 | } 704 | return this.treeTableData 705 | }, 706 | // 展开节点 707 | handleExpanding (row: RowInfo) { 708 | if (hasChilds(this, row)) { 709 | const { treeTableData, treeOpts } = this 710 | let childRows = row[treeOpts.children] 711 | let expandList: RowInfo[] = [] 712 | let rowIndex = treeTableData.indexOf(row) 713 | if (rowIndex === -1) { 714 | throw new Error('Expanding error') 715 | } 716 | const expandMaps: Map = new Map() 717 | XEUtils.eachTree(childRows, (item, index, obj, paths, parent, nodes) => { 718 | if (!parent || (parent._X_EXPAND && expandMaps.has(parent))) { 719 | expandMaps.set(item, 1) 720 | expandList.push(item) 721 | } 722 | }, treeOpts) 723 | row._X_EXPAND = true 724 | treeTableData.splice(rowIndex + 1, 0, ...expandList) 725 | } 726 | return this.treeTableData 727 | }, 728 | // 收起节点 729 | handleCollapsing (row: RowInfo) { 730 | if (hasChilds(this, row)) { 731 | const { treeTableData, treeOpts } = this 732 | let childRows = row[treeOpts.children] 733 | let nodeChildList: RowInfo[] = [] 734 | XEUtils.eachTree(childRows, item => { 735 | nodeChildList.push(item) 736 | }, treeOpts) 737 | row._X_EXPAND = false 738 | this.treeTableData = treeTableData.filter((item: any) => nodeChildList.indexOf(item) === -1) 739 | } 740 | return this.treeTableData 741 | }, 742 | /** 743 | * 展开/收起所有树节点 744 | */ 745 | virtualAllExpand (expanded: boolean) { 746 | const { treeOpts } = this 747 | if (expanded) { 748 | const tableList: RowInfo[] = [] 749 | XEUtils.eachTree(this.fullTreeData, row => { 750 | row._X_EXPAND = expanded 751 | tableList.push(row) 752 | }, treeOpts) 753 | this.treeTableData = tableList 754 | } else { 755 | XEUtils.eachTree(this.fullTreeData, row => { 756 | row._X_EXPAND = expanded 757 | }, treeOpts) 758 | this.treeTableData = this.fullTreeData.slice(0) 759 | } 760 | return this.treeTableData 761 | }, 762 | checkboxAllEvent (params: any) { 763 | const { checkboxOpts, treeOpts } = this 764 | const { checkField, halfField, checkStrictly } = checkboxOpts 765 | const { checked } = params 766 | if (checkField && !checkStrictly) { 767 | XEUtils.eachTree(this.fullTreeData, row => { 768 | row[checkField] = checked 769 | if (halfField) { 770 | row[halfField] = false 771 | } 772 | }, treeOpts) 773 | } 774 | this.$emit('checkbox-all', params) 775 | }, 776 | checkboxChangeEvent (params: any) { 777 | const { checkboxOpts, treeOpts } = this 778 | const { checkField, halfField, checkStrictly } = checkboxOpts 779 | const { row, checked } = params 780 | if (checkField && !checkStrictly) { 781 | XEUtils.eachTree([row], row => { 782 | row[checkField] = checked 783 | if (halfField) { 784 | row[halfField] = false 785 | } 786 | }, treeOpts) 787 | this.checkParentNodeSelection(row) 788 | } 789 | this.$emit('checkbox-change', params) 790 | }, 791 | checkParentNodeSelection (row: RowInfo) { 792 | const { checkboxOpts, treeOpts } = this 793 | const { children } = treeOpts 794 | const { checkField, halfField, checkStrictly } = checkboxOpts 795 | const matchObj = XEUtils.findTree(this.fullTreeData, item => item === row, treeOpts) 796 | if (matchObj && checkField && !checkStrictly) { 797 | const parentRow: RowInfo = matchObj.parent 798 | if (parentRow) { 799 | const isAll = parentRow[children].every((item: RowInfo) => item[checkField]) 800 | if (halfField && !isAll) { 801 | parentRow[halfField] = parentRow[children].some((item: RowInfo) => item[checkField] || item[halfField]) 802 | } 803 | parentRow[checkField] = isAll 804 | this.checkParentNodeSelection(parentRow) 805 | } else { 806 | this.$refs.xTable.checkSelectionStatus() 807 | } 808 | } 809 | }, 810 | getCheckboxRecords () { 811 | const { checkboxOpts, treeOpts } = this 812 | const { checkField } = checkboxOpts 813 | if (checkField) { 814 | const records: RowInfo[] = [] 815 | XEUtils.eachTree(this.fullTreeData, row => { 816 | if (row[checkField]) { 817 | records.push(row) 818 | } 819 | }, treeOpts) 820 | return records 821 | } 822 | return this.$refs.xTable.getCheckboxRecords() 823 | }, 824 | getCheckboxIndeterminateRecords () { 825 | const { checkboxOpts, treeOpts } = this 826 | const { halfField } = checkboxOpts 827 | if (halfField) { 828 | const records: RowInfo[] = [] 829 | XEUtils.eachTree(this.fullTreeData, row => { 830 | if (row[halfField]) { 831 | records.push(row) 832 | } 833 | }, treeOpts) 834 | return records 835 | } 836 | return this.$refs.xTable.getCheckboxIndeterminateRecords() 837 | } 838 | } 839 | } 840 | 841 | vxetable.Vue.component(options.name, options) 842 | } 843 | 844 | /** 845 | * 基于 vxe-table 表格的增强插件,实现简单的虚拟树表格 846 | */ 847 | export const VXETablePluginVirtualTree = { 848 | install (vxetable: typeof VXETable) { 849 | registerComponent(vxetable) 850 | } 851 | } 852 | 853 | if (typeof window !== 'undefined' && window.VXETable && window.VXETable.use) { 854 | window.VXETable.use(VXETablePluginVirtualTree) 855 | } 856 | 857 | export default VXETablePluginVirtualTree 858 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vxe-table-plugin-virtual-tree", 3 | "version": "0.5.5", 4 | "description": "基于 vxe-table 的表格插件,实现简单的虚拟树表格(属于内测阶段)", 5 | "scripts": { 6 | "lib": "gulp build" 7 | }, 8 | "files": [ 9 | "dist", 10 | "*.ts", 11 | "*.d.ts", 12 | "style.scss" 13 | ], 14 | "main": "dist/index.common.js", 15 | "unpkg": "dist/index.umd.js", 16 | "jsdelivr": "dist/index.umd.js", 17 | "style": "dist/style.css", 18 | "typings": "index.ts", 19 | "devDependencies": { 20 | "@babel/core": "^7.12.3", 21 | "@babel/plugin-transform-runtime": "^7.12.1", 22 | "@babel/preset-env": "^7.12.1", 23 | "@babel/runtime": "^7.12.5", 24 | "@typescript-eslint/eslint-plugin": "^4.6.1", 25 | "@typescript-eslint/parser": "^4.6.1", 26 | "del": "^6.0.0", 27 | "eslint": "^7.13.0", 28 | "eslint-config-prettier": "^6.15.0", 29 | "eslint-config-standard": "^16.0.1", 30 | "eslint-friendly-formatter": "^4.0.1", 31 | "eslint-plugin-import": "^2.22.1", 32 | "eslint-plugin-node": "^11.1.0", 33 | "eslint-plugin-prettier": "^3.1.4", 34 | "eslint-plugin-promise": "^4.2.1", 35 | "eslint-plugin-standard": "^4.0.2", 36 | "eslint-plugin-typescript": "^0.14.0", 37 | "gulp": "^4.0.2", 38 | "gulp-autoprefixer": "^7.0.1", 39 | "gulp-babel": "^8.0.0", 40 | "gulp-clean-css": "^4.3.0", 41 | "gulp-concat": "^2.6.1", 42 | "gulp-rename": "^2.0.0", 43 | "gulp-replace": "^1.0.0", 44 | "gulp-sass": "^4.1.0", 45 | "gulp-sourcemaps": "^2.6.5", 46 | "gulp-typescript": "^5.0.1", 47 | "gulp-uglify": "^3.0.2", 48 | "markdown-doctest": "^1.1.0", 49 | "prettier": "^2.1.2", 50 | "typescript": "^4.0.5", 51 | "vue": "^3.0.2", 52 | "vxe-table": "^4.0.3", 53 | "xe-utils": "^3.3.0" 54 | }, 55 | "peerDependencies": { 56 | "vxe-table": "^4.0.0" 57 | }, 58 | "repository": { 59 | "type": "git", 60 | "url": "git+https://github.com/x-extends/vxe-table-plugin-virtual-tree.git" 61 | }, 62 | "keywords": [ 63 | "vxe-table-plugin-virtual-tree" 64 | ], 65 | "author": { 66 | "name": "Xu Liangzhan", 67 | "email": "xu_liangzhan@163.com" 68 | }, 69 | "license": "MIT", 70 | "bugs": { 71 | "url": "https://github.com/x-extends/vxe-table-plugin-virtual-tree/issues" 72 | }, 73 | "homepage": "https://github.com/x-extends/vxe-table-plugin-virtual-tree#readme" 74 | } 75 | -------------------------------------------------------------------------------- /style.scss: -------------------------------------------------------------------------------- 1 | /*virtual-tree*/ 2 | .vxe-virtual-tree { 3 | &.has--tree-line { 4 | .vxe-body--row { 5 | &:first-child { 6 | .vxe-tree--line { 7 | border-width: 0 0 1px 0; 8 | } 9 | } 10 | } 11 | .vxe-body--column { 12 | border-bottom-color: transparent; 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "index.ts" 4 | ], 5 | "compilerOptions": { 6 | "strict": true, 7 | "moduleResolution": "node", 8 | "noImplicitAny": true, 9 | "target": "esnext", 10 | "lib": [ 11 | "esnext", 12 | "dom", 13 | "dom.iterable" 14 | ] 15 | } 16 | } --------------------------------------------------------------------------------