├── .browserslistrc ├── .github └── workflows │ └── main.yml ├── .gitignore ├── .npmrc ├── README-CN.md ├── README.md ├── babel.config.js ├── package-lock.json ├── package.json ├── public ├── favicon.ico └── index.html ├── server.js ├── src ├── App.vue ├── ant-use.ts ├── assets │ └── styles │ │ ├── highlight.github.css │ │ ├── iconfont.css │ │ ├── index.scss │ │ ├── reset.scss │ │ └── variable.scss ├── components │ ├── VirtualCheckbox │ │ ├── demo.vue │ │ ├── index.scss │ │ └── index.tsx │ ├── VirtualList │ │ ├── demo.vue │ │ ├── index.tsx │ │ └── types.ts │ ├── VirtualTree │ │ ├── demo.vue │ │ ├── index.scss │ │ ├── index.tsx │ │ ├── node.tsx │ │ ├── render.tsx │ │ ├── service.ts │ │ ├── types.ts │ │ └── uses备份.ts │ ├── index.ts │ ├── selections.ts │ └── tsconfig.json ├── doc │ ├── AsyncDataDemo.vue │ ├── BaseDemo.vue │ ├── CheckboxDemo.vue │ ├── CustomIconDemo.vue │ ├── CustomNodeDemo.vue │ ├── DemoBox.vue │ ├── HighlightCodes.json │ ├── index.vue │ ├── principle.png │ ├── tableData.ts │ ├── temp.vue │ └── uses │ │ └── index.ts ├── main.ts └── shims-vue.d.ts ├── tsconfig.json └── vue.config.js /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not dead 4 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: GitHub Actions Build and Deploy 2 | on: 3 | push: 4 | tags: 5 | - 'v*' 6 | jobs: 7 | build-and-deploy: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@main # If you're using actions/checkout@v2 you must set persist-credentials to false in most cases for the deployment to work correctly. 12 | with: 13 | persist-credentials: false 14 | - name: Install and Build 15 | run: | 16 | npm install 17 | npm run-script build 18 | - name: Deploy 19 | uses: JamesIves/github-pages-deploy-action@3.7.1 20 | with: 21 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 22 | BRANCH: gh-pages 23 | FOLDER: dist 24 | CLEAN: true 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | dist 4 | lib 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | # registry=https://registry.npm.taobao.org 2 | # sass_binary_site=http://cdn.npm.taobao.org/dist/node-sass 3 | -------------------------------------------------------------------------------- /README-CN.md: -------------------------------------------------------------------------------- 1 | # vue-virtual-tree 2 | 3 | > **该库已经废弃,请使用重构后的[virtual-tree](https://github.com/lycHub/ysx-library/blob/master/projects/VirtualTree/README.md)** 4 | 5 | 6 | ### 基于vue3封装的,大数据量专用的tree组件,如果数据量不大,用本组件有些浪费了 7 | 8 | [English](README.md) & 简体中文 9 | 10 | ## [文档 & 示例](https://lychub.github.io/vue-virtual-tree) 11 | ### [在线demo](https://stackblitz.com/edit/vue-virtual-tree-demos?file=src/App.vue) 12 | ### [在线demo v4](https://stackblitz.com/edit/vue-virtual-tree-demos-bvicgw?file=src/App.vue) 13 | 14 | ## 基本使用 15 | 16 | ``` 17 | npm i vue-virtual-tree 18 | ``` 19 | 20 | 全局注册, 但这会丢失类型,如果你用了typescript, 不推荐这种方式 21 | ``` js 22 | import { createApp } from 'vue'; 23 | import VirTree from 'vue-virtual-tree'; 24 | 25 | createApp(App).use(VirTree).mount('#app'); 26 | 27 | In components: 28 | 29 | ``` 30 | 31 | 32 | 局部注册, 可以获得完整的类型 33 | ``` js 34 | 39 | 40 | 55 | 56 | ``` 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue-virtual-tree 2 | 3 | > **This library is deprecated, please use the refactored [virtual-tree](https://github.com/lycHub/ysx-library/blob/master/projects/VirtualTree/README.md)** 4 | 5 | ### Based on the tree component encapsulated by vue3 and dedicated to large data volume, if the data volume is not large, using this component is a bit wasteful 6 | 7 | English & [简体中文](README-CN.md) 8 | 9 | ## [Docs & Demo](https://lychub.github.io/vue-virtual-tree) 10 | ### [online demo](https://stackblitz.com/edit/vue-virtual-tree-demos?file=src/App.vue) 11 | ### [online demo v4](https://stackblitz.com/edit/vue-virtual-tree-demos-bvicgw?file=src/App.vue) 12 | 13 | 14 | ## How to use 15 | 16 | ``` 17 | npm i vue-virtual-tree 18 | ``` 19 | 20 | Global registration, but this will lose the type, if you use typescript, this method is not recommended 21 | ``` js 22 | import { createApp } from 'vue'; 23 | import VirTree from 'vue-virtual-tree'; 24 | 25 | createApp(App).use(VirTree).mount('#app'); 26 | 27 | In components: 28 | 29 | ``` 30 | 31 | 32 | Partial registration, you can get a complete type 33 | ``` js 34 | 39 | 40 | 55 | 56 | ``` 57 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ], 5 | plugins: ['@vue/babel-plugin-jsx'] 6 | } 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-virtual-tree", 3 | "version": "4.0.7", 4 | "description": "Tree component for large amount of data, base on Vue3", 5 | "scripts": { 6 | "start": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "build:lib": "vue-cli-service build --dest lib --target lib ./src/components/index.ts && npm run build:types", 9 | "build:types": "tsc --project src/components/tsconfig.json" 10 | }, 11 | "main": "./lib/vue-virtual-tree.common.js", 12 | "typings": "lib/index.d.ts", 13 | "files": ["lib"], 14 | "dependencies": { 15 | "core-js": "^3.6.5", 16 | "lodash-es": "^4.17.21" 17 | }, 18 | "peerDependencies": { 19 | "vue": "^3.0.0" 20 | }, 21 | "devDependencies": { 22 | "@types/lodash-es": "^4.17.4", 23 | "@vue/babel-plugin-jsx": "^1.0.3", 24 | "@vue/cli-plugin-babel": "~4.5.0", 25 | "@vue/cli-plugin-typescript": "~4.5.0", 26 | "@vue/cli-service": "~4.5.0", 27 | "@vue/compiler-sfc": "^3.0.0", 28 | "ant-design-vue": "^2.1.2", 29 | "del": "^6.0.0", 30 | "express": "^4.17.1", 31 | "sass": "^1.26.5", 32 | "sass-loader": "^8.0.2", 33 | "style-resources-loader": "^1.4.1", 34 | "ts-import-plugin": "^1.6.7", 35 | "typescript": "~4.1.5", 36 | "vue-cli-plugin-style-resources-loader": "~0.1.5" 37 | }, 38 | "keywords": [ 39 | "tree component", 40 | "vue-next" 41 | ], 42 | "repository": { 43 | "type": "git", 44 | "url": "https://github.com/lycHub/vue-virtual-tree.git" 45 | }, 46 | "homepage": "https://lychub.github.io/vue-virtual-tree", 47 | "license": "ISC" 48 | } 49 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lycHub/vue-virtual-tree/568d45ba4e179af3827193f67c6f05f910a1f172/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const app = express(); 3 | const PORT = 3333; 4 | app.use(express.static('dist')); 5 | app.listen(PORT, function(err) { 6 | if (err) { 7 | console.log('err :', err); 8 | } else { 9 | console.log('Listen at http://localhost:' + PORT); 10 | } 11 | }); 12 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 33 | 45 | 77 | -------------------------------------------------------------------------------- /src/ant-use.ts: -------------------------------------------------------------------------------- 1 | import { App } from 'vue'; 2 | import { Alert, Button, Layout, Menu, Row, Col, Tooltip, Typography, Anchor, Card, Input, Table } from "ant-design-vue"; 3 | 4 | const components = [ 5 | Alert, 6 | Button, 7 | Layout, 8 | Menu, 9 | Row, 10 | Col, 11 | Tooltip, 12 | Typography, 13 | Anchor, 14 | Card, 15 | Input, 16 | Table 17 | ] 18 | export default function(app: App) { 19 | components.forEach(item => app.use(item)); 20 | } 21 | -------------------------------------------------------------------------------- /src/assets/styles/highlight.github.css: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | github.com style (c) Vasily Polovnyov 4 | 5 | */ 6 | 7 | .hljs { 8 | display: block; 9 | overflow-x: auto; 10 | padding: 0.5em; 11 | color: #333; 12 | background: #f8f8f8; 13 | } 14 | 15 | .hljs-comment, 16 | .hljs-quote { 17 | color: #998; 18 | font-style: italic; 19 | } 20 | 21 | .hljs-keyword, 22 | .hljs-selector-tag, 23 | .hljs-subst { 24 | color: #333; 25 | font-weight: bold; 26 | } 27 | 28 | .hljs-number, 29 | .hljs-literal, 30 | .hljs-variable, 31 | .hljs-template-variable, 32 | .hljs-tag .hljs-attr { 33 | color: #008080; 34 | } 35 | 36 | .hljs-string, 37 | .hljs-doctag { 38 | color: #d14; 39 | } 40 | 41 | .hljs-title, 42 | .hljs-section, 43 | .hljs-selector-id { 44 | color: #900; 45 | font-weight: bold; 46 | } 47 | 48 | .hljs-subst { 49 | font-weight: normal; 50 | } 51 | 52 | .hljs-type, 53 | .hljs-class .hljs-title { 54 | color: #458; 55 | font-weight: bold; 56 | } 57 | 58 | .hljs-tag, 59 | .hljs-name, 60 | .hljs-attribute { 61 | color: #000080; 62 | font-weight: normal; 63 | } 64 | 65 | .hljs-regexp, 66 | .hljs-link { 67 | color: #009926; 68 | } 69 | 70 | .hljs-symbol, 71 | .hljs-bullet { 72 | color: #990073; 73 | } 74 | 75 | .hljs-built_in, 76 | .hljs-builtin-name { 77 | color: #0086b3; 78 | } 79 | 80 | .hljs-meta { 81 | color: #999; 82 | font-weight: bold; 83 | } 84 | 85 | .hljs-deletion { 86 | background: #fdd; 87 | } 88 | 89 | .hljs-addition { 90 | background: #dfd; 91 | } 92 | 93 | .hljs-emphasis { 94 | font-style: italic; 95 | } 96 | 97 | .hljs-strong { 98 | font-weight: bold; 99 | } 100 | -------------------------------------------------------------------------------- /src/assets/styles/iconfont.css: -------------------------------------------------------------------------------- 1 | @font-face {font-family: "iconfont"; 2 | src: url('//at.alicdn.com/t/font_2449547_gb7qdqvmbuu.eot?t=1617200330813'); /* IE9 */ 3 | src: url('//at.alicdn.com/t/font_2449547_gb7qdqvmbuu.eot?t=1617200330813#iefix') format('embedded-opentype'), /* IE6-IE8 */ 4 | url('data:application/x-font-woff2;charset=utf-8;base64,d09GMgABAAAAAAVQAAsAAAAAChQAAAUEAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHEIGVgCDSAqHBIVaATYCJAMYCw4ABCAFhG0HWRuRCMg+QCa3noPICIOagRA4mxaSBMvtvq/r3SIenvb7du7MvFlRQSNVLJkmk7QJ80qySiS0rViikchk8RD/IJsaNqe/t36hU3MGgVKjp7mIGruJOMadbzTbYKOvq51AC4TZdCzfgoSVpVIE+z+3SJPrwfZ/jqVeJlj+azuXyqRKund0fHS3DWhAUUFFw9l4E5uY7gGe4cIE3k2g0yw/2L6MvDLgpBAOCsQax5LAyeVSYpih3VpDzizihUZ7epbOgef678dvCieSJhOOPHotnQZJ37AXj20HtZ46oyKoywvgdh4ZWwGFeBgaus3WLG1lq/PzF+zsB3Rrl5Rv8azz2dbnni8G/B+XToiFblHdp394SSMTFZm6EXuw3PANk410M44OBc1+KCXx3VNKw48BZrNuqyXu0UlMAxAXgEBZ9mpZloSotWyPSGzfPtCZYth4GpIJLJMImfS6jL7TZTJvPmL29r0L1WofuccajxVnPHCLFR54mNs06TK3MCXu5jnTE/Ur1W0kkpXBmLtsubs79e72nq6qwyI9FWJx7cGDRzWs7yr51XIlOqRSWS6KzNq3b5ebVM5XwiNmSJetm7+7Qu4hl89Rz9mgVqu1ekwzGIy91lBAM1NBwnMX7NmmVE6zK2dhvUo2CFOpZqPcsM/2JsPqOutl0X1Fccw1gTDoOmFsy9MHi3TZtDFrBisqK2YMXjtWVj59EDlGLLggii2pLY/Lz1/3PGPWshXyJWKC+KDs0LXr1/OvP648Wjlz5fLZ0msQKFtmHYbW6YOkdEJCnn+IyDiH27tO+mn6z86veFnbp1tUHxD0iskI79e2Y8fLM2J0R56Mu9dV2zAw5/QN8ekDuTDkyBFU1fhj85it9tJQitzUuvUFAN7cbA/ldmM/8O7Q7+DT9s93zHcN2pXfbs/15hRTUgfg3DvsWhVfQE03LdE/zZpyh1vKfunIR5KCmifl4icepHa9c//BkWvm0b1zcvDW0xjWsVqZar3fv/pox45Dnw8xT9tTV1KWkaX200gwWxcb1qPpxpuO30RA618JRKy/zsnalXtBYk2dmrvrgpzHXNv4xK9Zs9dttPbcOOFzz2lnu5xds2afC/hPPTwj/lM+z33a/2tE+o1s0uVY2qb4boG+vjeSmh6hEmf3KstwYmDHESnddNYSHJ+2p22G2g9t0XQoCUn7Z0G+5Cg/8fWdnZQ0SjlKSc8Ga2jb/D9to7U8JWv1/Z/PagBIb5N4Gu7ly4JVgxvbxn1t79Bc+jpm+RnqaqWA/JRzySy1+qRTp5wqX63gruU9We4TqNZ3VCGhU5zjVIsNuleWZSS1yVltGbEhaTMDWbtlZMFuhUaXbVC12wmdtsg8v8sQga0oHdhsOEDotxGSXh8h63ceWbB3oTHqLVT9YQudTkfHK3ZZDQ5PFSCJIE009yY4ijczkhfhRmMJZLsbScGp1VkVULByWSI4IMhbzYRmKJRxjLUHG4IQQzACbyIy4H7QaOQJi8DrIYUCdAhZYgIDmbAPCqB4E8DtJYBICEQjNOuNwKHwzBh1MYL7fr4ExOrOiCTETPzIVoAEVtzmCcECBKVAZprMqSa2ZXurHlghEITh/RgCngkhAwOQUSzxCJbw3fQgChJAl6NgESOQZjFpTQH1W0xLeQN0El5SI0WOEk3UaFMu8zSUUN2tiDf5z8PNHfvoSLOB5ByNPElzZtYhvZeFNNMAAA==') format('woff2'), 5 | url('//at.alicdn.com/t/font_2449547_gb7qdqvmbuu.woff?t=1617200330813') format('woff'), 6 | url('//at.alicdn.com/t/font_2449547_gb7qdqvmbuu.ttf?t=1617200330813') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+ */ 7 | url('//at.alicdn.com/t/font_2449547_gb7qdqvmbuu.svg?t=1617200330813#iconfont') format('svg'); /* iOS 4.1- */ 8 | } 9 | 10 | .iconfont { 11 | font-family: "iconfont" !important; 12 | font-size: 16px; 13 | font-style: normal; 14 | -webkit-font-smoothing: antialiased; 15 | -moz-osx-font-smoothing: grayscale; 16 | } 17 | 18 | .iconcode:before { 19 | content: "\e601"; 20 | } 21 | 22 | .iconcustom-icon:before { 23 | content: "\e62c"; 24 | } 25 | 26 | .iconzhankai:before { 27 | content: "\e87e"; 28 | } 29 | 30 | .iconloading:before { 31 | content: "\e60a"; 32 | } 33 | 34 | .iconExpand:before { 35 | content: "\e727"; 36 | } 37 | -------------------------------------------------------------------------------- /src/assets/styles/index.scss: -------------------------------------------------------------------------------- 1 | @import "reset"; 2 | @import "highlight.github.css"; 3 | 4 | .table-row-abandoned { 5 | text-decoration: line-through; 6 | } 7 | -------------------------------------------------------------------------------- /src/assets/styles/reset.scss: -------------------------------------------------------------------------------- 1 | @import "variable"; 2 | * { 3 | padding: 0; 4 | margin: 0; 5 | } 6 | 7 | 8 | html, body { 9 | width: 100%; 10 | height: 100%; 11 | } 12 | 13 | body { 14 | color: $text-color; 15 | font-size: $font-size-base; 16 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; 17 | font-variant: tabular-nums; 18 | line-height: 1.5; 19 | background-color: $white-color; 20 | font-feature-settings: "tnum"; 21 | } 22 | -------------------------------------------------------------------------------- /src/assets/styles/variable.scss: -------------------------------------------------------------------------------- 1 | $vir-pre: vir-; 2 | $white-color: #fff; 3 | $border-color: #dcdee2; 4 | $dash-border-color: #f0f0f0; 5 | $primary-color: #2d8cf0; 6 | $assist-color: #bae7ff; 7 | $disable-color: #c5c8ce; 8 | $text-color: #333; 9 | $gray-color-tree: #e7eaef; 10 | 11 | $font-size-base: 14px; 12 | $font-size-mid: $font-size-base + 2; 13 | $font-size-large: $font-size-base + 4; 14 | $font-size-huge: $font-size-base + 10; 15 | -------------------------------------------------------------------------------- /src/components/VirtualCheckbox/demo.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 26 | -------------------------------------------------------------------------------- /src/components/VirtualCheckbox/index.scss: -------------------------------------------------------------------------------- 1 | .#{$vir-pre}checkbox { 2 | display: inline-block; 3 | cursor: pointer; 4 | .inner { 5 | display: inline-block; 6 | vertical-align: text-bottom; 7 | position: relative; 8 | top: 0; 9 | left: 0; 10 | width: 16px; 11 | height: 16px; 12 | direction: ltr; 13 | background-color: $white-color; 14 | border: 1px solid $border-color; 15 | border-radius: 2px; 16 | border-collapse: initial; 17 | &:after { 18 | position: absolute; 19 | top: 50%; 20 | left: 50%; 21 | width: 4px; 22 | height: 8px; 23 | margin-left: -2px; 24 | margin-top: -5px; 25 | border: 2px solid $white-color; 26 | border-top: 0; 27 | border-left: 0; 28 | content: " "; 29 | opacity: 0; 30 | transition: all .2s ease-in-out; 31 | } 32 | } 33 | .content { 34 | display: inline-block; 35 | } 36 | &.half-checked { 37 | .inner:after { 38 | top: 50%; 39 | left: 50%; 40 | width: 10px; 41 | height: 10px; 42 | background-color: $primary-color; 43 | border: none; 44 | margin: 0; 45 | transform: translate(-50%,-50%); 46 | opacity: 1; 47 | content: " "; 48 | } 49 | } 50 | &.checked { 51 | .inner { 52 | border-color: $primary-color; 53 | background-color: $primary-color; 54 | &:after { 55 | transform: rotate(45deg); 56 | opacity: 1; 57 | } 58 | } 59 | } 60 | &.disabled { 61 | color: $disable-color; 62 | cursor: not-allowed; 63 | .inner { 64 | border-color: $disable-color; 65 | background-color: $disable-color; 66 | &:after { 67 | //background-color: $disable-color; 68 | } 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/components/VirtualCheckbox/index.tsx: -------------------------------------------------------------------------------- 1 | import {defineComponent, computed} from 'vue'; 2 | import './index.scss'; 3 | 4 | export default defineComponent({ 5 | name: 'VirCheckbox', 6 | props: { 7 | modelValue: { 8 | type: Boolean, 9 | default: false 10 | }, 11 | disabled: { 12 | type: Boolean, 13 | default: false 14 | }, 15 | halfChecked: { 16 | type: Boolean, 17 | default: false 18 | } 19 | }, 20 | emits: ['update:modelValue', 'change'], 21 | setup(props, { emit, slots }) { 22 | const rootCls = computed(() => { 23 | let result = 'vir-checkbox'; 24 | if (props.modelValue) { 25 | result += ' checked'; 26 | } else if (props.halfChecked) { 27 | result += ' half-checked'; 28 | } 29 | if (props.disabled) { 30 | result += ' disabled'; 31 | } 32 | return result; 33 | }); 34 | const handleClick = (event: MouseEvent) => { 35 | event.stopPropagation(); 36 | if (!props.disabled) { 37 | emit('update:modelValue', !props.modelValue); 38 | emit('change', !props.modelValue); 39 | } 40 | } 41 | return () => { 42 | return ( 43 |
46 |
47 |
{ slots.default && slots.default() }
48 |
49 | ); 50 | } 51 | } 52 | }); 53 | -------------------------------------------------------------------------------- /src/components/VirtualList/demo.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 55 | 66 | -------------------------------------------------------------------------------- /src/components/VirtualList/index.tsx: -------------------------------------------------------------------------------- 1 | import {defineComponent, computed, onMounted, ref, markRaw, watch} from 'vue'; 2 | import {ZoneInfo} from "./types"; 3 | 4 | export default defineComponent({ 5 | name: 'VirtualList', 6 | props: { 7 | list: { 8 | type: Array, 9 | default: () => [] 10 | }, 11 | customForOf: { 12 | type: Boolean, 13 | default: false 14 | }, 15 | size: { 16 | type: Number, 17 | required: true 18 | }, 19 | remain: { 20 | type: Number, 21 | required: true 22 | }, 23 | start: { 24 | type: Number, 25 | default: 0 26 | }, 27 | offset: { // 设置scrollTop 28 | type: Number, 29 | default: 0 30 | }, 31 | additional: { // 额外渲染多少个节点? 32 | type: Number, 33 | default: 0 34 | }, 35 | dataKey: { 36 | type: String, 37 | default: 'id' 38 | } 39 | }, 40 | emits: ['update:modelValue', 'range'], 41 | setup(props, { emit, slots }) { 42 | const root = ref(null); 43 | const base = markRaw({ 44 | start: 0, 45 | end: 0, 46 | scrollTop: 0, 47 | paddingTop: 0, 48 | paddingBottom: 0 49 | }) 50 | const visibleList = ref([]); 51 | const keeps = computed(() => props.remain + (props.additional || props.remain)); 52 | const maxHeight = computed(() => props.size * props.remain); 53 | 54 | const getEndIndex = (start: number): number => { 55 | const end = start + keeps.value - 1; 56 | return props.list.length ? Math.min(props.list.length - 1, end) : end; 57 | } 58 | 59 | const getZone = (startIndex: number): ZoneInfo => { 60 | let start = Math.max(0, startIndex); 61 | const remainCount = props.list.length - keeps.value; 62 | const isLastZone = start >= remainCount; 63 | if (isLastZone) { 64 | start = Math.max(0, remainCount); 65 | } 66 | return { 67 | start, 68 | end: getEndIndex(start), 69 | isLastZone 70 | }; 71 | } 72 | 73 | const updateContainer = () => { 74 | const total = props.list.length; 75 | const needPadding = total > keeps.value; 76 | const paddingTop = props.size * (needPadding ? base.start : 0); 77 | let paddingBottom = props.size * (needPadding ? total - keeps.value : 0) - paddingTop; 78 | if (paddingBottom < props.size) { 79 | paddingBottom = 0; 80 | } 81 | base.paddingTop = paddingTop; 82 | base.paddingBottom = paddingBottom; 83 | } 84 | const filterNodes = (): any[] => { 85 | if (props.list.length) { 86 | const nodes = []; 87 | for (let a = base.start; a <= base.end; a++) { 88 | nodes.push(props.list[a]); 89 | } 90 | return nodes; 91 | } 92 | return []; 93 | } 94 | 95 | const updateVisibleList = () => { 96 | if (props.customForOf) { 97 | emit('range', { 98 | start: base.start, 99 | end: base.end 100 | }) 101 | } else { 102 | visibleList.value = filterNodes(); 103 | } 104 | } 105 | const updateZone = (offset: number, forceUpdate = false) => { 106 | // console.log('updateZone', offset, forceUpdate); 107 | const overs = Math.floor(offset / props.size); 108 | const zone = getZone(overs); 109 | const additional = props.additional || props.remain; 110 | let shouldRefresh = false; 111 | if (forceUpdate) { 112 | shouldRefresh = true; 113 | } else { 114 | if (overs < base.start) { // 向上滚 115 | shouldRefresh = true; 116 | } else { 117 | if (zone.isLastZone) { 118 | if ((base.start !== zone.start) || (base.end !== zone.end)) { 119 | shouldRefresh = true; 120 | } 121 | } else { 122 | shouldRefresh = overs >= base.start + additional; 123 | } 124 | } 125 | } 126 | if (shouldRefresh) { 127 | base.start = zone.start; 128 | base.end = zone.end; 129 | updateContainer(); 130 | updateVisibleList(); 131 | } 132 | } 133 | const refresh = (init = false) => { 134 | if (init) { 135 | base.start = props.list.length > base.start + keeps.value ? props.start : 0; 136 | } else { 137 | base.start = 0; 138 | } 139 | base.end = getEndIndex(base.start); 140 | updateContainer(); 141 | updateVisibleList(); 142 | } 143 | const onScroll = () => { 144 | if (props.list.length > keeps.value) { 145 | updateZone(root.value!.scrollTop); 146 | } else { 147 | refresh(); 148 | } 149 | } 150 | 151 | const setScrollTop = (scrollTop: number) => { 152 | root.value!.scrollTop = scrollTop; 153 | } 154 | 155 | watch(() => props.list, (newVal: any[]) => { 156 | if (props.list.length > keeps.value) { 157 | updateZone(root.value!.scrollTop,true); 158 | } else { 159 | refresh(); 160 | } 161 | }, { 162 | deep: true 163 | }); 164 | onMounted(() => { 165 | if (props.list.length) { 166 | refresh(true); 167 | } 168 | if (props.start) { 169 | const start = getZone(props.start).start; 170 | setScrollTop(start * props.size); 171 | } else if (props.offset) { 172 | setScrollTop(props.offset); 173 | } 174 | }); 175 | const boxContent = () => { 176 | if (props.customForOf) { 177 | return slots.default!(); 178 | } 179 | return visibleList.value.map((item, index) => { 180 | return
{ slots.default!({ item, index: base.start + index }) }
; 181 | }); 182 | } 183 | return () => { 184 | return ( 185 |
190 |
193 | { boxContent() } 194 |
195 |
196 | ); 197 | } 198 | } 199 | }); 200 | -------------------------------------------------------------------------------- /src/components/VirtualList/types.ts: -------------------------------------------------------------------------------- 1 | interface Range { 2 | start: number; 3 | end: number; 4 | } 5 | 6 | interface ZoneInfo extends Range { 7 | isLastZone: boolean; 8 | } 9 | 10 | export { Range, ZoneInfo }; 11 | -------------------------------------------------------------------------------- /src/components/VirtualTree/demo.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 114 | -------------------------------------------------------------------------------- /src/components/VirtualTree/index.scss: -------------------------------------------------------------------------------- 1 | @import "../../assets/styles/iconfont.css"; 2 | 3 | .#{$vir-pre}tree { 4 | position: relative; 5 | display: inline-block; 6 | width: 100%; 7 | user-select: none; 8 | &-wrap { 9 | .#{$vir-pre}tree-node { 10 | padding: 4px 0; 11 | line-height: normal; 12 | font-size: $font-size-base; 13 | cursor: pointer; 14 | transition: all .2s ease-in-out; 15 | &:hover { 16 | .node-content .node-title { 17 | color: $primary-color; 18 | } 19 | background-color: $gray-color-tree; 20 | } 21 | .node-arrow { 22 | display: inline-block; 23 | margin-right: 4px; 24 | cursor: pointer; 25 | min-width: 16px; 26 | .iconfont { 27 | display: block; 28 | } 29 | &.expanded { 30 | .iconfont { 31 | transform: rotate(90deg); 32 | } 33 | } 34 | .ico-loading { 35 | animation: roundLoading 1s linear infinite; 36 | } 37 | } 38 | .node-content { 39 | display: inline-block; 40 | vertical-align: top; 41 | .node-title { 42 | padding: 0 6px; 43 | vertical-align: top; 44 | color: #515a6e; 45 | white-space: nowrap; 46 | transition: background-color .2s; 47 | &.selected { 48 | background-color: $assist-color; 49 | } 50 | &.disabled { 51 | cursor: not-allowed; 52 | color: $disable-color; 53 | } 54 | } 55 | } 56 | 57 | } 58 | .node-selected .node-title { 59 | background-color: #d5e8fc; 60 | } 61 | /*.#{$vir-pre}children { 62 | padding-left: 18px; 63 | }*/ 64 | } 65 | 66 | @keyframes roundLoading { 67 | from { 68 | transform: rotate(0); 69 | } 70 | to { 71 | transform: rotate(360deg); 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/components/VirtualTree/index.tsx: -------------------------------------------------------------------------------- 1 | import {defineComponent, watch, ref, shallowRef, PropType, h} from 'vue'; 2 | import { cloneDeep, uniq } from 'lodash-es'; 3 | import {NodeKey, TreeNodeInstance, TreeNodeOptions, TypeWithNull, TypeWithUndefined} from "./types"; 4 | 5 | import VirTreeNode from './node'; 6 | import VirtualList from '../VirtualList'; 7 | import './index.scss'; 8 | import {TreeService} from "./service"; 9 | 10 | export default defineComponent({ 11 | name: 'VirTree', 12 | props: { 13 | source: { 14 | type: Array as PropType, 15 | default: () => [] 16 | }, 17 | defaultExpandedKeys: { 18 | type: Array as PropType, 19 | default: () => [] 20 | }, 21 | defaultSelectedKey: { 22 | type: [String, Number], 23 | default: '' 24 | }, 25 | defaultCheckedKeys: { 26 | type: Array as PropType, 27 | default: () => [] 28 | }, 29 | defaultDisabledKeys: { 30 | type: Array as PropType, 31 | default: () => [] 32 | }, 33 | showCheckbox: { 34 | type: Boolean, 35 | default: false 36 | }, 37 | checkStrictly: { 38 | type: Boolean, 39 | default: false 40 | }, 41 | size: { 42 | type: Number, 43 | default: 27 44 | }, 45 | remain: { 46 | type: Number, 47 | default: 8 48 | }, 49 | loadData: Function as PropType<(node: TreeNodeOptions, callback: (children: TreeNodeOptions[]) => void) => void>, 50 | render: Function 51 | }, 52 | emits: ['selectChange', 'checkChange', 'toggleExpand'], 53 | setup: function (props, {emit, slots, expose}) { 54 | const loading = shallowRef(false); 55 | const flatList = ref[]>([]); 56 | 57 | const service = new TreeService(); 58 | 59 | watch(() => props.source, newVal => { 60 | flatList.value = service.flattenTree(newVal, props.defaultSelectedKey, props.defaultCheckedKeys, props.defaultExpandedKeys, props.defaultDisabledKeys, props.checkStrictly); 61 | // console.log('flatList :>> ', flatList.value); 62 | }, {immediate: true}); 63 | 64 | watch(() => props.defaultExpandedKeys, newVal => { 65 | service.resetDefaultExpandedKeys(newVal); 66 | service.expandedKeys.value.clear(); 67 | service.expandedKeys.value.select(...newVal); 68 | flatList.value = service.flattenTree(props.source, props.defaultSelectedKey, props.defaultCheckedKeys, props.defaultExpandedKeys, props.defaultDisabledKeys); 69 | }); 70 | 71 | watch(() => props.defaultDisabledKeys, newVal => { 72 | service.resetDefaultDisabledKeys(newVal); 73 | service.disabledKeys.value.clear(); 74 | service.disabledKeys.value.select(...newVal); 75 | }); 76 | 77 | watch(() => props.defaultSelectedKey, newVal => { 78 | service.resetDefaultSelectedKey(newVal); 79 | const target = flatList.value.find(item => item.nodeKey === newVal); 80 | if (target) { 81 | service.selectedNodes.value.clear(); 82 | service.selectedNodes.value.select(target); 83 | } 84 | }); 85 | 86 | watch(() => props.defaultCheckedKeys, newVal => { 87 | service.resetDefaultCheckedKeys(newVal); 88 | if (newVal.length) { 89 | service.checkedNodeKeys.value.clear(); 90 | service.checkedNodeKeys.value.select(...newVal); 91 | } 92 | }); 93 | 94 | 95 | const selectChange = (node: Required) => { 96 | const preSelectedNode = service.selectedNodes.value.selected[0]; 97 | let currentNode: TypeWithNull = node; 98 | if (service.selectedNodes.value.isSelected(node)) { 99 | service.selectedNodes.value.clear(); 100 | currentNode = null; 101 | service.resetDefaultSelectedKey(); 102 | } else { 103 | service.selectedNodes.value.select(node); 104 | } 105 | emit('selectChange', { 106 | preSelectedNode, 107 | node: currentNode 108 | }); 109 | } 110 | 111 | 112 | const checkChange = ([checked, node]: [boolean, Required]) => { 113 | service.checkedNodeKeys.value.toggle(node.nodeKey); 114 | if (!checked) { 115 | service.removeDefaultCheckedKeys(node); 116 | } 117 | if (!props.checkStrictly) { 118 | service.updateDownwards(checked, node); 119 | service.updateUpwards(node, flatList.value); 120 | } 121 | // console.log('checkChange defaultCheckedKeys:>> ', service.defaultCheckedKeys); 122 | // console.log('checkChange selected:>> ', service.checkedNodeKeys.value.selected); 123 | emit('checkChange', {checked, node}); 124 | } 125 | 126 | const expandNode = (node: Required, children: TreeNodeOptions[] = []) => { 127 | const trueChildren = children.length ? children : cloneDeep(node.children)!; 128 | const selectedKey = service.selectedNodes.value.selected[0]?.nodeKey || service.defaultSelectedKey; 129 | const allExpandedKeys = service.expandedKeys.value.selected.concat(service.defaultExpandedKeys); 130 | const allCheckedKeys = service.checkedNodeKeys.value.selected.concat(service.defaultCheckedKeys); 131 | // !props.checkStrictly && 132 | if (!props.checkStrictly && service.checkedNodeKeys.value.isSelected(node.nodeKey)) { 133 | allCheckedKeys.push(...trueChildren.map(item => item.nodeKey)); 134 | } 135 | node.children = service.flattenTree(trueChildren, selectedKey, uniq(allCheckedKeys), allExpandedKeys, props.defaultDisabledKeys, props.checkStrictly, node); 136 | const targetIndex = flatList.value.findIndex(item => item.nodeKey === node.nodeKey); 137 | flatList.value.splice(targetIndex + 1, 0, ...(node.children as Required[])); 138 | } 139 | 140 | const collapseNode = (targetNode: Required) => { 141 | const delKeys: NodeKey[] = []; 142 | const recursion = (node: Required) => { 143 | if (node.children?.length) { 144 | (node.children as Required[]).forEach(item => { 145 | delKeys.push(item.nodeKey); 146 | if (service.expandedKeys.value.isSelected(item.nodeKey)) { 147 | service.expandedKeys.value.deselect(item.nodeKey); 148 | service.removeDefaultExpandedKeys(item.nodeKey); 149 | recursion(item as Required); 150 | } 151 | }); 152 | } 153 | } 154 | recursion(targetNode); 155 | if (delKeys.length) { 156 | flatList.value = flatList.value.filter(item => !delKeys.includes(item.nodeKey)); 157 | } 158 | } 159 | 160 | const toggleExpand = (node: Required) => { 161 | if (loading.value) return; 162 | service.expandedKeys.value.toggle(node.nodeKey); 163 | if (service.expandedKeys.value.isSelected(node.nodeKey)) { 164 | if (node.children?.length) { 165 | expandNode(node); 166 | } else { 167 | if (props.loadData) { 168 | node.loading = true; 169 | loading.value = true; 170 | // this.$forceUpdate(); 171 | props.loadData(node, children => { 172 | node.loading = false; 173 | loading.value = false; 174 | if (children.length) { 175 | expandNode(node, children); 176 | } 177 | }); 178 | } 179 | } 180 | } else { 181 | service.removeDefaultExpandedKeys(node.nodeKey); 182 | collapseNode(node); 183 | } 184 | emit('toggleExpand', { status: service.expandedKeys.value.isSelected(node.nodeKey), node }); 185 | } 186 | const nodeRefs = ref([]); 187 | const setRef = (index: number, node: any) => { 188 | if (node) { 189 | nodeRefs.value[index] = node as TreeNodeInstance; 190 | } 191 | } 192 | expose({ 193 | getSelectedNode: (): TypeWithUndefined => { 194 | return service.selectedNodes.value.selected[0]; 195 | }, 196 | getCheckedNodes: (): TreeNodeOptions[] => { 197 | return props.loadData 198 | ? flatList.value.filter(item => service.checkedNodeKeys.value.selected.includes(item.nodeKey)) 199 | : service.getCheckedNodes(props.source, service.checkedNodeKeys.value.selected, props.checkStrictly); 200 | }, 201 | getHalfCheckedNodes: (): TreeNodeOptions[] => { 202 | return nodeRefs.value.filter(item => item.halfChecked()).map(item => item.rawNode); 203 | }, 204 | getExpandedKeys: (): NodeKey[] => { 205 | return service.expandedKeys.value.selected; 206 | } 207 | }); 208 | 209 | return () => { 210 | return ( 211 |
212 | { 213 | h(VirtualList, { 214 | class: ['vir-tree-wrap'], 215 | size: props.size, 216 | remain: props.remain, 217 | list: flatList.value, 218 | dataKey: 'nodeKey', 219 | }, { 220 | // @ts-ignore 221 | default: (data: { item: Required, index: number }) => h(VirTreeNode, { 222 | ref: setRef.bind(null, data.index), 223 | node: data.item, 224 | selectedNodes: service.selectedNodes.value.selected, 225 | checkedNodeKeys: service.checkedNodeKeys.value.selected, 226 | expandedKeys: service.expandedKeys.value.selected, 227 | disabledKeys: service.disabledKeys.value.selected, 228 | showCheckbox: props.showCheckbox, 229 | checkStrictly: props.checkStrictly, 230 | iconSlot: slots.icon, 231 | render: props.render, 232 | onSelectChange: selectChange, 233 | onToggleExpand: toggleExpand, 234 | onCheckChange: checkChange 235 | }) 236 | }) 237 | } 238 |
239 | ); 240 | } 241 | } 242 | }); 243 | -------------------------------------------------------------------------------- /src/components/VirtualTree/node.tsx: -------------------------------------------------------------------------------- 1 | import { computed, defineComponent, PropType, Slot, watch, onMounted } from 'vue'; 2 | import {NodeKey, TreeNodeOptions} from "./types"; 3 | import VirtualCheckbox from '../VirtualCheckbox'; 4 | import RenderNode from './render'; 5 | import {SelectionModel} from "../selections"; 6 | 7 | export default defineComponent({ 8 | name: 'VirTreeNode', 9 | props: { 10 | selectedNodes: { 11 | type: Array as PropType[]>, 12 | required: true 13 | }, 14 | checkedNodeKeys: { 15 | type: Array as PropType, 16 | required: true 17 | }, 18 | expandedKeys: { 19 | type: Array as PropType, 20 | required: true 21 | }, 22 | disabledKeys: { 23 | type: Array as PropType, 24 | required: true 25 | }, 26 | node: { 27 | type: Object as PropType>, 28 | required: true 29 | }, 30 | iconSlot: Function as PropType, 31 | showCheckbox: { 32 | type: Boolean, 33 | default: false 34 | }, 35 | checkStrictly: { 36 | type: Boolean, 37 | default: false 38 | }, 39 | render: Function 40 | }, 41 | emits: ['selectChange', 'toggleExpand', 'checkChange'], 42 | setup(props, { emit, expose }) { 43 | const getCheckedChildrenSize =(): number => { 44 | let result = 0; 45 | if (!props.checkStrictly && props.node.hasChildren) { 46 | const { children } = props.node; 47 | const checkedChildren = (children as Required[])!.filter(item => props.checkedNodeKeys.includes(item.nodeKey)); 48 | result = checkedChildren.length; 49 | } 50 | return result; 51 | } 52 | 53 | const setCheckedStatus = ()=> { 54 | const checkedChildrenSize = getCheckedChildrenSize(); 55 | const shouldChecked = checkedChildrenSize > 0 && checkedChildrenSize === props.node.children!.length; 56 | if (shouldChecked && ! props.checkedNodeKeys.includes(props.node.nodeKey)) { 57 | handleCheckChange(shouldChecked); 58 | } 59 | } 60 | 61 | const handleSelect = (event: MouseEvent) => { 62 | event.stopPropagation(); 63 | if (!props.disabledKeys.includes(props.node.nodeKey)) { 64 | emit('selectChange', props.node); 65 | } 66 | } 67 | const handleExpand = () => { 68 | emit('toggleExpand', props.node); 69 | } 70 | const handleCheckChange = (checked: boolean) => { 71 | emit('checkChange', [checked, props.node]); 72 | } 73 | 74 | watch(() => props.node, () => { 75 | setCheckedStatus(); 76 | }); 77 | 78 | watch(() => props.checkedNodeKeys, newVal => { 79 | setCheckedStatus(); 80 | }); 81 | 82 | onMounted(() => { 83 | setCheckedStatus(); 84 | }); 85 | 86 | const halfChecked = computed(() => { 87 | let result = false; 88 | const checkedChildrenSize = getCheckedChildrenSize(); 89 | result = checkedChildrenSize > 0 && checkedChildrenSize < props.node.children!.length; 90 | return result; 91 | }); 92 | 93 | const textCls = computed(() => { 94 | let result = 'node-title'; 95 | if (props.selectedNodes[0].nodeKey === props.node.nodeKey) { 96 | result += ' selected'; 97 | } 98 | if (props.disabledKeys.includes(props.node.nodeKey)) { 99 | result += ' disabled'; 100 | } 101 | return result; 102 | }); 103 | 104 | const renderArrow = (): JSX.Element | null => { 105 | return
106 | { 107 | props.node.hasChildren 108 | ? props.iconSlot ? props.iconSlot(props.node.loading) : props.node.loading 109 | ? 110 | : 111 | : null 112 | } 113 |
114 | } 115 | 116 | const renderContent = () => { 117 | if (props.showCheckbox) { 118 | return 125 | { 126 | props.render ? : { props.node.name } 127 | } 128 | ; 129 | } 130 | return
131 | { 132 | props.render ? : { props.node.name } 133 | } 134 |
; 135 | } 136 | expose({ 137 | rawNode: props.node, 138 | halfChecked: () => halfChecked.value 139 | }); 140 | // console.log('iconSlot', props.iconSlot); 141 | return () => { 142 | return ( 143 |
144 | { renderArrow() } 145 | { renderContent() } 146 |
147 | ); 148 | } 149 | } 150 | }); 151 | -------------------------------------------------------------------------------- /src/components/VirtualTree/render.tsx: -------------------------------------------------------------------------------- 1 | import {defineComponent, PropType} from "vue"; 2 | import {TreeNodeOptions} from "./types"; 3 | 4 | export default defineComponent({ 5 | name: 'RenderNode', 6 | props: { 7 | node: { 8 | type: Object as PropType, 9 | required: true 10 | }, 11 | render: { 12 | type: Function, 13 | required: true 14 | } 15 | }, 16 | setup(props) { 17 | return () => { 18 | return props.render(props.node); 19 | } 20 | } 21 | }); 22 | -------------------------------------------------------------------------------- /src/components/VirtualTree/service.ts: -------------------------------------------------------------------------------- 1 | import {NodeKey, TreeNodeOptions, TypeWithNull} from "./types"; 2 | import {ref} from "vue"; 3 | import {SelectionModel} from "../selections"; 4 | 5 | class TreeService { 6 | selectedNodes = ref(new SelectionModel>()); 7 | checkedNodeKeys = ref(new SelectionModel(true)); 8 | expandedKeys = ref(new SelectionModel(true)); 9 | disabledKeys = ref(new SelectionModel(true)); 10 | 11 | defaultSelectedKey: NodeKey = ''; 12 | defaultCheckedKeys: NodeKey[] = []; 13 | defaultExpandedKeys: NodeKey[] = []; 14 | defaultDisabledKeys: NodeKey[] = []; 15 | 16 | // flatSourceTree: TreeNodeOptions[] = []; 17 | flatTree: Required[] = []; 18 | 19 | constructor() {} 20 | 21 | flattenTree( 22 | source: TreeNodeOptions[], 23 | defaultSelectedKey: NodeKey, 24 | defaultCheckedKeys: NodeKey[], 25 | defaultExpandedKeys: NodeKey[], 26 | defaultDisabledKeys: NodeKey[], 27 | checkStrictly = false, 28 | parent: TypeWithNull> = null 29 | ): Required[] { 30 | this.defaultSelectedKey = defaultSelectedKey; 31 | this.defaultCheckedKeys = defaultCheckedKeys; 32 | this.defaultExpandedKeys = defaultExpandedKeys; 33 | this.defaultDisabledKeys = defaultDisabledKeys; 34 | const result: Required[] = []; 35 | const recursion = (list: TreeNodeOptions[], parent: TypeWithNull> = null) => { 36 | return list.map(item => { 37 | const childrenSize = item.children?.length || 0; 38 | const flatNode: Required = { 39 | ...item, 40 | level: parent ? parent.level + 1 : item.level || 0, 41 | loading: false, 42 | hasChildren: item.hasChildren || childrenSize > 0, 43 | parentKey: parent?.nodeKey || null, 44 | children: item.children || [] 45 | }; 46 | let goon = true; 47 | if (parent) { 48 | if (defaultExpandedKeys.includes(parent.nodeKey)) { 49 | if (!checkStrictly && defaultCheckedKeys.includes(parent.nodeKey)) { // 默认展开并选中了 50 | defaultCheckedKeys.push(flatNode.nodeKey); 51 | this.checkedNodeKeys.value.select(flatNode.nodeKey); 52 | } 53 | result.push(flatNode); 54 | } else { 55 | goon = false; 56 | } 57 | } else { 58 | result.push(flatNode); 59 | } 60 | 61 | if (defaultDisabledKeys.includes(flatNode.nodeKey)) { 62 | this.disabledKeys.value.select(flatNode.nodeKey); 63 | } 64 | if (defaultSelectedKey === flatNode.nodeKey) { 65 | this.selectedNodes.value.select(flatNode); 66 | } 67 | if (defaultExpandedKeys.includes(flatNode.nodeKey)) { 68 | this.expandedKeys.value.select(flatNode.nodeKey); 69 | } 70 | 71 | if (defaultCheckedKeys.includes(flatNode.nodeKey)) { 72 | this.checkedNodeKeys.value.select(flatNode.nodeKey); 73 | } 74 | 75 | if (goon && childrenSize) { 76 | flatNode.children = recursion(flatNode.children, flatNode); 77 | } 78 | return flatNode; 79 | }); 80 | } 81 | 82 | recursion(source, parent); 83 | return result; 84 | } 85 | 86 | updateDownwards(checked: boolean, node: Required) { 87 | const update = (children: Required[]) => { 88 | if (children.length) { 89 | children.forEach(child => { 90 | const checkFunc = checked ? 'select' : 'deselect'; 91 | this.checkedNodeKeys.value[checkFunc](child.nodeKey); 92 | if (!checked) { 93 | this.removeDefaultCheckedKeys(child); 94 | } 95 | if (child.children?.length) { 96 | update(child.children as Required[]); 97 | } 98 | }); 99 | } 100 | } 101 | update(node.children as Required[]); 102 | } 103 | 104 | updateUpwards(targetNode: Required, flatList: Required[]) { 105 | const update = (node: Required) => { 106 | if (node.parentKey != null) { // 说明是子节点 107 | const parentNode = flatList.find(item => item.nodeKey == node.parentKey)!; 108 | // console.log('parentNode', parentNode); 109 | const parentChecked = (parentNode.children as Required[]).every((child) => this.checkedNodeKeys.value.isSelected(child.nodeKey)); 110 | if (parentChecked !== this.checkedNodeKeys.value.isSelected(parentNode.nodeKey)) { // 父节点变了的话,就还要继续向上更新 111 | this.checkedNodeKeys.value.toggle(parentNode.nodeKey); 112 | if (!parentChecked) { 113 | this.removeDefaultCheckedKeys(parentNode); 114 | } 115 | update(parentNode); 116 | } 117 | } 118 | } 119 | update(targetNode); 120 | } 121 | 122 | resetDefaultSelectedKey(key: NodeKey = '') { 123 | this.defaultSelectedKey = key; 124 | } 125 | 126 | resetDefaultDisabledKeys(keys: NodeKey[]) { 127 | this.defaultDisabledKeys = keys; 128 | } 129 | 130 | resetDefaultCheckedKeys(keys: NodeKey[]) { 131 | this.defaultCheckedKeys = keys; 132 | } 133 | 134 | removeDefaultCheckedKeys(node: TreeNodeOptions) { 135 | const inDefaultIndex = this.defaultCheckedKeys.findIndex(item => item === node.nodeKey); 136 | if (inDefaultIndex > -1) { 137 | this.defaultCheckedKeys.splice(inDefaultIndex, 1); 138 | } 139 | } 140 | 141 | resetDefaultExpandedKeys(keys: NodeKey[]) { 142 | this.defaultExpandedKeys = keys; 143 | } 144 | 145 | removeDefaultExpandedKeys(key: NodeKey) { 146 | const inDefaultIndex = this.defaultExpandedKeys.findIndex(item => item === key); 147 | if (inDefaultIndex > -1) { 148 | this.defaultExpandedKeys.splice(inDefaultIndex, 1); 149 | } 150 | } 151 | 152 | 153 | getCheckedNodes( 154 | source: TreeNodeOptions[], 155 | checkedKeys: NodeKey[], 156 | checkStrictly = false 157 | ): TreeNodeOptions[] { 158 | const result: TreeNodeOptions[] = []; 159 | const checkedSize = checkedKeys.length; 160 | // console.log('checkedSize :>> ', checkedSize); 161 | let count = 0; 162 | // console.log('flatSourceTree :>> ', this.flatSourceTree); 163 | const recursion = (list: TreeNodeOptions[], parent: TypeWithNull = null) => { 164 | for (const item of list) { 165 | let goon = true; 166 | if (parent) { 167 | if (checkedKeys.includes(item.nodeKey)) { // 本身就在checkedKeys里的让它走正常流程 168 | count++; 169 | result.push(item); 170 | } else { 171 | if (!checkStrictly && result.map(rItem => rItem.nodeKey).includes(parent.nodeKey)) { 172 | result.push(item); // 爹已选中 但自身不在checkedKeys里的让它跟爹走 173 | } else { 174 | if (count >= checkedSize) { // 爹和自己都没选中,如果checkedKeys里的内容找齐了,结束 175 | goon = false; 176 | } 177 | } 178 | } 179 | } else { 180 | if (checkedKeys.includes(item.nodeKey)) { 181 | count++; 182 | result.push(item); 183 | } else { 184 | if (count >= checkedSize) { 185 | goon = false; 186 | } 187 | } 188 | } 189 | if (goon) { 190 | if ( item.children?.length) { 191 | recursion(item.children, item); 192 | } 193 | } else { 194 | break; 195 | } 196 | } 197 | } 198 | if (checkedSize) { 199 | recursion(source); 200 | } 201 | return result; 202 | } 203 | } 204 | 205 | 206 | 207 | 208 | export { TreeService }; 209 | -------------------------------------------------------------------------------- /src/components/VirtualTree/types.ts: -------------------------------------------------------------------------------- 1 | type NodeKey = string | number; 2 | 3 | /* 4 | * 用户传入的source必须要有 key, name 5 | * */ 6 | 7 | interface TreeNodeOptions { 8 | nodeKey: NodeKey; 9 | name: string; 10 | level?: number; 11 | loading?: boolean; 12 | hasChildren?: boolean; 13 | children?: TreeNodeOptions[]; 14 | parentKey?: NodeKey | null; 15 | } 16 | 17 | interface TreeInstance { 18 | getSelectedNode: () => TreeNodeOptions | undefined; 19 | getCheckedNodes: () => TreeNodeOptions[]; 20 | getHalfCheckedNodes: () => TreeNodeOptions[]; 21 | getExpandedKeys: () => NodeKey[]; 22 | } 23 | 24 | interface TreeNodeInstance { 25 | rawNode: TreeNodeOptions; 26 | halfChecked: () => boolean; 27 | } 28 | 29 | type TypeWithNull = T | null; 30 | type TypeWithUndefined = T | undefined; 31 | 32 | export { TreeNodeOptions, NodeKey, TreeInstance, TreeNodeInstance, TypeWithUndefined, TypeWithNull }; 33 | -------------------------------------------------------------------------------- /src/components/VirtualTree/uses备份.ts: -------------------------------------------------------------------------------- 1 | import {NodeKey, TreeNodeOptions} from "./types"; 2 | import {ref} from "vue"; 3 | import {SelectionModel} from "../selections"; 4 | 5 | const selectedNodes = ref(new SelectionModel>()); 6 | const checkedNodes = ref(new SelectionModel>(true)); 7 | const expandedKeys = ref(new SelectionModel(true)); 8 | const disabledKeys = ref(new SelectionModel(true)); 9 | 10 | function flattenTree( 11 | source: TreeNodeOptions[], 12 | defaultCheckedKeys: NodeKey[], 13 | defaultExpandedKeys: NodeKey[], 14 | ): Required[] { 15 | const result: Required[] = []; 16 | function recursion (list: TreeNodeOptions[], level = 0, parent: Required | null = null) { 17 | return list.map(item => { 18 | const flatNode: Required = { 19 | ...item, 20 | level, 21 | loading: false, 22 | hasChildren: item.hasChildren || false, 23 | parentKey: parent?.nodeKey || null, 24 | children: item.children || [] 25 | }; 26 | result.push(flatNode); 27 | if (defaultCheckedKeys.includes(item.nodeKey)) { 28 | checkedNodes.value.select(flatNode); 29 | } 30 | if (defaultExpandedKeys.includes(item.nodeKey) && item.children?.length) { 31 | expandedKeys.value.select(item.nodeKey); 32 | flatNode.children = recursion(item.children, level + 1, flatNode); 33 | } 34 | return flatNode; 35 | }); 36 | } 37 | 38 | recursion(source); 39 | return result; 40 | } 41 | 42 | 43 | function updateDownwards(checked: boolean, node: Required) { 44 | const update = (children: Required[]) => { 45 | if (children.length) { 46 | children.forEach(child => { 47 | const checkFunc = checked ? 'select' : 'deselect'; 48 | checkedNodes.value[checkFunc](child); 49 | if (child.children?.length) { 50 | update(child.children as Required[]); 51 | } 52 | }); 53 | } 54 | } 55 | update(node.children as Required[]); 56 | } 57 | 58 | function updateUpwards(targetNode: Required, flatList: Required[]) { 59 | const update = (node: Required) => { 60 | if (node.parentKey != null) { // 说明是子节点 61 | const parentNode = flatList.find(item => item.nodeKey == node.parentKey)!; 62 | // console.log('parentNode', parentNode); 63 | const parentChecked = (parentNode.children as Required[]).every((child) => checkedNodes.value.isSelected(child)); 64 | if (parentChecked !== checkedNodes.value.isSelected(parentNode)) { // 父节点变了的话,就还要继续向上更新 65 | checkedNodes.value.toggle(parentNode) 66 | update(parentNode); 67 | } 68 | } 69 | } 70 | update(targetNode); 71 | } 72 | 73 | export { selectedNodes, checkedNodes, expandedKeys, disabledKeys, flattenTree, updateUpwards, updateDownwards }; 74 | -------------------------------------------------------------------------------- /src/components/index.ts: -------------------------------------------------------------------------------- 1 | import { App } from 'vue'; 2 | import VirTree from './VirtualTree'; 3 | export { VirTree }; 4 | export * from './VirtualTree/types'; 5 | export default function (app: App) { 6 | app.component(VirTree.name, VirTree); 7 | } 8 | -------------------------------------------------------------------------------- /src/components/selections.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | /** 4 | * Class to be used to power selecting one or more options from a list. 5 | */ 6 | export class SelectionModel { 7 | /** Currently-selected values. */ 8 | private _selection = new Set(); 9 | 10 | /** Keeps track of the deselected options that haven't been emitted by the change event. */ 11 | private _deselectedToEmit: T[] = []; 12 | 13 | /** Keeps track of the selected options that haven't been emitted by the change event. */ 14 | private _selectedToEmit: T[] = []; 15 | 16 | /** Cache for the array value of the selected items. */ 17 | private _selected: T[] | null = null; 18 | 19 | /** Selected values. */ 20 | get selected(): T[] { 21 | if (!this._selected) { 22 | this._selected = Array.from(this._selection.values()); 23 | } 24 | 25 | return this._selected; 26 | } 27 | 28 | /** Event emitted when the value has changed. */ 29 | // readonly changed = new Subject>(); 30 | 31 | constructor( 32 | private _multiple = false, 33 | initiallySelectedValues?: T[], 34 | private _emitChanges = true, 35 | ) { 36 | if (initiallySelectedValues && initiallySelectedValues.length) { 37 | if (_multiple) { 38 | initiallySelectedValues.forEach(value => this._markSelected(value)); 39 | } else { 40 | this._markSelected(initiallySelectedValues[0]); 41 | } 42 | 43 | // Clear the array in order to avoid firing the change event for preselected values. 44 | this._selectedToEmit.length = 0; 45 | } 46 | } 47 | 48 | /** 49 | * Selects a value or an array of values. 50 | */ 51 | select(...values: T[]): void { 52 | this._verifyValueAssignment(values); 53 | values.forEach(value => this._markSelected(value)); 54 | this._emitChangeEvent(); 55 | } 56 | 57 | /** 58 | * Deselects a value or an array of values. 59 | */ 60 | deselect(...values: T[]): void { 61 | this._verifyValueAssignment(values); 62 | values.forEach(value => this._unmarkSelected(value)); 63 | this._emitChangeEvent(); 64 | } 65 | 66 | /** 67 | * Toggles a value between selected and deselected. 68 | */ 69 | toggle(value: T): void { 70 | this.isSelected(value) ? this.deselect(value) : this.select(value); 71 | } 72 | 73 | /** 74 | * Clears all of the selected values. 75 | */ 76 | clear(): void { 77 | this._unmarkAll(); 78 | this._emitChangeEvent(); 79 | } 80 | 81 | /** 82 | * Determines whether a value is selected. 83 | */ 84 | isSelected(value: T): boolean { 85 | return this._selection.has(value); 86 | } 87 | 88 | /** 89 | * Determines whether the model does not have a value. 90 | */ 91 | isEmpty(): boolean { 92 | return this._selection.size === 0; 93 | } 94 | 95 | /** 96 | * Determines whether the model has a value. 97 | */ 98 | hasValue(): boolean { 99 | return !this.isEmpty(); 100 | } 101 | 102 | /** 103 | * Sorts the selected values based on a predicate function. 104 | */ 105 | sort(predicate?: (a: T, b: T) => number): void { 106 | if (this._multiple && this.selected) { 107 | this._selected!.sort(predicate); 108 | } 109 | } 110 | 111 | /** 112 | * Gets whether multiple values can be selected. 113 | */ 114 | isMultipleSelection() { 115 | return this._multiple; 116 | } 117 | 118 | /** Emits a change event and clears the records of selected and deselected values. */ 119 | private _emitChangeEvent() { 120 | // Clear the selected values so they can be re-cached. 121 | this._selected = null; 122 | 123 | if (this._selectedToEmit.length || this._deselectedToEmit.length) { 124 | /*this.changed.next({ 125 | source: this, 126 | added: this._selectedToEmit, 127 | removed: this._deselectedToEmit, 128 | });*/ 129 | 130 | this._deselectedToEmit = []; 131 | this._selectedToEmit = []; 132 | } 133 | } 134 | 135 | /** Selects a value. */ 136 | private _markSelected(value: T) { 137 | if (!this.isSelected(value)) { 138 | if (!this._multiple) { 139 | this._unmarkAll(); 140 | } 141 | 142 | this._selection.add(value); 143 | 144 | if (this._emitChanges) { 145 | this._selectedToEmit.push(value); 146 | } 147 | } 148 | } 149 | 150 | /** Deselects a value. */ 151 | private _unmarkSelected(value: T) { 152 | if (this.isSelected(value)) { 153 | this._selection.delete(value); 154 | 155 | if (this._emitChanges) { 156 | this._deselectedToEmit.push(value); 157 | } 158 | } 159 | } 160 | 161 | /** Clears out the selected values. */ 162 | private _unmarkAll() { 163 | if (!this.isEmpty()) { 164 | this._selection.forEach(value => this._unmarkSelected(value)); 165 | } 166 | } 167 | 168 | /** 169 | * Verifies the value assignment and throws an error if the specified value array is 170 | * including multiple values while the selection model is not supporting multiple values. 171 | */ 172 | private _verifyValueAssignment(values: T[]) { 173 | if (values.length > 1 && !this._multiple) { 174 | throw getMultipleValuesInSingleSelectionError(); 175 | 176 | } 177 | } 178 | } 179 | 180 | /** 181 | * Event emitted when the value of a MatSelectionModel has changed. 182 | * @docs-private 183 | */ 184 | export interface SelectionChange { 185 | /** Model that dispatched the event. */ 186 | source: SelectionModel; 187 | /** Options that were added to the model. */ 188 | added: T[]; 189 | /** Options that were removed from the model. */ 190 | removed: T[]; 191 | } 192 | 193 | /** 194 | * Returns an error that reports that multiple values are passed into a selection model 195 | * with a single value. 196 | * @docs-private 197 | */ 198 | export function getMultipleValuesInSingleSelectionError() { 199 | return Error('Cannot pass multiple values into SelectionModel with single-value mode.'); 200 | } 201 | -------------------------------------------------------------------------------- /src/components/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "emitDeclarationOnly": true, 6 | "declarationDir": "../../lib" 7 | }, 8 | "include": ["."] 9 | } 10 | -------------------------------------------------------------------------------- /src/doc/AsyncDataDemo.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 73 | -------------------------------------------------------------------------------- /src/doc/BaseDemo.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 67 | -------------------------------------------------------------------------------- /src/doc/CheckboxDemo.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 87 | 96 | -------------------------------------------------------------------------------- /src/doc/CustomIconDemo.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 62 | -------------------------------------------------------------------------------- /src/doc/CustomNodeDemo.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 50 | -------------------------------------------------------------------------------- /src/doc/DemoBox.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 67 | 68 | 120 | -------------------------------------------------------------------------------- /src/doc/HighlightCodes.json: -------------------------------------------------------------------------------- 1 | { 2 | "base": { 3 | "source": "\n\n\n", 4 | "highlight": "\n
<template>\n  <div class=\"demo\">\n    <a-button @click=\"selectedNode\">获取选中节点</a-button>\n    <vir-tree\n      ref=\"virTree\"\n      :source=\"list\"\n      :default-disabled-keys=\"defaultDisabledKeys\"\n      :default-selected-key=\"defaultSelectedKey\"\n      :default-expanded-keys=\"defaultExpandedKeys\"\n    />\n  </div>\n</template>\n\n<script lang=\"tsx\">\n  import {defineComponent, onMounted, ref} from 'vue';\n  import {TreeInstance, TreeNodeOptions} from \"vue-virtual-tree\";\n\n  function recursion(path = '0', level = 3): TreeNodeOptions[] {\n    const list = [];\n    for (let i = 0; i < 10; i += 1) {\n      const nodeKey = `${path}-${i}`;\n      const treeNode: TreeNodeOptions  = {\n        nodeKey,\n        name: nodeKey,\n        children: [],\n        hasChildren: true\n      };\n\n      if (level > 0) {\n        treeNode.children = recursion(nodeKey, level - 1);\n      } else {\n        treeNode.hasChildren = false;\n      }\n\n      list.push(treeNode);\n    }\n    return list;\n  }\n\n  export default defineComponent({\n    name: 'BaseDemo',\n    setup() {\n      const list = ref<TreeNodeOptions[]>([]);\n      const virTree = ref<TreeInstance | null>(null);\n      const defaultExpandedKeys = ref(['0-0', '0-1', '0-1-0']);\n      const defaultSelectedKey = ref('0-0-1-0');\n      const defaultDisabledKeys = ref(['0-0-1']);\n\n      onMounted(() => {\n        list.value = recursion();\n      });\n      const selectedNode = () => {\n\t\tconst node = virTree.value!.getSelectedNode();\n\t\tconsole.log('selected node', node);\n      }\n      return {\n        list,\n        virTree,\n        selectedNode,\n        defaultExpandedKeys,\n        defaultSelectedKey,\n        defaultDisabledKeys\n      }\n    }\n  });\n</script>\n
" 5 | }, 6 | "checkbox": { 7 | "source": "\n\n\n\n", 8 | "highlight": "\n
<template>\n  <div class=\"demo\">\n    <section>\n      <h5>默认父子节点联动</h5>\n      <a-button @click=\"halfNodes\">获取半选节点</a-button>\n      <vir-tree ref=\"virTreeOne\" show-checkbox :source=\"list\" :default-checked-keys=\"defaultCheckedKeys\" />\n    </section>\n    <section>\n      <h5>父子节点不联动</h5>\n      <a-button @click=\"checkedNodes\">获取勾选节点</a-button>\n      <vir-tree ref=\"virTreeTwo\" show-checkbox check-strictly :source=\"list\" :default-checked-keys=\"defaultCheckedKeys\" />\n    </section>\n  </div>\n</template>\n\n<script lang=\"tsx\">\n  import {defineComponent, onMounted, ref} from 'vue';\n  import {TreeInstance, TreeNodeOptions} from \"vue-virtual-tree\";\";\n\n  function recursion(path = '0', level = 3): TreeNodeOptions[] {\n    const list = [];\n    for (let i = 0; i < 10; i++) {\n      const nodeKey = `${path}-${i}`;\n      const treeNode: TreeNodeOptions = {\n        nodeKey,\n        name: nodeKey,\n        children: [],\n        hasChildren: true\n      };\n\n      if (level > 0) {\n        treeNode.children = recursion(nodeKey, level - 1);\n      } else {\n        treeNode.hasChildren = false;\n      }\n\n      list.push(treeNode);\n    }\n    return list;\n  }\n\n  export default defineComponent({\n    name: 'CheckboxDemo',\n    setup() {\n      const list = ref<TreeNodeOptions[]>([]);\n      const virTreeOne = ref<TreeInstance | null>(null);\n      const virTreeTwo = ref<TreeInstance | null>(null);\n      const defaultCheckedKeys = ref(['0-0-0', '0-2']);\n\n\n      onMounted(() => {\n        list.value = recursion();\n      });\n      const halfNodes = () => {\n\t\tconst node = virTree.value!.getHalfCheckedNodes();\n\t\tconsole.log('halfNodes', node);\n      }\n      const checkedNodes = () => {\n\t\tconst node = virTree.value!.getCheckedNodes();\n\t\tconsole.log('checkedNodes node', node);\n      }\n      return {\n        list,\n        virTreeOne,\n        virTreeTwo,\n        halfNodes,\n        checkedNodes,\n        defaultCheckedKeys\n      }\n    }\n  });\n</script>\n<style scoped lang=\"scss\">\n  .demo {\n    display: flex;\n    justify-content: space-between;\n    section {\n      width: 45%;\n    }\n  }\n</style>\n
" 9 | }, 10 | "asyncData": { 11 | "source": "\n\n\n", 12 | "highlight": "
<template>\n  <div class=\"demo\">\n    <button @click=\"checkedNodes\">获取勾选节点</button>\n    <vir-tree ref=\"virTree\" :source=\"list\" show-checkbox :loadData=\"loadData\" />\n  </div>\n</template>\n\n<script lang=\"tsx\">\n  import {defineComponent, onMounted, ref} from 'vue';\n  import { VirTree } from \"vue-virtual-tree\";\n  import {TreeInstance, TreeNodeOptions} from \"vue-virtual-tree\";\n\n  function recursion(path = '0'): TreeNodeOptions[] {\n    const list = [];\n    for (let i = 0; i < 2; i += 1) {\n      const nodeKey = `${path}-${i}`;\n      const treeNode: TreeNodeOptions  = {\n        nodeKey,\n        name: nodeKey,\n        children: [],\n        hasChildren: true\n      };\n      list.push(treeNode);\n    }\n    return list;\n  }\n\n  export default defineComponent({\n    name: 'AsyncDataDemo',\n    components: { VirTree },\n    setup(prop, {emit}) {\n      const list = ref<TreeNodeOptions[]>([]);\n      const virTree = ref<TreeInstance | null>(null);\n      onMounted(() => {\n        list.value = recursion();\n      });\n      const loadData = (node: TreeNodeOptions, callback: (children: TreeNodeOptions[]) => void) => {\n        console.log('loadData', node);\n        const result: TreeNodeOptions[] = [];\n        for (let i = 0; i < 2; i += 1) {\n          const nodeKey = `${node.nodeKey}-${i}`;\n          const treeNode: TreeNodeOptions  = {\n            nodeKey,\n            name: nodeKey,\n            children: [],\n            hasChildren: true\n          };\n          result.push(treeNode);\n        }\n        setTimeout(() => {\n          callback(result);\n        }, 500);\n      }\n      const checkedNodes = () => {\n        const checks = virTree.value!.getCheckedNodes();\n        console.log('checks', checks);\n      }\n      return {\n        list,\n        virTree,\n        loadData,\n        checkedNodes\n      }\n    }\n  });\n</script>\n
" 13 | }, 14 | "customNode": { 15 | "source": "\n\n\n", 16 | "highlight": "
<template>\n  <div class=\"demo\">\n    <vir-tree :source=\"list\" show-checkbox :render=\"renderNode\" />\n  </div>\n</template>\n\n<script lang=\"tsx\">\n  import {defineComponent, onMounted, ref} from 'vue';\n  import { VirTree } from \"vue-virtual-tree\";\n  import {TreeInstance, TreeNodeOptions} from \"vue-virtual-tree\";\n\n  function recursion(path = '0', level = 3): TreeNodeOptions[] {\n    const list = [];\n    for (let i = 0; i < 10; i++) {\n      const nodeKey = `${path}-${i}`;\n      const treeNode: TreeNodeOptions = {\n        nodeKey,\n        name: nodeKey,\n        children: [],\n        hasChildren: true\n      };\n\n      if (level > 0) {\n        treeNode.children = recursion(nodeKey, level - 1);\n      } else {\n        treeNode.hasChildren = false;\n      }\n\n      list.push(treeNode);\n    }\n    return list;\n  }\n\n  export default defineComponent({\n    name: 'CustomNodeDemo',\n    setup(prop, {emit}) {\n      const list = ref<TreeNodeOptions[]>([]);\n      onMounted(() => {\n        list.value = recursion();\n      });\n      const renderNode = (node: TreeNodeOptions) => {\n        return <div style=\"padding: 0 4px;\"><b style=\"color: #f60;\">{ node.name }</b></div>\n      }\n      return {\n        list,\n        renderNode\n      }\n    }\n  });\n</script>"
17 |   },
18 |   "customIcon": {
19 |     "source": "\n\n\n",
20 |     "highlight": "
<template>\n  <div class=\"demo\">\n    <vir-tree :source=\"list\" show-checkbox :loadData=\"loadData\">\n      <template #icon=\"loading\">\n        <i v-if=\"loading\" class=\"iconfont iconcustom-icon ico-loading\"></i>\n        <i v-else class=\"iconfont iconzhankai\"></i>\n      </template>\n    </vir-tree>\n  </div>\n</template>\n\n<script lang=\"tsx\">\nimport {defineComponent, onMounted, ref} from 'vue';\nimport { VirTree } from \"vue-virtual-tree\";\nimport { TreeNodeOptions } from \"vue-virtual-tree\";\n\nfunction recursion(path = '0'): TreeNodeOptions[] {\n  const list = [];\n  for (let i = 0; i < 2; i += 1) {\n    const nodeKey = `${path}-${i}`;\n    const treeNode: TreeNodeOptions  = {\n      nodeKey,\n      name: nodeKey,\n      children: [],\n      hasChildren: true\n    };\n    list.push(treeNode);\n  }\n  return list;\n}\n\nexport default defineComponent({\n  name: 'CustomIcon',\n  setup(prop, {emit}) {\n    const list = ref<TreeNodeOptions[]>([]);\n    onMounted(() => {\n      list.value = recursion();\n    });\n    const loadData = (node: TreeNodeOptions, callback: (children: TreeNodeOptions[]) => void) => {\n      console.log('loadData', node);\n      const result: TreeNodeOptions[] = [];\n      for (let i = 0; i < 2; i += 1) {\n        const nodeKey = `${node.nodeKey}-${i}`;\n        const treeNode: TreeNodeOptions  = {\n          nodeKey,\n          name: nodeKey,\n          children: [],\n          hasChildren: true\n        };\n        result.push(treeNode);\n      }\n      setTimeout(() => {\n        callback(result);\n      }, 500);\n    }\n    return {\n      list,\n      loadData\n    }\n  }\n});\n</script>\n
" 21 | }, 22 | "searchNode": { 23 | "source": "\n\n\n", 24 | "highlight": "\n
<template>\n  <div class=\"demo\">\n    <a-input placeholder=\"回车搜索\" @pressEnter=\"search\" />\n    <section>\n      <vir-tree show-checkbox :source=\"list\" :render=\"renderNode\" :default-expanded-keys=\"expandKeys\" />\n    </section>\n  </div>\n</template>\n\n<script lang=\"tsx\">\n  import {defineComponent, onMounted, ref} from 'vue';\n  import {TreeNodeOptions, NodeKey} from \"vue-virtual-tree\";\n\n  interface TreeNodeOptionsWithParentPath extends TreeNodeOptions {\n    parentPath: NodeKey;\n  }\n\n  const UNIQUE_WRAPPERS = ['##==-open_tag-==##', '##==-close_tag-==##'];\n  function getParentPath (parent: TreeNodeOptionsWithParentPath | null): Array<string | number> {\n    let result: Array<string | number> = [];\n    if (parent) {\n      const base = parent.parentPath || [];\n      result = base.concat(parent.nodeKey);\n    }\n    return result;\n  }\n  function recursion(path = '0', level = 3, parent: TreeNodeOptionsWithParentPath | null = null): TreeNodeOptionsWithParentPath[] {\n    const list = [];\n    for (let i = 0; i < 10; i++) {\n      const nodeKey = `${path}-${i}`;\n      const treeNode: TreeNodeOptionsWithParentPath = {\n        nodeKey,\n        name: nodeKey,\n        children: [],\n        hasChildren: true,\n        parentPath: getParentPath(parent)\n      };\n\n      if (level > 0) {\n        treeNode.children = recursion(nodeKey, level - 1, treeNode);\n      } else {\n        treeNode.hasChildren = false;\n      }\n\n      list.push(treeNode);\n    }\n    return list;\n  }\n\n  export default defineComponent({\n    name: 'SearchNodeDemo',\n    setup() {\n      const keywords = ref('');\n      const list = ref<TreeNodeOptionsWithParentPath[]>([]);\n      const expandKeys = ref<NodeKey[]>([]);\n      onMounted(() => {\n        list.value = recursion();\n        // console.log('list', list.value);\n      });\n      const formatSearchValue = (value: string) => {\n        return new RegExp(value.replace(/([.*+?^=!:${}()|[\\]\\/\\\\])/g, '\\\\$&'), 'i');\n      }\n      const findMatchedNodes = (keywords: string): TreeNodeOptionsWithParentPath[] => {\n        const result: TreeNodeOptionsWithParentPath[] = [];\n        const recursion = (list: TreeNodeOptionsWithParentPath[], parent: TreeNodeOptionsWithParentPath | null = null) => {\n          for (const item of list) {\n            const matched = formatSearchValue(keywords).test(item.name);\n            if (matched) {\n              result.push(item);\n            }\n            if (parent) {\n              // parent.expanded = matched;\n            }\n            if (item.children?.length) {\n              recursion(item.children as TreeNodeOptionsWithParentPath[], item);\n            }\n          }\n        }\n        if (keywords) {\n          recursion(list.value);\n        }\n        return result;\n      }\n      const search = (event: KeyboardEvent) => {\n        keywords.value = (event.target as HTMLInputElement).value;\n        const matchedNodes = findMatchedNodes(keywords.value);\n        if (matchedNodes.length) {\n          // 取出parentPath > 拍扁 > 去重\n          expandKeys.value = [...new Set(matchedNodes.map(item => item.parentPath).flat())];\n        }\n      }\n\n      const transform = (value: string, matchValue: string) => {\n        if (matchValue) {\n          const wrapValue = value.replace(formatSearchValue(matchValue), `${UNIQUE_WRAPPERS[0]}$&${UNIQUE_WRAPPERS[1]}`);\n          return wrapValue\n            .replace(new RegExp(UNIQUE_WRAPPERS[0], 'g'), '<span style=\"color: #ff2041;\">')\n            .replace(new RegExp(UNIQUE_WRAPPERS[1], 'g'), '</span>');\n        }\n        return value;\n      }\n      const renderNode = (node: TreeNodeOptions) => {\n        const content = transform(node.name, keywords.value);\n        return <div style=\"padding: 0 4px;\" innerHTML={ content } />;\n      }\n      return {\n        list,\n        search,\n        renderNode,\n        expandKeys\n      }\n    }\n  });\n</script>\n
" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/doc/index.vue: -------------------------------------------------------------------------------- 1 | 104 | 105 | 133 | 134 | 152 | -------------------------------------------------------------------------------- /src/doc/principle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lycHub/vue-virtual-tree/568d45ba4e179af3827193f67c6f05f910a1f172/src/doc/principle.png -------------------------------------------------------------------------------- /src/doc/tableData.ts: -------------------------------------------------------------------------------- 1 | const columns = [ 2 | { 3 | title: '参数', 4 | dataIndex: 'argument' 5 | }, 6 | { 7 | title: '说明', 8 | dataIndex: 'description' 9 | }, 10 | { 11 | title: '类型', 12 | dataIndex: 'type' 13 | }, 14 | { 15 | title: '默认值', 16 | dataIndex: 'defaultValue' 17 | }, 18 | { 19 | title: '版本号', 20 | dataIndex: 'version' 21 | } 22 | ]; 23 | const methodColumns = [ 24 | { 25 | title: '名称', 26 | dataIndex: 'name' 27 | }, 28 | { 29 | title: '说明', 30 | dataIndex: 'description' 31 | }, 32 | { 33 | title: '参数', 34 | dataIndex: 'type' 35 | }, 36 | { 37 | title: '版本号', 38 | dataIndex: 'version' 39 | } 40 | ]; 41 | 42 | const propData = [ 43 | { 44 | argument: 'size', 45 | description: '用于虚拟计算,每个节点的高度', 46 | type: 'number', 47 | defaultValue: 27 48 | }, 49 | { 50 | argument: 'remain', 51 | description: '用于虚拟计算,可视区内显示多少个节点', 52 | type: 'number', 53 | defaultValue: 8 54 | }, 55 | { 56 | argument: 'source', 57 | description: '数据源', 58 | type: 'TreeNodeOptions[]', 59 | defaultValue: '[]' 60 | }, 61 | { 62 | argument: 'showCheckbox', 63 | description: '勾选模式', 64 | type: 'boolean', 65 | defaultValue: 'false' 66 | }, 67 | { 68 | argument: 'checkStrictly', 69 | description: '勾选时,父子不联动', 70 | type: 'boolean', 71 | defaultValue: 'false' 72 | }, 73 | { 74 | argument: 'loadData', 75 | description: '异步加载', 76 | type: '(node: TreeNodeOptions, callback: (children: TreeNodeOptions[]) => void) => void', 77 | defaultValue: 'undefined' 78 | }, 79 | { 80 | argument: 'render', 81 | description: '自定义渲染节点', 82 | type: '() => JSX.Element', 83 | defaultValue: 'undefined' 84 | }, 85 | { 86 | argument: 'defaultExpandedKeys', 87 | description: '默认展开的nodeKey数组', 88 | type: 'Array', 89 | defaultValue: '[]', 90 | version: '4.0.0' 91 | }, 92 | { 93 | argument: 'defaultDisabledKeys', 94 | description: '默认禁用的nodeKey数组', 95 | type: 'Array', 96 | defaultValue: '[]', 97 | version: '4.0.0' 98 | }, 99 | { 100 | argument: 'defaultCheckedKeys', 101 | description: '默认勾选的nodeKey数组', 102 | type: 'Array', 103 | defaultValue: '[]', 104 | version: '4.0.0' 105 | }, 106 | { 107 | argument: 'defaultSelectedKey', 108 | description: '默认选中的nodeKey', 109 | type: 'string | number', 110 | defaultValue: '', 111 | version: '4.0.0' 112 | }, 113 | ]; 114 | const eventData = [ 115 | { 116 | name: 'selectChange', 117 | description: '选择节点时触发', 118 | type: '{ preSelectedNode: TreeNodeOptions; node: TreeNodeOptions; },preSelectedNode和node分别是之前选中和当前选中的节点' 119 | }, 120 | { 121 | name: 'checkChange', 122 | description: '勾选节点时触发', 123 | type: '{ checked: boolean; node: TreeNodeOptions }' 124 | }, 125 | { 126 | name: 'toggleExpand', 127 | description: '展开收起时触发', 128 | type: '{ status: boolean; node: TreeNodeOptions; },status是当前的展开状态' 129 | } 130 | ]; 131 | const methodData = [ 132 | { 133 | name: 'getSelectedNode', 134 | description: '获取选中的节点', 135 | type: '() => TreeNodeOptions | undefined' 136 | }, 137 | { 138 | name: 'getCheckedNodes', 139 | description: '获取已勾选的节点', 140 | type: '() => TreeNodeOptions' 141 | }, 142 | { 143 | name: 'getHalfCheckedNodes', 144 | description: '获取半勾选的节点', 145 | type: '() => TreeNodeOptions' 146 | }, 147 | { 148 | name: 'getExpandedKeys', 149 | description: '获取已展开的nodeKeys', 150 | type: '() => Array', 151 | version: '4.0.0' 152 | } 153 | ]; 154 | const nodeOptionData = [ 155 | { 156 | argument: 'nodeKey', 157 | description: '必传,节点的唯一标识', 158 | type: 'string | number' 159 | }, 160 | { 161 | argument: 'name', 162 | description: '必传,显示的节点名称', 163 | type: 'string' 164 | }, 165 | { 166 | argument: 'hasChildren', 167 | description: '必传,用于判断是否还有children,控制展开图标的显示', 168 | type: 'boolean' 169 | }, 170 | { 171 | argument: 'level', 172 | description: '层级,内部计算', 173 | type: 'number' 174 | }, 175 | { 176 | argument: 'loading', 177 | description: '是否正在加载数据', 178 | type: 'boolean', 179 | defaultValue: 'false' 180 | }, 181 | { 182 | argument: 'disabled', 183 | description: '是否禁用', 184 | type: 'boolean', 185 | defaultValue: 'false', 186 | version: '4.0.0已废弃' 187 | }, 188 | { 189 | argument: 'expanded', 190 | description: '是否展开', 191 | type: 'boolean', 192 | defaultValue: 'false', 193 | version: '4.0.0已废弃' 194 | }, 195 | { 196 | argument: 'selected', 197 | description: '是否选中', 198 | type: 'boolean', 199 | defaultValue: 'false', 200 | version: '4.0.0已废弃' 201 | }, 202 | { 203 | argument: 'checked', 204 | description: '是否勾选', 205 | type: 'boolean', 206 | defaultValue: 'false', 207 | version: '4.0.0已废弃' 208 | }, 209 | { 210 | argument: 'children', 211 | description: '子集', 212 | type: 'TreeNodeOptions[]', 213 | defaultValue: '[]' 214 | }, 215 | { 216 | argument: 'parentKey', 217 | description: '父节点的nodeKey, 组件内部自动设置', 218 | type: 'string | number | null', 219 | defaultValue: 'null' 220 | }, 221 | ]; 222 | 223 | export { columns, methodColumns, propData, eventData, methodData, nodeOptionData }; 224 | -------------------------------------------------------------------------------- /src/doc/temp.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 113 | -------------------------------------------------------------------------------- /src/doc/uses/index.ts: -------------------------------------------------------------------------------- 1 | import { message } from 'ant-design-vue'; 2 | import {TreeInstance, TreeNodeOptions} from '../../components'; 3 | 4 | 5 | function getSelectedNode(tree: TreeInstance) { 6 | const node = tree.getSelectedNode(); 7 | console.log('selected node', node); 8 | if (node) { 9 | message.info(`选中了${node.name}`); 10 | } else { 11 | message.info('未选中节点'); 12 | } 13 | } 14 | 15 | 16 | function getHalfCheckNodes(tree: TreeInstance) { 17 | const checks = tree.getHalfCheckedNodes(); 18 | console.log('checks', checks); 19 | message.info(`${checks.length}个半选节点`); 20 | } 21 | 22 | function getCheckNodes(tree: TreeInstance) { 23 | const checks = tree.getCheckedNodes(); 24 | console.log('checks', checks); 25 | message.info(`选中了${checks.length}条数据`); 26 | } 27 | 28 | 29 | export { getCheckNodes, getSelectedNode, getHalfCheckNodes } 30 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue'; 2 | import App from './App.vue'; 3 | import AntUse from './ant-use'; 4 | import VirTree from './components'; 5 | import './assets/styles/index.scss'; 6 | createApp(App) 7 | .use(AntUse) 8 | .use(VirTree) 9 | .mount('#app'); 10 | -------------------------------------------------------------------------------- /src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | declare module '*.vue' { 3 | import type { DefineComponent } from 'vue' 4 | const component: DefineComponent<{}, {}, any> 5 | export default component 6 | } 7 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "strict": true, 6 | "jsx": "preserve", 7 | "importHelpers": true, 8 | "moduleResolution": "node", 9 | "skipLibCheck": true, 10 | "esModuleInterop": true, 11 | "allowSyntheticDefaultImports": true, 12 | "sourceMap": true, 13 | "baseUrl": ".", 14 | "types": [ 15 | "webpack-env" 16 | ], 17 | "lib": [ 18 | "esnext", 19 | "dom", 20 | "dom.iterable", 21 | "scripthost" 22 | ] 23 | }, 24 | "include": [ 25 | "src/**/*.ts", 26 | "src/**/*.tsx", 27 | "src/**/*.vue", 28 | "tests/**/*.ts", 29 | "tests/**/*.tsx" 30 | ], 31 | "exclude": [ 32 | "node_modules" 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | const tsImportPluginFactory = require('ts-import-plugin'); 2 | const lazyImport = () => ({ 3 | before: [tsImportPluginFactory({ 4 | libraryName: 'ant-design-vue', 5 | style: 'css' 6 | })] 7 | }); 8 | 9 | module.exports = { 10 | productionSourceMap: false, 11 | css: { 12 | extract: false 13 | }, 14 | pluginOptions: { 15 | 'style-resources-loader': { 16 | preProcessor: 'scss', 17 | patterns: ['./src/assets/styles/variable.scss'] 18 | } 19 | }, 20 | parallel: false, 21 | publicPath: process.env.NODE_ENV === 'production' ? '/vue-virtual-tree' : '/', 22 | chainWebpack(config) { 23 | config.module 24 | .rule('tsx') 25 | .use('ts-loader') 26 | .tap(options => { 27 | options.getCustomTransformers = lazyImport; 28 | // options.happyPackMode = false; 29 | return options 30 | }) 31 | .end(); 32 | 33 | config.module 34 | .rule('ts') 35 | .use('ts-loader') 36 | .tap(options => { 37 | options.getCustomTransformers = lazyImport; 38 | // options.happyPackMode = false; 39 | return options 40 | }) 41 | .end(); 42 | } 43 | } 44 | --------------------------------------------------------------------------------