├── .browserslistrc
├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── question.md
└── workflows
│ ├── deploy.yml
│ └── test.yml
├── .gitignore
├── .npmrc
├── .prettierrc.js
├── CHANGELOG.md
├── LICENSE
├── README.md
├── README_EN.md
├── babel.config.js
├── docs
├── assets
│ ├── index-BcUbPbUa.css
│ └── index-Cp_mucJn.js
└── index.html
├── examples
├── App.vue
├── Drag.vue
├── Drop.vue
├── DropDataChange.vue
├── DropRemote.vue
├── Feature.vue
├── InsertRenderTree.vue
├── Loading.vue
├── Mobile.vue
├── Performance.vue
├── Search.vue
├── SearchRemote.vue
├── SearchRootRemote.vue
├── app.css
├── env.d.ts
├── main.ts
├── tsconfig.json
├── tsconfig.node.json
└── vite.config.ts
├── index.html
├── markdown
├── design-tree-search.md
└── design-tree.md
├── package.json
├── pnpm-lock.yaml
├── postcss.config.js
├── site
├── .vitepress
│ ├── code
│ │ ├── ActionsSlot.vue
│ │ ├── BasicDrop.vue
│ │ ├── Cascade.vue
│ │ ├── Checkable.vue
│ │ ├── CustomDropDisplay.vue
│ │ ├── CustomDropInput.vue
│ │ ├── CustomNode.vue
│ │ ├── DataDisplay.vue
│ │ ├── DragAndDrop.vue
│ │ ├── ExpandAnimation.vue
│ │ ├── IgnoreMode.vue
│ │ ├── LocalSearch.vue
│ │ ├── NodeCreationAndRemoval.vue
│ │ ├── Performance.vue
│ │ ├── ReloadChildren.vue
│ │ ├── Remote.vue
│ │ ├── RemoteSearch.vue
│ │ ├── Selectable.vue
│ │ ├── SelectableAndCheckable.vue
│ │ ├── ShowLine.vue
│ │ ├── UpdateCustomField.vue
│ │ └── UpdateNodeTitle.vue
│ ├── components
│ │ ├── DemoRender.vue
│ │ ├── Playground.vue
│ │ ├── PlaygroundLink.vue
│ │ ├── VersionSelect.vue
│ │ └── code-demo.md
│ ├── config.mts
│ ├── constants
│ │ └── i18n.ts
│ ├── data
│ │ └── code.data.ts
│ ├── en.mts
│ ├── theme
│ │ └── index.ts
│ ├── utils
│ │ └── i18n.ts
│ └── zh.mts
├── api
│ ├── vtree-drop.md
│ ├── vtree-search.md
│ └── vtree.md
├── en
│ ├── api
│ │ ├── vtree-drop.md
│ │ ├── vtree-search.md
│ │ └── vtree.md
│ ├── examples
│ │ ├── node-manipulation.md
│ │ ├── performance.md
│ │ ├── tree-drop.md
│ │ ├── tree-search.md
│ │ └── tree.md
│ ├── guide
│ │ ├── getting-started.md
│ │ └── migration.md
│ └── index.md
├── examples
│ ├── node-manipulation.md
│ ├── performance.md
│ ├── tree-drop.md
│ ├── tree-search.md
│ └── tree.md
├── guide
│ ├── getting-started.md
│ └── migration.md
├── index.md
├── package.json
├── playground.md
└── vite.config.ts
├── src
├── components
│ ├── LoadingIcon.vue
│ ├── Tree.vue
│ ├── TreeDrop.vue
│ ├── TreeNode.vue
│ └── TreeSearch.vue
├── constants
│ ├── events.ts
│ └── index.ts
├── env.d.ts
├── hooks
│ ├── useExpandAnimation.ts
│ ├── useIframeResize.ts
│ ├── usePublicTreeAPI.ts
│ ├── useTreeCls.ts
│ ├── useTreeDropCls.ts
│ ├── useTreeNodeCls.ts
│ ├── useTreeSearchCls.ts
│ └── useVirtualList.ts
├── index.ts
├── store
│ ├── index.ts
│ ├── tree-event-target.ts
│ ├── tree-node.ts
│ └── tree-store.ts
├── styles
│ ├── index.less
│ ├── loading-icon.less
│ ├── tree-drop.less
│ ├── tree-search.less
│ ├── tree.less
│ └── variables.less
├── types
│ └── index.ts
├── utils.ts
└── vite-env.d.ts
├── tests
├── tree-data-generator.ts
└── unit
│ ├── tree-search.spec.ts
│ └── tree.spec.ts
├── tsconfig.json
├── tsconfig.node.json
├── tsconfig.tsbuildinfo
├── tslint.json
├── types
├── components
│ ├── LoadingIcon.vue.d.ts
│ ├── Tree.vue.d.ts
│ ├── TreeDrop.vue.d.ts
│ ├── TreeNode.vue.d.ts
│ └── TreeSearch.vue.d.ts
├── const.d.ts
├── constants
│ ├── events.d.ts
│ └── index.d.ts
├── hooks
│ ├── useExpandAnimation.d.ts
│ ├── useIframeResize.d.ts
│ ├── usePublicTreeAPI.d.ts
│ ├── useTreeCls.d.ts
│ ├── useTreeDropCls.d.ts
│ ├── useTreeNodeCls.d.ts
│ ├── useTreeSearchCls.d.ts
│ └── useVirtualList.d.ts
├── index.d.ts
├── store
│ ├── index.d.ts
│ ├── tree-event-target.d.ts
│ ├── tree-node.d.ts
│ └── tree-store.d.ts
├── types.d.ts
├── types
│ └── index.d.ts
└── utils.d.ts
├── vite.config.ts
└── vitest.config.ts
/.browserslistrc:
--------------------------------------------------------------------------------
1 | > 1%
2 | last 2 versions
3 | not ie <= 8
4 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Bug 描述**
11 | 请清晰描述 Bug 情况
12 |
13 | **复现步骤**
14 | 请描述复现步骤,并且提供最小可复现示例(CodeSandbox, CodePen 链接等)
15 |
16 |
17 |
18 | **期望表现**
19 |
20 |
21 | **实际表现**
22 |
23 |
24 | **组件版本**
25 | - Vue:
26 | - @wsfe/ctree:
27 | - @wsfe/vue-tree:
28 | - 其他可帮助复现的 npm 包名称及版本
29 |
30 | **额外信息**
31 | 其他要补充的信息
32 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/question.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Question
3 | about: Ask questions
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **问题描述**
11 | 想咨询的问题
12 |
13 | **组件版本**
14 | - Vue2 或 Vue3
15 | - @wsfe/ctree 或者 @wsfe/vue-tree
16 |
--------------------------------------------------------------------------------
/.github/workflows/deploy.yml:
--------------------------------------------------------------------------------
1 | # 构建 VitePress 站点并将其部署到 GitHub Pages 的示例工作流程
2 | #
3 | name: Deploy VitePress site to Pages
4 |
5 | on:
6 | # 在针对 `main` 分支的推送上运行。如果你
7 | # 使用 `master` 分支作为默认分支,请将其更改为 `master`
8 | push:
9 | branches: [dev]
10 |
11 | # 允许你从 Actions 选项卡手动运行此工作流程
12 | workflow_dispatch:
13 |
14 | # 设置 GITHUB_TOKEN 的权限,以允许部署到 GitHub Pages
15 | permissions:
16 | contents: read
17 | pages: write
18 | id-token: write
19 |
20 | # 只允许同时进行一次部署,跳过正在运行和最新队列之间的运行队列
21 | # 但是,不要取消正在进行的运行,因为我们希望允许这些生产部署完成
22 | concurrency:
23 | group: pages
24 | cancel-in-progress: false
25 |
26 | jobs:
27 | # 构建工作
28 | build:
29 | runs-on: ubuntu-latest
30 | steps:
31 | - name: Checkout
32 | uses: actions/checkout@v4
33 | with:
34 | fetch-depth: 0 # 如果未启用 lastUpdated,则不需要
35 | - uses: pnpm/action-setup@v3 # 如果使用 pnpm,请取消注释
36 | with:
37 | version: 8
38 | # - uses: oven-sh/setup-bun@v1 # 如果使用 Bun,请取消注释
39 | - name: Setup Node
40 | uses: actions/setup-node@v4
41 | with:
42 | node-version: 20
43 | cache: pnpm # 或 pnpm / yarn
44 | - name: Setup Pages
45 | uses: actions/configure-pages@v4
46 | - name: Install dependencies
47 | run: pnpm install # 或 pnpm install / yarn install / bun install
48 | - name: Build with VitePress
49 | run: pnpm docs:build # 或 pnpm docs:build / yarn docs:build / bun run docs:build
50 | - name: Upload artifact
51 | uses: actions/upload-pages-artifact@v3
52 | with:
53 | path: site/.vitepress/dist
54 |
55 | # 部署工作
56 | deploy:
57 | environment:
58 | name: github-pages
59 | url: ${{ steps.deployment.outputs.page_url }}
60 | needs: build
61 | runs-on: ubuntu-latest
62 | name: Deploy
63 | steps:
64 | - name: Deploy to GitHub Pages
65 | id: deployment
66 | uses: actions/deploy-pages@v4
67 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs
3 |
4 | name: Test
5 |
6 | on:
7 | push:
8 | branches: [ "dev" ]
9 | pull_request:
10 | branches: [ "dev" ]
11 |
12 | jobs:
13 | build:
14 |
15 | runs-on: ubuntu-latest
16 |
17 | strategy:
18 | matrix:
19 | node-version: [16.x, 18.x]
20 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/
21 |
22 | steps:
23 | - uses: actions/checkout@v4
24 | - uses: pnpm/action-setup@v3
25 | with:
26 | version: 8
27 | - name: Use Node.js ${{ matrix.node-version }}
28 | uses: actions/setup-node@v3
29 | with:
30 | node-version: ${{ matrix.node-version }}
31 | cache: 'pnpm'
32 | - run: pnpm install --frozen-lockfile
33 | - run: pnpm run test:ci
34 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | debug.log
2 | .DS_Store
3 | node_modules
4 | /dist
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 |
15 | # Editor directories and files
16 | .idea
17 | .vscode
18 | *.suo
19 | *.ntvs*
20 | *.njsproj
21 | *.sln
22 | *.sw*
23 |
24 | # Vitepress
25 | site/.vitepress/dist
26 | site/.vitepress/cache
27 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | # shamefully-hoist = true
2 | # strict-peer-dependencies=true
3 | # legacy-peer-deps=true
4 | # dedupe-peer-dependents=false
--------------------------------------------------------------------------------
/.prettierrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | bracketSpacing: true, // {abc: 'aa'} 会被格式化成 { abc: 'aa' }
3 | singleQuote: true, // 单引号
4 | arrowParens: 'avoid', // (x) => x 变成 x => x
5 | trailingComma: 'none', // 不需要尾部逗号
6 | semi: false // 分号结尾
7 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 ChuChencheng
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.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Vue-Tree 4.x
2 |
3 | 简体中文 | [English](https://github.com/wsfe/vue-tree/blob/dev/README_EN.md)
4 |
5 | [接口文档与在线示例](https://wsfe.github.io/vue-tree/)
6 |
7 | 一款高性能 Vue3 虚拟树控件,支持搜索,定位,拖拽等。该控件是在公司业务的基础上不断打磨出来的,提供了十分丰富强大的 API,几乎能够满足你对树控件的所有需求。
8 |
9 | Vue2 版本树组件请使用 [`@wsfe/ctree`](https://github.com/wsfe/vue-tree/tree/2.x)
10 |
--------------------------------------------------------------------------------
/README_EN.md:
--------------------------------------------------------------------------------
1 | # Vue-Tree 4.x
2 |
3 | [简体中文](https://github.com/wsfe/vue-tree) | English
4 |
5 | [API Document & Online Demo](https://wsfe.github.io/vue-tree/en/)
6 |
7 | A high performance Vue3 tree component optimized using virtual list. It supports searching, node locating, drag-and-drop, etc. This component is built based on business, providing rich and powerful APIs which can meet your various needs for a tree component.
8 |
9 | For Vue2 users, please use [`@wsfe/ctree`](https://github.com/wsfe/vue-tree/tree/2.x) (Chinese doc only)
10 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | ['@babel/preset-env', {targets: {node: 'current'}}],
4 | '@babel/preset-typescript',
5 | [
6 | '@vue/app',
7 | {
8 | 'useBuiltIns': false
9 | }
10 | ]
11 | ]
12 | }
13 |
--------------------------------------------------------------------------------
/docs/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | vue tree
8 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/examples/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
70 |
71 |
102 |
--------------------------------------------------------------------------------
/examples/Drag.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
11 | slot 传进来的暂无数据
12 |
13 | {{ value }}
14 |
15 |
16 |
17 |
56 |
--------------------------------------------------------------------------------
/examples/Drop.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
自定义展示 slot :
5 |
17 |
18 |
19 | {{
20 | scope.checkedNodes.map((node: TreeNode) => node.title).join(',')
21 | }}
22 |
23 |
24 |
25 | {{ value }}
26 |
27 |
28 |
默认:
29 |
40 | slot 传进来的暂无数据
41 |
42 | {{ value }}
43 |
44 |
45 |
单选:
46 |
57 | slot 传进来的暂无数据
58 |
59 | 选中的值:{{ value2 }}
60 |
61 |
62 |
63 |
64 |
118 |
--------------------------------------------------------------------------------
/examples/DropDataChange.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
12 | slot 传进来的暂无数据
13 |
14 | {{ value }}
15 |
16 |
17 |
18 |
59 |
--------------------------------------------------------------------------------
/examples/DropRemote.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
14 | slot 传进来的暂无数据
15 |
16 | {{ value }}
17 |
18 |
19 |
20 |
21 |
124 |
--------------------------------------------------------------------------------
/examples/InsertRenderTree.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
43 |
--------------------------------------------------------------------------------
/examples/Loading.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
47 |
--------------------------------------------------------------------------------
/examples/Mobile.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | {}"
12 | selectable
13 | >
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
33 |
34 |
35 |
多选,父节点不能选择
36 | v-model:
37 | {{ checkableValue }}
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
102 |
103 |
--------------------------------------------------------------------------------
/examples/Search.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 | slot 传进来的暂无数据
9 |
10 | 折叠
11 | 展开
12 | slot 按钮
13 |
14 |
15 |
16 |
17 |
57 |
--------------------------------------------------------------------------------
/examples/SearchRemote.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | slot 传进来的暂无数据
6 | {{ value }}
7 |
8 |
9 |
10 |
59 |
--------------------------------------------------------------------------------
/examples/SearchRootRemote.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | slot 传进来的暂无数据
13 | {{ value }}
14 |
15 |
16 |
17 |
65 |
--------------------------------------------------------------------------------
/examples/app.css:
--------------------------------------------------------------------------------
1 | html, body {
2 | width: 100%;
3 | height: 100%;
4 | margin: 0;
5 | padding: 0;
6 | }
7 |
--------------------------------------------------------------------------------
/examples/env.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.vue' {
2 | import type { defineComponent } from 'vue'
3 | const Component: ReturnType
4 | export default Component
5 | }
6 |
--------------------------------------------------------------------------------
/examples/main.ts:
--------------------------------------------------------------------------------
1 | import { createApp } from 'vue'
2 | import App from './App.vue'
3 | import './app.css'
4 |
5 | const app = createApp(App)
6 | app.mount('#app')
7 |
--------------------------------------------------------------------------------
/examples/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "useDefineForClassFields": true,
5 | "module": "ESNext",
6 | "moduleResolution": "Node",
7 | "strict": true,
8 | "jsx": "preserve",
9 | "resolveJsonModule": true,
10 | "isolatedModules": true,
11 | "esModuleInterop": true,
12 | "lib": ["ESNext", "DOM"],
13 | "skipLibCheck": true
14 | },
15 | "include": [
16 | "examples/**/*.ts",
17 | "examples/**/*.d.ts",
18 | "examples/**/*.tsx",
19 | "examples/**/*.vue"
20 | ],
21 | "references": [
22 | { "path": "../tsconfig.json" },
23 | { "path": "./tsconfig.node.json" }
24 | ]
25 | }
26 |
--------------------------------------------------------------------------------
/examples/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "module": "ESNext",
5 | "moduleResolution": "Node",
6 | "allowSyntheticDefaultImports": true
7 | },
8 | "include": ["vite.config.ts"]
9 | }
10 |
--------------------------------------------------------------------------------
/examples/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import vue from '@vitejs/plugin-vue'
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig(({ mode }) => {
6 | return {
7 | base: './',
8 | plugins: [vue()],
9 | build: {
10 | outDir: 'docs'
11 | }
12 | }
13 | })
14 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | vue tree
8 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/markdown/design-tree-search.md:
--------------------------------------------------------------------------------
1 | # 界面结构
2 |
3 | 1. 整体分为上下两部分,上部分为搜索、操作区域;下部分为 Tree
4 | 2. 上部分水平分为三块,分别是:全选勾选框(可能不可见)、搜索输入框、操作按钮(可能不可见)
5 | 3. 下部分为一个高度限定、宽度 100% 的树
6 |
--------------------------------------------------------------------------------
/markdown/design-tree.md:
--------------------------------------------------------------------------------
1 | # 界面结构
2 |
3 | 节点:从左到右分别为 `展开图标`, `复选框`, `标题`
4 |
5 | ```
6 | > 口 title
7 | ```
8 |
9 | 其中展开图标与复选框等宽,分别占用一个正方形位置(宽高相等),并且垂直居中。
10 |
11 | 展开图标位置如果是父节点则显示图标,非父节点则不显示图标但有占位。
12 |
13 | 复选框在非多选状态下不显示且不占位。
14 |
15 | 整个节点宽度需大于等于三个正方形宽度,高度需大于等于一个正方形宽度。
16 |
17 | 整棵树:
18 |
19 | ```
20 | 口 parent-1
21 | > 口 parent-2
22 | 口 child
23 | ```
24 |
25 | 子节点在父节点下方,且相对父节点向后推移一个正方形宽度的距离。
26 |
27 | 整棵树宽高由外层容器控制
28 |
29 | 需要设置一个高度,以实现大量数据的高性能展示。
30 |
31 | (* 如果不设置高度,可以考虑以屏幕高度为最大高度)
32 |
33 | 树整体结构解析:
34 |
35 | 最外层可滚动部分:ScollArea
36 |
37 | 内层为 Block 包括:TopSpace, RenderArea, BottomSpace
38 |
39 | # 功能
40 |
41 | ### 普通树结构 `VTree` 组件
42 |
43 | #### 树查看
44 |
45 | - 可查看展示树结构
46 | - 父节点可展开
47 |
48 | #### 树单选
49 |
50 | - 点击未选中节点则选中该节点
51 | - 点击选中节点则取消选中该节点
52 |
53 | #### 树多选
54 |
55 | - 树标题前面有复选框
56 | - 点击复选框或者标题可选中该节点
57 | - 再次点击则取消选中
58 | - 选中父节点则下属子节点均选中(反之均取消选中)
59 | - 可选父子是否关联,不关联则上一条无效
60 |
61 | #### 节点功能
62 |
63 | - 如果是父节点,最前面有展开图标
64 | - 多选模式下展开图标后面为复选框
65 | - 再后面为节点标题,开发者可自定义标题内容
66 | - 单选选中状态下标题背景高亮
67 | - 多选选中状态下复选框为勾选状态
68 | - 可禁用节点,禁用时复选框、标题均不可点击
69 | - 节点可拖拽
70 |
71 | # 思路
72 |
73 | 考虑到性能以及数据量问题,组件需要尽可能减少递归、搜索次数、DOM复杂度等。大致上是用空间去换取时间,但在内存占用上也要有所控制,尽可能避免 Vue 监听不必要的数据。
74 |
75 | 大方向:
76 |
77 | - DOM 数量以及深度限制
78 | - DOM 扁平化
79 | - 数据扁平化
80 |
81 | 第一条其实就是 clusterize 的思想,只渲染看得见的几条数据,上下用空白元素填充,在数据量大的情况下可以有效减少 DOM 的数量,减少页面卡顿的可能。 DOM 深度的限制则需要对树节点的设计进行优化,尽可能保持简洁,删除不必要的动画、类、样式,减少重绘。
82 |
83 | 后两者实际上是相辅相成的,在数据上实现了扁平化,DOM 自然也会使用扁平化的结构
84 |
85 | # 数据结构
86 |
87 | 外部传入的数据就是一棵树的数据结构,这点基本上无法改变,除非特殊要求传入扁平化后的数据。
88 |
89 | 首先要对传入的树结构进行拍平。
90 |
91 | 在这个过程中需要引入 `level` 字段标明每个节点的层级关系,使用 `parent`, `children` 字段存储每个节点的上下层级关系。
92 |
93 | 同时最好建立一个 hash 表,以唯一的 id 作为键值,可快速定位某个节点。
94 |
95 | 如果是进行树搜索,可考虑以其他结构存储一份数据,加快搜索速度。(或许会比较占用空间,可让开发者决定是否建立这份数据。)
96 |
97 | # 数据流
98 |
99 | 1.
100 |
101 | 外部修改 -> VTree -> TreeStore (-> TreeNode) -> (事件等)通知 VTree -> VTreeNode 视图改变
102 |
103 | 2.
104 |
105 | VTreeNode -> VTree -> TreeStore (-> TreeNode) -> (事件等)通知 VTree -> VTreeNode 视图改变
106 |
107 | # 选中实现流程
108 |
109 | 初始化时,首先执行 `new TreeNode` ,待所有节点都实例化后,进行拍平。
110 |
111 | 1. 拍平期间,进行数据上的勾选,例如对 `checked: true` 的节点执行 `setChecked` 操作
112 | 2. 由于数据源勾选数据与 `value` 勾选的数据不一定是一致的,因此在拍平结束后,针对 `value` 的数据进行勾选
113 | 3. 勾选完 `value` 后,将数据源的勾选与 `value` 的勾选合并,通过 `input` 事件传出去同步 `value` 的值
114 | 4. 设置数据源时与初始化无异,也是执行前三个步骤
115 | 5. `value` 改变时,也可能导致最终选中的 key 不一致(因为开发者可能设置了 ignoreMode,或者勾选的是个父节点),因此也需要触发 `input` 事件
116 |
117 | # value 与 data 改变逻辑
118 |
119 | ## 多选
120 |
121 | 1. 一开始进来时, value 的值全都当做未加载的选中节点,在执行拍平数据后(data 未改变时不执行拍平),遍历 value 数据,将树数据中存在的节点 checked 置为 true
122 | 2. 因为树数据上也可能存在初始就是 checked true 的节点(初始化情况),或者选中的节点与父子节点有级联选中关系(value 改变情况),因此触发一次自定义的 `checked-change` 事件通知 VTree 把最终勾选的结果通过 input 事件同步到 value
123 | 3. value 改变时,先清空当前选中的节点(包括加载与未加载的选中节点),然后重新把 value 的值设置为未加载的选中节点,重复1、2步骤
124 | 4. data 改变时,重复1、2步骤
125 |
126 | ## 单选
127 |
128 | 1. 与多选一样,一开始进来时, value 值当做未加载的选中节点,数据初始化、拍平后,将此节点设置为 selected true ,树数据上设置的 selected true 会被 value 覆盖
129 | 2. 因为单选的 value 有的话一定只有一个,因此不需要触发 input 事件去同步 value 的值
130 | 3. value 改变时,执行选中 value 的值(如果树数据中存在,则直接执行选中,不存在则记录为未加载的选中节点)
131 | 4. data 改变时,重复1、2步骤
132 | 5. 注意:后续异步加载的子节点即使有 selected true 也不会再次覆盖 value 的值,除非 value 没有值
133 |
134 | # 树增删 API
135 |
136 | ```typescript
137 | interface IInsertRemoveAPI {
138 | insertBefore(insertedNode: TreeNodeKeyType | ITreeNodeOptions, referenceKey: TreeNodeKeyType): TreeNode
139 | insertAfter(insertedNode: TreeNodeKeyType | ITreeNodeOptions, referenceKey: TreeNodeKeyType): TreeNode
140 | append(newNodeData: ITreeNodeOptions, parentKey: TreeNodeKeyType): TreeNode
141 | prepend(newNodeData: ITreeNodeOptions, parentKey: TreeNodeKeyType): TreeNode
142 | remove(removedKey: TreeNodeKeyType): void
143 | }
144 | ```
145 |
146 | ## 节点拖拽设计
147 |
148 | ### 节点 DOM 结构
149 |
150 | 在原来的节点前后加上两个 div 用于接收 drop 事件
151 |
152 | 这样,一个节点总体上就被分为三部分
153 |
154 | -节点前
155 | -节点本体
156 | -节点后
157 |
158 | ### 过程中涉及的变化
159 |
160 | 层级:需要更新节点以及其所有子节点的 _level
161 | 父节点:需要给 children 属性添加/删除相应节点
162 | 多选:更新节点后保持被移动节点状态,调用 checkNodeUpward 更新新的父节点状态
163 | 单选: 没有变化
164 |
165 | ### 步骤(用户拖动了节点并 drop):
166 |
167 | 1. 判断 dropzone 节点 drop 的位置是节点前后还是节点本体
168 | 2. 如果是节点前后,则相应调用 `insertBefore`, `insertAfter`
169 | 3. 如果是节点本体,则调用 `prepend`
170 |
171 | ### insertBefore insertAfter
172 |
173 | 步骤:
174 |
175 | 1. 声明变量存储原节点
176 | 2. 从 `flatData`, `mapData` 和 父节点的 `children` 中移除原节点,包括其子节点
177 | 3. 更新被移除处父节点的 `isLeaf` 和 `expand`
178 | 4. 更新被移除处父节点的选中状态
179 | 5. 更新原节点的 `_parent`
180 | 6. 更新原节点的 `_level` ,包括其子节点
181 | 7. 将原节点放置于新的位置(`flatData`, `mapData` 和 父节点的 `children`)
182 | 8. 更新新位置节点其父节点的 `isLeaf` ,并 setExpand 父节点
183 | 9. 更新新位置节点其父节点的选中状态
184 |
185 | 其中,2、3、4 步骤为 `remove` API 的内容,5-9 为 `insertIntoStore` API 的内容,但 `insertIntoStore` API 不打算公开
186 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@wsfe/vue-tree",
3 | "version": "4.1.1",
4 | "types": "./types",
5 | "description": "A vue tree component using virtual list.",
6 | "main": "./dist/vue-tree.umd.js",
7 | "module": "./dist/vue-tree.mjs",
8 | "exports": {
9 | ".": {
10 | "types": "./types/index.d.ts",
11 | "import": "./dist/vue-tree.mjs",
12 | "require": "./dist/vue-tree.umd.js"
13 | },
14 | "./style.css": "./dist/style.css",
15 | "./*": "./*"
16 | },
17 | "scripts": {
18 | "dev": "vite",
19 | "dts": "vue-tsc --declaration --emitDeclarationOnly",
20 | "build": "npm run dts && vite build",
21 | "docs": "vue-tsc --noEmit && vite build -c examples/vite.config.ts",
22 | "preview": "vite preview",
23 | "test": "vitest watch",
24 | "test:ci": "vitest run",
25 | "prettier": "prettier --write \"{src,examples,tests}/**/*.{ts,js,json,vue,tsx,less,scss,less,html}\" --fix",
26 | "prepublishOnly": "npm run build",
27 | "docs:dev": "vitepress dev site",
28 | "docs:build": "vitepress build site",
29 | "docs:preview": "vitepress preview site"
30 | },
31 | "publishConfig": {
32 | "registry": "https://registry.npmjs.org/",
33 | "access": "public"
34 | },
35 | "files": [
36 | "dist",
37 | "src",
38 | "types"
39 | ],
40 | "author": {
41 | "name": "ChuChencheng",
42 | "url": "https://github.com/ChuChencheng"
43 | },
44 | "keywords": [
45 | "vue",
46 | "vue2",
47 | "vue3",
48 | "tree",
49 | "select",
50 | "tree select",
51 | "virtualtree",
52 | "virtual tree",
53 | "virtual-tree",
54 | "vue-tree",
55 | "vue tree",
56 | "vue tree component",
57 | "虚拟树"
58 | ],
59 | "homepage": "https://github.com/wsfe/vue-tree",
60 | "license": "MIT",
61 | "devDependencies": {
62 | "@babel/preset-env": "^7.24.7",
63 | "@babel/preset-typescript": "^7.24.7",
64 | "@faker-js/faker": "^8.4.1",
65 | "@vitejs/plugin-vue": "^5.0.5",
66 | "@vue/babel-preset-app": "^5.0.8",
67 | "@vue/repl": "^4.3.0",
68 | "@vue/test-utils": "^2.4.6",
69 | "@vue/vue3-jest": "^29.2.6",
70 | "autoprefixer": "^10.4.19",
71 | "happy-dom": "^14.12.0",
72 | "less": "^4.2.0",
73 | "postcss": "^8.4.38",
74 | "prettier": "^3.3.1",
75 | "typescript": "^5.4.5",
76 | "vite": "^5.2.13",
77 | "vitepress": "^1.2.3",
78 | "vitest": "^1.6.0",
79 | "vue": "^3.4.30",
80 | "vue-tsc": "^2.0.22"
81 | }
82 | }
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | autoprefixer: {}
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/site/.vitepress/code/ActionsSlot.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Expand All
11 |
12 |
13 |
14 |
15 |
101 |
--------------------------------------------------------------------------------
/site/.vitepress/code/BasicDrop.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
84 |
--------------------------------------------------------------------------------
/site/.vitepress/code/Cascade.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | cascade:
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | Checked Nodes: {{ checked }}
13 |
14 |
15 |
105 |
--------------------------------------------------------------------------------
/site/.vitepress/code/Checkable.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | Checked Nodes: {{ checked }}
4 |
5 |
6 |
87 |
--------------------------------------------------------------------------------
/site/.vitepress/code/CustomDropDisplay.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
12 | {{ scope.checkedNodes.map((node) => node.title).join(',') }}
13 |
14 |
15 |
16 |
17 |
18 |
97 |
--------------------------------------------------------------------------------
/site/.vitepress/code/CustomDropInput.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Click to show drop
10 |
11 |
12 |
13 |
14 |
93 |
--------------------------------------------------------------------------------
/site/.vitepress/code/CustomNode.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ node.title }}
5 |
6 |
7 |
8 |
9 |
88 |
--------------------------------------------------------------------------------
/site/.vitepress/code/DataDisplay.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
84 |
--------------------------------------------------------------------------------
/site/.vitepress/code/DragAndDrop.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
84 |
--------------------------------------------------------------------------------
/site/.vitepress/code/ExpandAnimation.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
84 |
--------------------------------------------------------------------------------
/site/.vitepress/code/IgnoreMode.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | ignoreMode:
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | Checked Nodes: {{ checked }}
16 |
17 |
18 |
109 |
--------------------------------------------------------------------------------
/site/.vitepress/code/LocalSearch.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
86 |
--------------------------------------------------------------------------------
/site/.vitepress/code/NodeCreationAndRemoval.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ node.title }}
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
110 |
111 |
122 |
--------------------------------------------------------------------------------
/site/.vitepress/code/Performance.vue:
--------------------------------------------------------------------------------
1 |
2 |
36 |
37 |
64 |
65 |
66 |
74 |
75 |
76 |
77 |
146 |
147 |
187 |
188 |
220 |
--------------------------------------------------------------------------------
/site/.vitepress/code/ReloadChildren.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
63 |
64 |
73 |
--------------------------------------------------------------------------------
/site/.vitepress/code/Remote.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
28 |
--------------------------------------------------------------------------------
/site/.vitepress/code/RemoteSearch.vue:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
12 |
39 |
--------------------------------------------------------------------------------
/site/.vitepress/code/Selectable.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | Selected Node: {{ selected }}
4 |
5 |
6 |
87 |
--------------------------------------------------------------------------------
/site/.vitepress/code/SelectableAndCheckable.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | v-model: {{ checked }}
4 |
5 |
6 |
87 |
--------------------------------------------------------------------------------
/site/.vitepress/code/ShowLine.vue:
--------------------------------------------------------------------------------
1 |
2 |
29 |
30 |
31 |
32 |
123 |
--------------------------------------------------------------------------------
/site/.vitepress/code/UpdateCustomField.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{ node.title }}
6 |
7 | Count: {{ node.count }}
8 |
9 |
10 |
11 |
12 |
13 |
57 |
58 |
67 |
--------------------------------------------------------------------------------
/site/.vitepress/code/UpdateNodeTitle.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
64 |
65 |
74 |
--------------------------------------------------------------------------------
/site/.vitepress/components/DemoRender.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
12 |
13 |
14 |
15 |
16 |
17 |
24 |
--------------------------------------------------------------------------------
/site/.vitepress/components/Playground.vue:
--------------------------------------------------------------------------------
1 |
2 |
13 |
14 |
15 |
16 |
99 |
100 |
136 |
--------------------------------------------------------------------------------
/site/.vitepress/components/PlaygroundLink.vue:
--------------------------------------------------------------------------------
1 |
2 | {{ text || i18n.openInPlayground }}
3 |
4 |
5 |
51 |
--------------------------------------------------------------------------------
/site/.vitepress/components/VersionSelect.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ name }} Version
5 | {{ modelValue }}
6 | >
7 |
8 |
9 |
10 | - Loading versions...
11 | -
16 | {{ version }}
17 |
18 |
19 |
20 |
21 |
22 |
23 |
66 |
67 |
116 |
--------------------------------------------------------------------------------
/site/.vitepress/components/code-demo.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Loading demo...
6 |
7 |
8 |
9 |
12 |
13 | ::: details {{i18n.showCode}}
14 |
15 | :::
16 |
17 |
35 |
--------------------------------------------------------------------------------
/site/.vitepress/config.mts:
--------------------------------------------------------------------------------
1 | import path from 'node:path'
2 | import { defineConfig } from 'vitepress'
3 | import zh from './zh.mjs'
4 | import en from './en.mjs'
5 |
6 | // https://vitepress.dev/reference/site-config
7 | export default defineConfig({
8 | base: '/vue-tree/',
9 | title: "Vue Tree",
10 | description: "Virtual list optimized Vue tree component",
11 | appearance: false,
12 | head: [
13 | [
14 | 'script',
15 | { async: '', src: 'https://www.googletagmanager.com/gtag/js?id=G-BBCLLNZQ2E' }
16 | ],
17 | [
18 | 'script',
19 | {},
20 | `window.dataLayer = window.dataLayer || [];
21 | function gtag(){dataLayer.push(arguments);}
22 | gtag('js', new Date());
23 | gtag('config', 'G-BBCLLNZQ2E');`
24 | ]
25 | ],
26 | themeConfig: {
27 | // https://vitepress.dev/reference/default-theme-config
28 | search: {
29 | provider: 'local'
30 | },
31 | socialLinks: [
32 | { icon: 'github', link: 'https://github.com/wsfe/vue-tree' }
33 | ]
34 | },
35 | vite: {
36 | resolve: {
37 | alias: {
38 | '@wsfe/vue-tree/style.css': path.resolve('src/styles/index.less'),
39 | '@wsfe/vue-tree': path.resolve('src'),
40 | },
41 | },
42 | },
43 |
44 | locales: {
45 | root: {
46 | label: '简体中文',
47 | ...zh,
48 | },
49 | en: {
50 | label: 'English',
51 | ...en,
52 | },
53 | },
54 | })
55 |
--------------------------------------------------------------------------------
/site/.vitepress/constants/i18n.ts:
--------------------------------------------------------------------------------
1 | export const codeDemoI18n = {
2 | root: {
3 | showCode: '查看代码',
4 | openInPlayground: '在 Playground 打开示例',
5 | },
6 | en: {
7 | showCode: 'Show code',
8 | openInPlayground: 'Open example in Playground',
9 | },
10 | }
11 |
12 | const themeConfigI18n = {
13 | zh: {
14 | guide: '指南',
15 | examples: '示例',
16 | api: '接口文档',
17 |
18 | gettingStarted: '快速开始',
19 | migration: '从旧版迁移',
20 | basicUsage: '基本用法',
21 | performance: '性能测试',
22 | nodeManipulation: '节点操作',
23 | treeSearch: '树搜索',
24 | treeDrop: '树下拉',
25 | },
26 | en: {
27 | guide: 'Guide',
28 | examples: 'Examples',
29 | api: 'API Document',
30 |
31 | gettingStarted: 'Getting Started',
32 | migration: 'Migrate from Old Version',
33 | basicUsage: 'Basic Usage',
34 | performance: 'Performance',
35 | nodeManipulation: 'Node Manipulation',
36 | treeSearch: 'Tree Search',
37 | treeDrop: 'Tree Drop',
38 | },
39 | }
40 |
41 | ;(themeConfigI18n as any).root = themeConfigI18n.zh
42 |
43 | export { themeConfigI18n }
44 |
--------------------------------------------------------------------------------
/site/.vitepress/data/code.data.ts:
--------------------------------------------------------------------------------
1 | import fs from 'node:fs/promises'
2 | import path from 'node:path'
3 | import { createMarkdownRenderer } from 'vitepress'
4 |
5 | export default {
6 | async load() {
7 | const md = await createMarkdownRenderer('')
8 |
9 | const codeDirName = path.resolve(__dirname, '../code')
10 | const fileNameList = await fs.readdir(codeDirName)
11 | const sourceCodeMap: Record = {}
12 | for (let fileName of fileNameList) {
13 | const [, componentName, extension] = fileName.match(/^(.+?)?(?:\.(.+))?$/) || []
14 | if (componentName) {
15 | const sourceCode = (await fs.readFile(path.resolve(codeDirName, fileName), { encoding: 'utf8' })).toString()
16 | sourceCodeMap[componentName] = {
17 | markdown: md.render(`\`\`\`${extension || 'vue'}\n${sourceCode}\n\`\`\``),
18 | sourceCode,
19 | extension,
20 | }
21 | }
22 | }
23 | return sourceCodeMap
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/site/.vitepress/en.mts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vitepress'
2 | import { getLocaleThemeConfig } from './utils/i18n'
3 |
4 | const lang = 'en'
5 |
6 | export default defineConfig({
7 | lang,
8 | themeConfig: getLocaleThemeConfig(lang),
9 | })
10 |
--------------------------------------------------------------------------------
/site/.vitepress/theme/index.ts:
--------------------------------------------------------------------------------
1 | import type { Theme } from 'vitepress'
2 | import DefaultTheme from 'vitepress/theme'
3 |
4 | import CodeDemo from '../components/code-demo.md'
5 | import PlaygroundLink from '../components/PlaygroundLink.vue'
6 | import '@wsfe/vue-tree/style.css'
7 |
8 | export default {
9 | extends: DefaultTheme,
10 | enhanceApp({ app }) {
11 | // 注册自定义全局组件
12 | app.component('CodeDemo', CodeDemo)
13 | app.component('PlaygroundLink', PlaygroundLink)
14 | }
15 | } satisfies Theme
16 |
--------------------------------------------------------------------------------
/site/.vitepress/utils/i18n.ts:
--------------------------------------------------------------------------------
1 | import { themeConfigI18n } from "../constants/i18n"
2 |
3 | export const getLocaleThemeConfig = (lang: string) => {
4 | const pathPrefix = lang === 'root' || lang === 'zh' ? '' : `/${lang}`
5 | const i18nMap = themeConfigI18n[lang]
6 |
7 | return {
8 | nav: [
9 | { text: i18nMap.guide, link: `${pathPrefix}/guide/getting-started`, activeMatch: '/guide' },
10 | { text: i18nMap.examples, link: `${pathPrefix}/examples/tree`, activeMatch: '/examples' },
11 | { text: i18nMap.api, link: `${pathPrefix}/api/vtree`, activeMatch: '/api' },
12 | ],
13 |
14 | sidebar: [
15 | {
16 | text: i18nMap.guide,
17 | items: [
18 | { text: i18nMap.gettingStarted, link: `${pathPrefix}/guide/getting-started` },
19 | { text: i18nMap.migration, link: `${pathPrefix}/guide/migration` },
20 | ]
21 | },
22 | {
23 | text: i18nMap.examples,
24 | items: [
25 | { text: i18nMap.basicUsage, link: `${pathPrefix}/examples/tree` },
26 | { text: i18nMap.performance, link: `${pathPrefix}/examples/performance` },
27 | { text: i18nMap.nodeManipulation, link: `${pathPrefix}/examples/node-manipulation` },
28 | { text: i18nMap.treeSearch, link: `${pathPrefix}/examples/tree-search` },
29 | { text: i18nMap.treeDrop, link: `${pathPrefix}/examples/tree-drop` },
30 | ]
31 | },
32 | {
33 | text: i18nMap.api,
34 | items: [
35 | { text: 'VTree', link: `${pathPrefix}/api/vtree` },
36 | { text: 'VTreeSearch', link: `${pathPrefix}/api/vtree-search` },
37 | { text: 'VTreeDrop', link: `${pathPrefix}/api/vtree-drop` },
38 | ]
39 | },
40 | ],
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/site/.vitepress/zh.mts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vitepress'
2 | import { getLocaleThemeConfig } from './utils/i18n'
3 |
4 | const lang = 'zh'
5 |
6 | export default defineConfig({
7 | lang,
8 | themeConfig: getLocaleThemeConfig(lang),
9 | })
10 |
--------------------------------------------------------------------------------
/site/api/vtree-drop.md:
--------------------------------------------------------------------------------
1 | # VTreeDrop API
2 |
3 | ## VTreeDrop Props
4 |
5 | 注:可在 `VTreeDrop` 上直接使用 `VTree` 和 `VTreeSearch` 的所有 Props
6 |
7 | | 属性 | 说明 | 类型 | 默认值 |
8 | | :------------------------- | :------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------- | :------------- |
9 | | dropHeight | 下拉内容高度 | `number` | 300 |
10 | | dropPlaceholder | 展示输入框 placeholder | `string` | 无 |
11 | | dropDisabled | 是否禁用 | `boolean` | false |
12 | | clearable | 允许清空 | `boolean` | false |
13 | | placement | 下拉弹出框位置,注意!!不支持自动识别方向 | `'bottom-start' \| 'bottom-end' \| 'bottom' \| 'top-start' \| 'top-end' \| 'top'` | 'bottom-start' |
14 | | transfer | 将下拉 DOM 转移到 body 中 | `boolean` | false |
15 | | dropdownClassName | 在下拉框容器上额外添加的 class | `string \| string[]` | 无 |
16 | | dropdownMinWidth `2.0.1` | 下拉框容器最小宽度,未指定则默认为展示输入框宽度。 适合 transfer 为 false 时使用 | `number` | 无 |
17 | | dropdownWidthFixed `2.0.5` | 固定下拉框容器宽度,当内容超出最小宽度不会伸长,而是出现横向滚动条 | `boolean` | false |
18 |
19 | ## VTreeDrop Events
20 |
21 | 注:可在 `VTreeDrop` 上直接监听 `VTree` 和 `VTreeSearch` 的所有 Events
22 |
23 | | 事件名 | 说明 | 返回值 |
24 | | :---------------------- | :--------------------- | :------------- |
25 | | dropdown-visible-change | 下拉框出现或消失时触发 | 下拉框是否可见 |
26 | | clear | 点击清空按钮时触发 | 无 |
27 |
28 | ## VTreeDrop Methods
29 |
30 | 注:可在 `VTreeDrop` 上直接调用 `VTree` 和 `VTreeSearch` 的所有 Methods
31 |
32 | ## VTreeDrop Slots
33 |
34 | 注:可在 `VTreeDrop` 上直接传入 `VTree` 和 `VTreeSearch` 的所有 Slots
35 |
36 | | 名称 | 说明 |
37 | | :------ | :--------------------------------------------------- |
38 | | 默认 | 展示输入框 |
39 | | display | 展示输入框的展示文字,如果有默认 slot 则此 slot 无效 |
40 | | clear | 替换清空图标,如果有默认 slot 则此 slot 无效 |
41 |
42 | 默认 slot 与 display slot 的 Slot Props `2.3.0` :
43 |
44 | ```typescript
45 | /** 展示 slot 的 props */
46 | slotProps: {
47 | /** 多选选中的节点 */
48 | checkedNodes: [] as TreeNode[],
49 |
50 | /** 多选选中的节点 key */
51 | checkedKeys: [] as Array,
52 |
53 | /** 单选选中的节点 */
54 | selectedNode: null as TreeNode | null,
55 |
56 | /** 单选选中的节点 key */
57 | selectedKey: null as string | number | null,
58 | },
59 | ```
60 |
61 | **注意**: `checkedNodes` 与 `selectedNode` 只包含已加载的节点,如果设置了选中的值(比如设置了 `value` Prop),但没有设置树的数据,则这两个字段内容将为空;而 `checkedKeys` 与 `selectedKey` 则会包含未加载的选中节点 key 。
62 |
--------------------------------------------------------------------------------
/site/api/vtree-search.md:
--------------------------------------------------------------------------------
1 | # VTreeSearch API
2 |
3 | ## VTreeSearch Props
4 |
5 | 注:可在 `VTreeSearch` 上直接使用 `VTree` 的所有 Props
6 |
7 | | 属性 | 说明 | 类型 | 默认值 |
8 | | :------------------- | :--------------------------------------------------------------------------------- | :------------------------------------------- | :----------- |
9 | | searchPlaceholder | 搜索输入框的 placeholder | `string` | '搜索关键字' |
10 | | showCheckAll | 是否显示全选复选框 | `boolean` | true |
11 | | showCheckedButton | 是否显示已选按钮 | `boolean` | true |
12 | | checkedButtonText | 已选按钮文字 | `string` | '已选' |
13 | | showFooter | 是否显示底部信息 | `boolean` | true |
14 | | searchMethod `2.0.2` | 如果传入此 Prop ,触发 `search` 事件后将会执行此方法,否则会执行组件内置的搜索方法 | `(keyword: string) => void \| Promise` | 无 |
15 | | searchLength | 触发搜索的字符长度 | `number` | 1 |
16 | | searchDisabled | 禁用搜索功能 | `boolean` | false |
17 | | searchRemote | 是否远程搜索,传入 `searchMethod` 时无效 | `boolean` | false |
18 | | searchDebounceTime | 搜索防抖时间,单位为毫秒 | `number` | 300 |
19 |
20 | ## VTreeSearch Events
21 |
22 | 注:可在 `VTreeSearch` 上直接监听 `VTree` 的所有 Events
23 |
24 | | 事件名 | 说明 | 返回值 |
25 | | :----- | :----------------- | :----------- |
26 | | search | 执行搜索操作时触发 | 搜索的关键字 |
27 |
28 | ## VTreeSearch Methods
29 |
30 | 注:可在 `VTreeSearch` 上直接调用 `VTree` 的所有 Methods
31 |
32 | | 方法 | 说明 | 参数 | 返回值 |
33 | | :----------- | :------------- | :------------------------------------------------------- | :-------------- |
34 | | clearKeyword | 清空关键字 | 无 | `void` |
35 | | getKeyword | 获取搜索关键字 | 无 | `string` |
36 | | search | 执行搜索 | `keyword: string`: 搜索的关键字,默认为内部 this.keyword | `Promise` |
37 |
38 | ## VTreeSearch Slots
39 |
40 | 注:可在 `VTreeSearch` 上直接传入 `VTree` 的所有 Slots
41 |
42 | | 名称 | 说明 |
43 | | :----------- | :------------------------------------------------- |
44 | | search-input | 搜索输入框,可通过此 slot 自行封装树搜索组件的行为 |
45 | | actions | 操作按钮,可在搜索输入框后加入更多操作按钮 |
46 | | footer | 底部信息 |
47 |
48 |
--------------------------------------------------------------------------------
/site/en/api/vtree-drop.md:
--------------------------------------------------------------------------------
1 | # VTreeDrop API
2 |
3 | ## VTreeDrop Props
4 |
5 | Note: You can use all the props of `VTree` and `VTreeSearch` in `VTreeDrop`
6 |
7 | | Prop | Description | Type | Default Value |
8 | | :------------------------- | :-------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------- | :------------- |
9 | | dropHeight | Height of the dropdown | `number` | 300 |
10 | | dropPlaceholder | Placeholder of display input | `string` | None |
11 | | dropDisabled | Whether to disable tree drop | `boolean` | false |
12 | | clearable | Allow to clear | `boolean` | false |
13 | | placement | The position of the dropdown. Note!! Does not support auto adapting directions | `'bottom-start' \| 'bottom-end' \| 'bottom' \| 'top-start' \| 'top-end' \| 'top'` | 'bottom-start' |
14 | | transfer | To transfer DOM to body | `boolean` | false |
15 | | dropdownClassName | Extra class on dropdown container | `string \| string[]` | None |
16 | | dropdownMinWidth `2.0.1` | Min width of the dropdown. The default width is the width of the display input. It's better to use when 'transfer' is false | `number` | None |
17 | | dropdownWidthFixed `2.0.5` | Fix the width of the dropdown. When the content length exceeds the min with of the dropdown, a horizontal scrollbar appears | `boolean` | false |
18 |
19 | ## VTreeDrop Events
20 |
21 | Note: You can listen to all the events of `VTree` and `VTreeSearch` on `VTreeDrop`
22 |
23 | | Event | Description | Return Value |
24 | | :---------------------- | :-------------------------------------- | :------------------------- |
25 | | dropdown-visible-change | Triggers when dropdown shows/hides | Visibility of the dropdown |
26 | | clear | Triggers when click on the clear button | None |
27 |
28 | ## VTreeDrop Methods
29 |
30 | Note: You can use all the methods of `VTree` and `VTreeSearch` in `VTreeDrop`
31 |
32 | ## VTreeDrop Slots
33 |
34 | Note: You can pass all the slots of `VTree` and `VTreeSearch` to `VTreeDrop`
35 |
36 | | Name | Description |
37 | | :------ | :------------------------------------------------------------------------ |
38 | | default | The whole input |
39 | | display | The text of display input. Not effective when the default slot is present |
40 | | clear | To replace the clear icon. Not effective when the default slot is present |
41 |
42 | Slot props of `default` and `display` slots `2.3.0`:
43 |
44 | ```typescript
45 | /** Slot props of display */
46 | slotProps: {
47 | /** Multiple selected nodes */
48 | checkedNodes: [] as TreeNode[],
49 |
50 | /** Multiple selected node keys */
51 | checkedKeys: [] as Array,
52 |
53 | /** Single selected nodes */
54 | selectedNode: null as TreeNode | null,
55 |
56 | /** Single selected node keys */
57 | selectedKey: null as string | number | null,
58 | },
59 | ```
60 |
61 | **Note**: `checkedNodes` and `selectedNode` only include loaded nodes. The content of these two fields will be empty if `value` prop is set but there's no tree data; While `checkedKeys` and `selectedKey` contains node keys that is not yet loaded.
62 |
--------------------------------------------------------------------------------
/site/en/api/vtree-search.md:
--------------------------------------------------------------------------------
1 | # VTreeSearch API
2 |
3 | ## VTreeSearch Props
4 |
5 | Note: You can use all the props of `VTree` in `VTreeSearch`
6 |
7 | | Prop | Description | Type | Default Value |
8 | | :------------------- | :---------------------------------------------------------------------------------------------------------------------- | :------------------------------------------- | :------------ |
9 | | searchPlaceholder | Placeholder of search input | `string` | '搜索关键字' |
10 | | showCheckAll | Whether to show check all checkbox | `boolean` | true |
11 | | showCheckedButton | Whether to show 'Checked' button | `boolean` | true |
12 | | checkedButtonText | Checked button text | `string` | '已选' |
13 | | showFooter | Whether to show footer | `boolean` | true |
14 | | searchMethod `2.0.2` | This method will be invoked when `search` event triggers if present, or else the internal search method will be invoked | `(keyword: string) => void \| Promise` | None |
15 | | searchLength | The length of search text that triggers a search | `number` | 1 |
16 | | searchDisabled | Disable search | `boolean` | false |
17 | | searchRemote | Enable remote search. Not effective when `searchMethod` is present | `boolean` | false |
18 | | searchDebounceTime | Search debounce time. Unit: ms | `number` | 300 |
19 |
20 | ## VTreeSearch Events
21 |
22 | Note: You can listen to all the events of `VTree` on `VTreeSearch`
23 |
24 | | Event | Description | Return Value |
25 | | :----- | :------------------- | :------------- |
26 | | search | Triggers when search | Search keyword |
27 |
28 | ## VTreeSearch Methods
29 |
30 | Note: You can use all the methods of `VTree` in `VTreeSearch`
31 |
32 | | Method | Description | Params | Return Value |
33 | | :----------- | :------------- | :---------------------------------------------------------------------------- | :-------------- |
34 | | clearKeyword | Clear keyword | None | `void` |
35 | | getKeyword | Get keyword | None | `string` |
36 | | search | Execute search | `keyword: string`: Search keyword, the default value is internal this.keyword | `Promise` |
37 |
38 | ## VTreeSearch Slots
39 |
40 | Note: You can pass all the slots of `VTree` to `VTreeSearch`
41 |
42 | | Name | Description |
43 | | :----------- | :------------------------------------------------------------ |
44 | | search-input | Search input |
45 | | actions | Action buttons. Append more action buttons after search input |
46 | | footer | Footer info |
47 |
48 |
--------------------------------------------------------------------------------
/site/en/examples/node-manipulation.md:
--------------------------------------------------------------------------------
1 | # Node Manipulation {#node-manipulation}
2 |
3 | ## Custom Node {#custom-node}
4 |
5 | There are two ways of customizing nodes:
6 |
7 | 1. Use named slot `node`
8 | 2. Use `render` prop
9 |
10 |
11 |
12 | ## Drag and Drop {#drag-and-drop}
13 |
14 | Enable `draggable` and `droppable`
15 |
16 |
17 |
18 | ## Node Creation and Removal {#node-creation-and-removal}
19 |
20 | - Invoke `insertBefore`, `insertAfter` method of tree component to insert new node before and after tree nodes
21 | - Invoke `prepend`, `append` method of tree component to prepend or append to child list
22 | - Invoke `remove` to remove a node
23 |
24 |
25 |
26 | ## Update Node Title {#update-node-title}
27 |
28 | Invoke `updateNode` method to update some fields of tree node
29 |
30 | Invoke `updateNodes` to update multiple nodes
31 |
32 |
33 |
34 | ## Update Custom Field {#update-custom-field}
35 |
36 | Invoke `updateNode` method to update custom fields in tree node
37 |
38 |
39 |
40 | ## Reload Child Nodes {#reload-children}
41 |
42 | Invoke `updateNode` and pass a new `children` list to reload child nodes
43 |
44 |
45 |
--------------------------------------------------------------------------------
/site/en/examples/performance.md:
--------------------------------------------------------------------------------
1 | # Performance {#performance}
2 |
3 | 1. Works fine in Chrome
4 | 2. There's a max height limitation of browser element/document. Issues may occur when the data is way too large
5 | 3. It takes time to generate nodes, so please be aware of the depth of the tree when you config
6 |
7 |
8 |
--------------------------------------------------------------------------------
/site/en/examples/tree-drop.md:
--------------------------------------------------------------------------------
1 | # Tree Drop {#tree-drop}
2 |
3 | ## Basic Drop {#basic-drop}
4 |
5 | Use `VTreeDrop` component to use tree in a dropdown
6 |
7 |
8 |
9 | ## Customize Display of Input Content {#custom-drop-display}
10 |
11 | You can use `display` slot to customize the text value of input
12 |
13 |
14 |
15 | ## Customize the Whole Dropdown Input {#custom-drop-input}
16 |
17 | Use the default slot to replace the whole input component of the dropdown
18 |
19 |
20 |
--------------------------------------------------------------------------------
/site/en/examples/tree-search.md:
--------------------------------------------------------------------------------
1 | # Tree Search {#tree-search}
2 |
3 | ## Local Search {#local-search}
4 |
5 | Use `VTreeSearch` component to search
6 |
7 |
8 |
9 | ## Remote Search {#remote-search}
10 |
11 | Use `searchRemote` along with `load` method to search from remote
12 |
13 |
14 |
15 | ## Actions Slot {#actions-slot}
16 |
17 | Use `actions` slot to customize action buttons
18 |
19 |
20 |
--------------------------------------------------------------------------------
/site/en/examples/tree.md:
--------------------------------------------------------------------------------
1 | # Basic Usage {#basic-usage}
2 |
3 | ## Data Display {#data-display}
4 |
5 | `data` prop can be accepted. To prevent from performance issue, use `setData` when the data is large.
6 |
7 |
8 |
9 | ## Single Select {#selectable}
10 |
11 | Use `selectable` to enable single select
12 |
13 |
14 |
15 | ## Multiple Select {#checkable}
16 |
17 | Use `checkable` to enable multiple select
18 |
19 |
20 |
21 | ## Ignore Specific Nodes {#ignore-mode}
22 |
23 | Parent nodes or child nodes can be ignored in `v-model` and `getCheckedNodes` using `ignoreMode` prop. This prop is effective only when it's set initially.
24 |
25 |
26 |
27 | ## Node Cascade {#cascade}
28 |
29 | Use `cascade` to config if parent and child nodes are cascaded
30 |
31 |
32 |
33 | ## Single and Multiple Select {#selectable-and-checkable}
34 |
35 | When single and multiple select are enabled at the same time, the value of `v-model` is bound to multiple select value
36 |
37 |
38 |
39 | ## Expand Animation {#expand-animation}
40 |
41 | Use `animation` to enable animation when expand/collapse
42 |
43 |
44 |
45 | ## Connecting Line {#show-line}
46 |
47 | Use `showLine` to show lines between nodes
48 |
49 | Apart from `boolean`, the `showLine` prop also takes an object to config the type, width, color and polyline of the line
50 |
51 |
52 |
53 | ## Remote {#remote}
54 |
55 | Config `load` method to load data from remote. `data` prop will be used as root data if present;
56 | If there's no `data`, then `load` will be invoked to load root data with a `null` node parameter
57 |
58 |
59 |
--------------------------------------------------------------------------------
/site/en/guide/getting-started.md:
--------------------------------------------------------------------------------
1 | # Getting Started {#getting-started}
2 |
3 | ## Try It Online {#try-it-online}
4 |
5 | Refer to
to try it online
6 |
7 | ## Install {#install}
8 |
9 | ```bash
10 | # npm
11 | npm install @wsfe/vue-tree
12 |
13 | # yarn
14 | yarn add @wsfe/vue-tree
15 |
16 | # pnpm
17 | pnpm add @wsfe/vue-tree
18 | ```
19 |
20 | ## Import {#import}
21 |
22 | `@wsfe/vue-tree` Provides three components
23 |
24 | ```typescript
25 | import VTree, { VTreeSearch, VTreeDrop } from '@wsfe/vue-tree'
26 | ```
27 |
28 | Import style
29 |
30 | ```css
31 | @import '~@wsfe/vue-tree/style.css';
32 | ```
33 |
34 | Import less to override variables
35 |
36 | ```less
37 | @import '~@wsfe/vue-tree/src/styles/index.less';
38 | ```
39 |
--------------------------------------------------------------------------------
/site/en/guide/migration.md:
--------------------------------------------------------------------------------
1 | # Migrate from Old Version {#migrate-from-old-version}
2 |
3 | ## Vue Support {#vue-support}
4 |
5 | `@wsfe/vue-tree` is the upgraded version of `@wsfe/ctree`. The new version changed the exported module names and Less/CSS prefixes.
6 |
7 | Vue support info:
8 |
9 | - @wsfe/vue-tree 4.x: Vue3
10 | - @wsfe/vue-tree 3.x: Supports Vue2 and Vue3 using `vue-demi`, but it's not under maintenance due to compatibility issues
11 | - @wsfe/ctree: Vue2. Severe bug fixes only
12 |
13 | ## Migration {#migration}
14 |
15 | To migrate from `@wsfe/ctree` or `@wsfe/vue-tree` 3.x to `@wsfe/vue-tree` 4.x, follow these steps:
16 |
17 | 1. Update all Less variables and CSS related class prefix from `ctree` to `vtree`
18 | 2. Change the prefix of exported modules from `C` to `V`
19 |
20 | ```typescript
21 | import CTree, { CTreeSearch, CTreeDrop } from '@wsfe/ctree' // [!code --]
22 | import VTree, { VTreeSearch, VTreeDrop } from '@wsfe/vue-tree' // [!code ++]
23 | ```
24 |
--------------------------------------------------------------------------------
/site/en/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | # https://vitepress.dev/reference/default-theme-home-page
3 | layout: home
4 |
5 | hero:
6 | name: "Vue Tree"
7 | text: "Vue tree component optimized using virtual list"
8 | tagline: 🌲@wsfe/vue-tree
9 | actions:
10 | - theme: brand
11 | text: Getting Started
12 | link: /en/guide/getting-started
13 | - theme: alt
14 | text: API Document
15 | link: /en/api/vtree
16 |
17 | features:
18 | - icon: 🚀
19 | title: Virtual List
20 | details: Optimized using virtual list, handles large data loads
21 | - icon: 📝
22 | title: Rich Features
23 | details: Supports tree display, single select, multiple select, search, dropdown, remote loading and drap-and-drop, etc.
24 | - icon:
25 | title: Vue3
26 | details: Supports Vue3
27 | ---
28 |
29 |
--------------------------------------------------------------------------------
/site/examples/node-manipulation.md:
--------------------------------------------------------------------------------
1 | # 节点操作 {#node-manipulation}
2 |
3 | ## 自定义节点 {#custom-node}
4 |
5 | 有两种自定义节点的方式:
6 |
7 | 1. 传入具名插槽 `node`
8 | 2. 使用 `render` Prop
9 |
10 |
11 |
12 | ## 拖拽 {#drag-and-drop}
13 |
14 | 启用 `draggable` 与 `droppable` 可实现拖拽功能
15 |
16 |
17 |
18 | ## 节点新增与删除 {#node-creation-and-removal}
19 |
20 | - 调用树组件的 `insertBefore`, `insertAfter` 方法,可在节点前后插入新的节点
21 | - 调用树组件的 `prepend`, `append` 方法,可插入新的节点到子节点列表的最前面或最后面
22 | - 调用树组件的 `remove` 方法,可移除节点
23 |
24 |
25 |
26 | ## 更新节点名称 {#update-node-title}
27 |
28 | 调用树组件的 `updateNode` 方法可更新节点部分字段
29 |
30 | 调用 `updateNodes` 可批量更新
31 |
32 |
33 |
34 | ## 更新自定义字段 {#update-custom-field}
35 |
36 | 调用树组件的 `updateNode` 方法更新自定义字段
37 |
38 |
39 |
40 | ## 重新加载子节点 {#reload-children}
41 |
42 | 调用 `updateNode` 传入新的 `children` 列表可以重新加载子节点
43 |
44 |
45 |
--------------------------------------------------------------------------------
/site/examples/performance.md:
--------------------------------------------------------------------------------
1 | # 性能测试 {#performance}
2 |
3 | 1. 在 Chrome 下表现良好
4 | 2. 浏览器元素/文档是有最大高度限制的,过多数据会导致显示不正常
5 | 3. 生成节点比较耗时,请注意节点深度
6 |
7 |
8 |
--------------------------------------------------------------------------------
/site/examples/tree-drop.md:
--------------------------------------------------------------------------------
1 | # 树下拉 {#tree-drop}
2 |
3 | ## 基础下拉 {#basic-drop}
4 |
5 | 使用 `VTreeDrop` 组件可实现树下拉
6 |
7 |
8 |
9 | ## 自定义输入框展示 {#custom-drop-display}
10 |
11 | 如果想自定义展示输入框选中的值,可使用 `display` 插槽
12 |
13 |
14 |
15 | ## 自定义整个输入框 {#custom-drop-input}
16 |
17 | 使用默认插槽可把整个输入框替换掉
18 |
19 |
20 |
--------------------------------------------------------------------------------
/site/examples/tree-search.md:
--------------------------------------------------------------------------------
1 | # 树搜索 {#tree-search}
2 |
3 | ## 本地搜索 {#local-search}
4 |
5 | 使用 `VTreeSearch` 组件可实现树搜索功能
6 |
7 |
8 |
9 | ## 远程搜索 {#remote-search}
10 |
11 | 启用 `searchRemote` 并配合 `load` 方法,可实现远程搜索
12 |
13 |
14 |
15 | ## 操作插槽 {#actions-slot}
16 |
17 | 使用 `actions` 插槽可定制功能按钮
18 |
19 |
20 |
--------------------------------------------------------------------------------
/site/examples/tree.md:
--------------------------------------------------------------------------------
1 | # 基本用法 {#basic-usage}
2 |
3 | ## 数据展示 {#data-display}
4 |
5 | 可直接传入 `data` Prop。当数据量较大时,为了防止 Vue 监听过多数据导致性能问题,可使用 `setData` 方法设置数据。
6 |
7 |
8 |
9 | ## 单选 {#selectable}
10 |
11 | 使用 `selectable` 启用单选功能
12 |
13 |
14 |
15 | ## 多选 {#checkable}
16 |
17 | 使用 `checkable` 启用单选功能
18 |
19 |
20 |
21 | ## 多选忽略特定节点 {#ignore-mode}
22 |
23 | 配置 `ignoreMode` 可在 `v-model` 和 `getCheckedNodes` 方法忽略父节点或者子节点,该 Prop 仅初始设置有效
24 |
25 |
26 |
27 | ## 父子节点级联 {#cascade}
28 |
29 | 配置 `cascade` 可指定父子节点是否级联
30 |
31 |
32 |
33 | ## 单选与多选并存 {#selectable-and-checkable}
34 |
35 | 当既可以单选又可以多选时, `v-model` 绑定的是多选的值
36 |
37 |
38 |
39 | ## 展开动画 {#expand-animation}
40 |
41 | 配置 `animation` 可开启展开收起动画
42 |
43 |
44 |
45 | ## 连接线 {#show-line}
46 |
47 | 配置 `showLine` 可开启连接线
48 |
49 | `showLine` 除了 `boolean`,还可以接收一个对象用于具体配置连接线的类型、宽度、颜色与是否启用折线
50 |
51 |
52 |
53 | ## 远程 {#remote}
54 |
55 | 设置 `load` 方法可以使用远程加载数据,如果有设置 `data`,则 `data` 数据作为根数据;
56 | 如果没有传 `data`,则初始化时调用 `load` 方法载入根数据,其中节点参数为 `null`
57 |
58 |
59 |
--------------------------------------------------------------------------------
/site/guide/getting-started.md:
--------------------------------------------------------------------------------
1 | # 快速开始 {#getting-started}
2 |
3 | ## 在线尝试 {#try-it-online}
4 |
5 |
6 |
7 | ## 安装 {#install}
8 |
9 | ```bash
10 | # npm
11 | npm install @wsfe/vue-tree
12 |
13 | # yarn
14 | yarn add @wsfe/vue-tree
15 |
16 | # pnpm
17 | pnpm add @wsfe/vue-tree
18 | ```
19 |
20 | ## 引入 {#import}
21 |
22 | `@wsfe/vue-tree` 提供了三个组件
23 |
24 | ```typescript
25 | import VTree, { VTreeSearch, VTreeDrop } from '@wsfe/vue-tree'
26 | ```
27 |
28 | 引入样式
29 |
30 | ```css
31 | @import '~@wsfe/vue-tree/style.css';
32 | ```
33 |
34 | 引入 less 以便于变量覆盖
35 |
36 | ```less
37 | @import '~@wsfe/vue-tree/src/styles/index.less';
38 | ```
39 |
--------------------------------------------------------------------------------
/site/guide/migration.md:
--------------------------------------------------------------------------------
1 | # 从旧版迁移 {#migrate-from-old-version}
2 |
3 | ## Vue 支持情况 {#vue-support}
4 |
5 | `@wsfe/vue-tree` 是 `@wsfe/ctree` 的升级版,新旧版本之间除了包的名称,导出的模块与 Less/CSS 相关前缀也有所不同
6 |
7 | 对 Vue 的支持情况如下:
8 |
9 | - @wsfe/vue-tree 4.x: 支持 Vue3
10 | - @wsfe/vue-tree 3.x: 使用 `vue-demi` 同时支持 Vue2 与 Vue3,但存在 Vue2 兼容性问题,后续不会再维护
11 | - @wsfe/ctree: 支持 Vue2,后续仅提供重大 bug 修复
12 |
13 | ## 迁移 {#migration}
14 |
15 | 从 `@wsfe/ctree` 或者 `@wsfe/vue-tree` 3.x 迁移到 `@wsfe/vue-tree` 4.x 需按如下步骤修改:
16 |
17 | 1. 所有 Less 变量与 CSS 相关 class 前缀从 `ctree` 改为 `vtree`
18 | 2. 导出的包前缀从 `C` 改为 `V`,例如:
19 |
20 | ```typescript
21 | import CTree, { CTreeSearch, CTreeDrop } from '@wsfe/ctree' // [!code --]
22 | import VTree, { VTreeSearch, VTreeDrop } from '@wsfe/vue-tree' // [!code ++]
23 | ```
24 |
--------------------------------------------------------------------------------
/site/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | # https://vitepress.dev/reference/default-theme-home-page
3 | layout: home
4 |
5 | hero:
6 | name: "Vue Tree"
7 | text: "使用虚拟列表优化的 Vue 树组件"
8 | tagline: 🌲@wsfe/vue-tree
9 | actions:
10 | - theme: brand
11 | text: 开始使用
12 | link: /guide/getting-started
13 | - theme: alt
14 | text: API 文档
15 | link: /api/vtree
16 |
17 | features:
18 | - icon: 🚀
19 | title: 虚拟列表
20 | details: 使用虚拟列表优化,可加载大量数据
21 | - icon: 📝
22 | title: 丰富特性
23 | details: 支持树形展示,单多选,搜索,下拉,远程加载,拖拽等
24 | - icon:
25 | title: Vue3
26 | details: 支持 Vue3
27 | ---
28 |
29 |
--------------------------------------------------------------------------------
/site/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@wsfe/vue-tree-site",
3 | "version": "0.1.0",
4 | "type": "module"
5 | }
--------------------------------------------------------------------------------
/site/playground.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: page
3 | navbar: false
4 | sidebar: false
5 | title: Playground
6 | ---
7 |
8 |
9 |
10 |
11 |
12 |
19 |
--------------------------------------------------------------------------------
/site/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig, UserConfig } from 'vite'
2 |
3 | export default defineConfig((): UserConfig => {
4 | return {
5 | ssr: {
6 | noExternal: ['@vue/repl'],
7 | },
8 | }
9 | })
10 |
--------------------------------------------------------------------------------
/src/components/LoadingIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
13 |
14 |
15 |
22 |
--------------------------------------------------------------------------------
/src/constants/events.ts:
--------------------------------------------------------------------------------
1 | import { IEventNames } from "../store/tree-event-target"
2 |
3 |
4 | export const TREE_NODE_EVENTS = [
5 | 'expand',
6 | 'check',
7 | 'click',
8 | 'select',
9 | 'node-dblclick',
10 | 'node-right-click',
11 | 'node-dragstart',
12 | 'node-dragenter',
13 | 'node-dragover',
14 | 'node-dragleave',
15 | 'node-drop'
16 | ]
17 |
18 | export const STORE_EVENTS: Array = [
19 | 'expand',
20 | 'select',
21 | 'unselect',
22 | 'selected-change',
23 | 'check',
24 | 'uncheck',
25 | 'checked-change',
26 | 'set-data'
27 | ]
28 |
29 | export const TREE_EVENTS = [...TREE_NODE_EVENTS, ...STORE_EVENTS]
30 |
31 | export const TREE_SEARCH_EVENTS = ['search', ...TREE_EVENTS]
32 |
33 | export const TREE_DROP_EVENTS = [
34 | 'clear',
35 | 'dropdown-visible-change',
36 | ...TREE_SEARCH_EVENTS,
37 | ]
38 |
--------------------------------------------------------------------------------
/src/constants/index.ts:
--------------------------------------------------------------------------------
1 | //#region ignoreMode
2 | export enum ignoreEnum {
3 | none = 'none',
4 | parents = 'parents',
5 | children = 'children'
6 | }
7 | //#endregion ignoreMode
8 |
9 | //#region API
10 | // Tree API
11 | export const TREE_API_METHODS = [
12 | 'setData',
13 | 'setChecked',
14 | 'setCheckedKeys',
15 | 'checkAll',
16 | 'clearChecked',
17 | 'setSelected',
18 | 'clearSelected',
19 | 'setExpand',
20 | 'setExpandKeys',
21 | 'setExpandAll',
22 | 'getCheckedNodes',
23 | 'getCheckedKeys',
24 | 'getIndeterminateNodes',
25 | 'getSelectedNode',
26 | 'getSelectedKey',
27 | 'getExpandNodes',
28 | 'getExpandKeys',
29 | 'getCurrentVisibleNodes',
30 | 'getNode',
31 | 'getTreeData',
32 | 'getFlatData',
33 | 'getNodesCount',
34 | 'insertBefore',
35 | 'insertAfter',
36 | 'append',
37 | 'prepend',
38 | 'remove',
39 | 'filter',
40 | 'showCheckedNodes',
41 | 'loadRootNodes',
42 | 'updateNode',
43 | 'updateNodes',
44 | 'scrollTo'
45 | ] as const
46 |
47 | export const TREE_SEARCH_API_METHODS = [...TREE_API_METHODS, 'clearKeyword', 'getKeyword', 'search'] as const
48 |
49 | export enum placementEnum {
50 | 'bottom-start' = 'bottom-start',
51 | 'bottom-end' = 'bottom-end',
52 | 'bottom' = 'bottom',
53 | 'top-start' = 'top-start',
54 | 'top-end' = 'top-end',
55 | 'top' = 'top'
56 | }
57 |
58 | //#region Scroll position
59 | export enum verticalPositionEnum {
60 | top = 'top',
61 | center = 'center',
62 | bottom = 'bottom'
63 | }
64 |
65 | export type VerticalPositionType = keyof typeof verticalPositionEnum
66 | //#endregion Scroll position
67 |
68 | //#region Drag
69 | export enum dragHoverPartEnum {
70 | before = 'before',
71 | body = 'body',
72 | after = 'after'
73 | }
74 | //#endregion Drag
75 |
76 | export enum showLineType {
77 | // dotted = 'dotted',
78 | dashed = 'dashed',
79 | solid = 'solid',
80 | }
81 |
--------------------------------------------------------------------------------
/src/env.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.vue' {
2 | import type { defineComponent } from 'vue'
3 | const Component: ReturnType
4 | export default Component
5 | }
6 |
--------------------------------------------------------------------------------
/src/hooks/useExpandAnimation.ts:
--------------------------------------------------------------------------------
1 | import { Ref, nextTick, ref } from "vue"
2 | import { TreeNode } from "../store"
3 | import { TreeProps } from "../components/Tree.vue"
4 |
5 | type IUseExpandAnimationProps = Required>
9 |
10 | export const useExpandAnimation = (renderNodesRef: Ref, renderStartRef: Ref, props: IUseExpandAnimationProps) => {
11 | const expandAnimationStart = ref(false)
12 | const expandAnimationReady = ref(false)
13 | const expandNodeIndex = ref(-1)
14 | const expandNodeLevel = ref(-1)
15 | const expandNodeCurrentState = ref(false)
16 | const expandNodeNextState = ref(false)
17 |
18 | const expandRenderStart = ref(0)
19 | const expandTopNodes = ref([])
20 | const expandMiddleNodes = ref([])
21 | const expandBottomNodes = ref([])
22 |
23 | const resetExpandAnimation = () => {
24 | expandAnimationStart.value = false
25 | expandAnimationReady.value = false
26 | expandNodeIndex.value = -1
27 | expandNodeLevel.value = -1
28 |
29 | expandRenderStart.value = 0
30 | expandTopNodes.value = []
31 | expandMiddleNodes.value = []
32 | expandBottomNodes.value = []
33 | }
34 |
35 | const updateMiddleNodes = () => {
36 | const nodeToExpandLevel = expandNodeLevel.value
37 | const middleNodes: TreeNode[] = []
38 | const renderNodesLength = renderNodesRef.value.length
39 | const expandRenderStartDiff = renderStartRef.value - expandRenderStart.value
40 | for (let i = expandNodeIndex.value - expandRenderStartDiff + 1; i < renderNodesLength; i++) {
41 | if (renderNodesRef.value[i]._level > nodeToExpandLevel) {
42 | middleNodes.push(renderNodesRef.value[i])
43 | } else break
44 | }
45 | expandMiddleNodes.value = middleNodes
46 | }
47 |
48 | const updateBeforeExpand = (nodeToExpand: TreeNode) => {
49 | if (!props.animation) return
50 | resetExpandAnimation()
51 |
52 | const key = nodeToExpand[props.keyField]
53 | const index = renderNodesRef.value.findIndex((renderNode) => renderNode[props.keyField] === key)
54 | if (index > -1) {
55 | expandNodeIndex.value = index
56 | expandNodeLevel.value = nodeToExpand._level
57 | expandAnimationStart.value = true
58 | expandNodeCurrentState.value = nodeToExpand.expand
59 | expandNodeNextState.value = !nodeToExpand.expand
60 | expandRenderStart.value = renderStartRef.value
61 |
62 | if (expandNodeNextState.value) {
63 | expandBottomNodes.value = renderNodesRef.value.slice(expandNodeIndex.value + 1)
64 | } else {
65 | updateMiddleNodes()
66 | }
67 | }
68 | }
69 |
70 | const updateAfterExpand = () => {
71 | if (!props.animation) return
72 |
73 | if (!expandAnimationStart.value) {
74 | expandAnimationStart.value = false
75 | return
76 | }
77 |
78 | if (expandNodeIndex.value === -1) return
79 |
80 | nextTick(() => {
81 | const expandRenderStartDiff = renderStartRef.value - expandRenderStart.value
82 | expandTopNodes.value = renderNodesRef.value.slice(0, expandNodeIndex.value - expandRenderStartDiff + 1)
83 | if (expandNodeNextState.value) {
84 | updateMiddleNodes()
85 | } else {
86 | expandBottomNodes.value = renderNodesRef.value.slice(expandNodeIndex.value - expandRenderStartDiff + 1)
87 | }
88 | expandAnimationReady.value = true
89 | nextTick(() => {
90 | expandNodeCurrentState.value = !expandNodeCurrentState.value
91 | })
92 | })
93 | }
94 |
95 | const onExpandAnimationFinish = () => {
96 | resetExpandAnimation()
97 | }
98 |
99 | return {
100 | ready: expandAnimationReady,
101 | currentExpandState: expandNodeCurrentState,
102 |
103 | topNodes: expandTopNodes,
104 | middleNodes: expandMiddleNodes,
105 | bottomNodes: expandBottomNodes,
106 |
107 | updateBeforeExpand,
108 | updateAfterExpand,
109 | onExpandAnimationFinish,
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/src/hooks/useIframeResize.ts:
--------------------------------------------------------------------------------
1 | import { onBeforeUnmount, onMounted, ref } from "vue"
2 |
3 | export const useIframeResize = (resizeCallback: () => void) => {
4 | const iframe = ref()
5 |
6 | onMounted(() => {
7 | const $iframe = iframe.value
8 | if ($iframe?.contentWindow) {
9 | $iframe.contentWindow.addEventListener('resize', resizeCallback)
10 | }
11 | })
12 |
13 | onBeforeUnmount(() => {
14 | const $iframe = iframe.value
15 | if ($iframe?.contentWindow) {
16 | $iframe.contentWindow.removeEventListener('resize', resizeCallback)
17 | }
18 | })
19 | }
20 |
--------------------------------------------------------------------------------
/src/hooks/useTreeCls.ts:
--------------------------------------------------------------------------------
1 | import { computed } from "vue"
2 |
3 | const prefixCls = 'vtree-tree'
4 |
5 | export { prefixCls as TREE_PREFIX_CLS }
6 |
7 | export const useTreeCls = () => {
8 | const wrapperCls = computed(() => {
9 | return [`${prefixCls}__wrapper`]
10 | })
11 | const scrollAreaCls = computed(() => {
12 | return [`${prefixCls}__scroll-area`]
13 | })
14 | const blockAreaCls = computed(() => {
15 | return [`${prefixCls}__block-area`]
16 | })
17 | const emptyCls = computed(() => {
18 | return [`${prefixCls}__empty`]
19 | })
20 | const emptyTextDefaultCls = computed(() => {
21 | return [`${prefixCls}__empty-text_default`]
22 | })
23 | const loadingCls = computed(() => {
24 | return [`${prefixCls}__loading`]
25 | })
26 | const loadingWrapperCls = computed(() => {
27 | return [`${prefixCls}__loading-wrapper`]
28 | })
29 | const loadingIconCls = computed(() => {
30 | return [`${prefixCls}__loading-icon`]
31 | })
32 | const iframeCls = computed(() => {
33 | return [`${prefixCls}__iframe`]
34 | })
35 |
36 | return {
37 | wrapperCls,
38 | scrollAreaCls,
39 | blockAreaCls,
40 | emptyCls,
41 | emptyTextDefaultCls,
42 | loadingCls,
43 | loadingWrapperCls,
44 | loadingIconCls,
45 | iframeCls,
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/hooks/useTreeDropCls.ts:
--------------------------------------------------------------------------------
1 | import { Ref, computed } from 'vue'
2 | import { TREE_SEARCH_PREFIX_CLS as treeSearchPrefixCls } from './useTreeSearchCls'
3 | import { TreeDropProps } from '../components/TreeDrop.vue'
4 |
5 | const prefixCls = 'vtree-tree-drop'
6 |
7 | export { prefixCls as TREE_DROP_PREFIX_CLS }
8 |
9 | export const useTreeDropCls = (props: TreeDropProps, options: {
10 | dropdownVisible: Ref
11 | checkedCount: Ref
12 | selectedTitle: Ref
13 | }) => {
14 | const {
15 | dropdownVisible,
16 | checkedCount,
17 | selectedTitle,
18 | } = options
19 |
20 | const wrapperCls = computed(() => {
21 | return [`${prefixCls}__wrapper`]
22 | })
23 | const referenceCls = computed(() => {
24 | return [`${prefixCls}__reference`]
25 | })
26 |
27 | const displayInputCls = computed(() => {
28 | return [
29 | `${treeSearchPrefixCls}__input`,
30 | `${prefixCls}__display-input`,
31 | {
32 | [`${prefixCls}__display-input_focus`]: dropdownVisible.value,
33 | [`${treeSearchPrefixCls}__input_disabled`]: props.dropDisabled
34 | }
35 | ]
36 | })
37 |
38 | const displayInputTextCls = computed(() => {
39 | let showPlaceholder: boolean = false
40 | if (typeof props.dropPlaceholder === 'string') {
41 | if (props.checkable) showPlaceholder = checkedCount.value === 0
42 | else if (props.selectable) showPlaceholder = selectedTitle.value === ''
43 | }
44 | return [
45 | `${prefixCls}__display-input-text`,
46 | {
47 | [`${prefixCls}__display-input-placeholder`]: showPlaceholder
48 | }
49 | ]
50 | })
51 |
52 | const dropIconCls = computed(() => {
53 | return [
54 | `${prefixCls}__display-icon-drop`,
55 | {
56 | [`${prefixCls}__display-icon-drop_active`]: dropdownVisible.value
57 | }
58 | ]
59 | })
60 |
61 | const clearIconCls = computed(() => {
62 | return [`${prefixCls}__display-icon-clear`]
63 | })
64 |
65 | const dropdownCls = computed(() => {
66 | const extraClassName = Array.isArray(props.dropdownClassName)
67 | ? props.dropdownClassName
68 | : [props.dropdownClassName]
69 | return [`${prefixCls}__dropdown`, ...extraClassName]
70 | })
71 |
72 | return {
73 | wrapperCls,
74 | referenceCls,
75 | displayInputCls,
76 | displayInputTextCls,
77 | dropIconCls,
78 | clearIconCls,
79 | dropdownCls,
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/src/hooks/useTreeNodeCls.ts:
--------------------------------------------------------------------------------
1 | import { Ref, computed } from "vue"
2 | import { TreeNodeProps } from "../components/TreeNode.vue"
3 |
4 | const prefixCls = 'vtree-tree-node'
5 |
6 | export { prefixCls as TREE_NODE_PREFIX_CLS }
7 |
8 | export const useTreeNodeCls = (props: TreeNodeProps, dragoverRefs: {
9 | dragoverBody: Ref
10 | dragoverBefore: Ref
11 | dragoverAfter: Ref
12 | }) => {
13 | const {
14 | dragoverBody,
15 | dragoverBefore,
16 | dragoverAfter,
17 | } = dragoverRefs
18 |
19 | const indentWrapperCls = computed(() => {
20 | return [
21 | `${prefixCls}__indent-wrapper`,
22 | ]
23 | })
24 | const wrapperCls = computed(() => {
25 | return [
26 | `${prefixCls}__wrapper`,
27 | {
28 | [`${prefixCls}__wrapper_is-leaf`]: props.data?.isLeaf,
29 | [`${prefixCls}_disabled`]:
30 | props.disableAll || props.data?.disabled
31 | },
32 | // 复选
33 | {
34 | [`${prefixCls}_checked`]: props.checkable && props.data?.checked,
35 | [`${prefixCls}_indeterminate`]: props.checkable && props.data?.indeterminate
36 | },
37 | // 单选
38 | {
39 | [`${prefixCls}_selected`]: props.data?.selected,
40 | }
41 | ]
42 | })
43 | const nodeBodyCls = computed(() => {
44 | return [
45 | `${prefixCls}__node-body`,
46 | {
47 | [`${prefixCls}__drop_active`]: dragoverBody.value
48 | }
49 | ]
50 | })
51 | const dropBeforeCls = computed(() => {
52 | return [
53 | `${prefixCls}__drop`,
54 | {
55 | [`${prefixCls}__drop_active`]: dragoverBefore.value
56 | }
57 | ]
58 | })
59 | const dropAfterCls = computed(() => {
60 | return [
61 | `${prefixCls}__drop`,
62 | {
63 | [`${prefixCls}__drop_active`]: dragoverAfter.value
64 | }
65 | ]
66 | })
67 | // const squareCls = computed(() => {
68 | // return [`${prefixCls}__square`]
69 | // })
70 | // 复选框图标
71 | const checkboxWrapperCls = computed(() => {
72 | return [`${prefixCls}__square`, `${prefixCls}__checkbox_wrapper`]
73 | })
74 | const expandCls = computed(() => {
75 | return [
76 | `${prefixCls}__square`,
77 | `${prefixCls}__expand`,
78 | {
79 | [`${prefixCls}__expand_active`]: props.data?.expand
80 | }
81 | ]
82 | })
83 | const loadingIconCls = computed(() => {
84 | return [`${prefixCls}__loading-icon`]
85 | })
86 | const checkboxCls = computed(() => {
87 | return [
88 | `${prefixCls}__checkbox`,
89 | {
90 | [`${prefixCls}__checkbox_checked`]: props.data?.checked,
91 | [`${prefixCls}__checkbox_indeterminate`]: props.data?.indeterminate,
92 | [`${prefixCls}__checkbox_disabled`]:
93 | props.disableAll || props.data?.disabled
94 | }
95 | ]
96 | })
97 | const titleCls = computed(() => {
98 | return [
99 | `${prefixCls}__title`,
100 | {
101 | [`${prefixCls}__title_selected`]: props.data?.selected,
102 | [`${prefixCls}__title_disabled`]:
103 | props.disableAll || props.data?.disabled
104 | }
105 | ]
106 | })
107 |
108 | return {
109 | indentWrapperCls,
110 | wrapperCls,
111 | nodeBodyCls,
112 | dropBeforeCls,
113 | dropAfterCls,
114 | checkboxWrapperCls,
115 | expandCls,
116 | loadingIconCls,
117 | checkboxCls,
118 | titleCls,
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/src/hooks/useTreeSearchCls.ts:
--------------------------------------------------------------------------------
1 | import { Ref, computed } from 'vue'
2 | import { TREE_NODE_PREFIX_CLS as treeNodePrefixCls } from './useTreeNodeCls'
3 | import { TreeSearchProps } from '../components/TreeSearch.vue'
4 |
5 | const prefixCls = 'vtree-tree-search'
6 |
7 | export { prefixCls as TREE_SEARCH_PREFIX_CLS }
8 |
9 | export const useTreeSearchCls = (props: TreeSearchProps, options: {
10 | checkAllStatus: {
11 | checked: boolean
12 | indeterminate: boolean
13 | disabled: boolean
14 | }
15 | isShowingChecked: Ref
16 | }) => {
17 | const { checkAllStatus, isShowingChecked } = options
18 |
19 | const wrapperCls = computed(() => {
20 | return [`${prefixCls}__wrapper`]
21 | })
22 | const searchCls = computed(() => {
23 | return [`${prefixCls}__search`]
24 | })
25 | const checkAllWrapperCls = computed(() => {
26 | return [`${prefixCls}__check-all-wrapper`]
27 | })
28 | const checkboxCls = computed(() => {
29 | return [
30 | `${prefixCls}__check-all`,
31 | `${treeNodePrefixCls}__checkbox`,
32 | {
33 | [`${treeNodePrefixCls}__checkbox_checked`]: checkAllStatus.checked,
34 | [`${treeNodePrefixCls}__checkbox_indeterminate`]:
35 | checkAllStatus.indeterminate,
36 | [`${treeNodePrefixCls}__checkbox_disabled`]:
37 | props.searchDisabled || checkAllStatus.disabled
38 | }
39 | ]
40 | })
41 | const inputWrapperCls = computed(() => {
42 | return [`${prefixCls}__input-wrapper`]
43 | })
44 | const inputCls = computed(() => {
45 | return [
46 | `${prefixCls}__input`,
47 | {
48 | [`${prefixCls}__input_disabled`]: props.searchDisabled
49 | }
50 | ]
51 | })
52 | const actionWrapperCls = computed(() => {
53 | return [`${prefixCls}__action-wrapper`]
54 | })
55 | const checkedButtonCls = computed(() => {
56 | return [
57 | `${prefixCls}__checked-button`,
58 | {
59 | [`${prefixCls}__checked-button_active`]: isShowingChecked.value
60 | }
61 | ]
62 | })
63 | const treeWrapperCls = computed(() => {
64 | return [`${prefixCls}__tree-wrapper`]
65 | })
66 | const footerCls = computed(() => {
67 | return [`${prefixCls}__footer`]
68 | })
69 |
70 | return {
71 | wrapperCls,
72 | searchCls,
73 | checkAllWrapperCls,
74 | checkboxCls,
75 | inputWrapperCls,
76 | inputCls,
77 | actionWrapperCls,
78 | checkedButtonCls,
79 | treeWrapperCls,
80 | footerCls,
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/src/hooks/useVirtualList.ts:
--------------------------------------------------------------------------------
1 | import { ref } from "vue"
2 | import { TreeNode } from ".."
3 | import { INonReactiveData, TreeNodeKeyType } from "../types"
4 | import { VerticalPositionType, verticalPositionEnum } from "../constants"
5 | import { TreeProps } from "../components/Tree.vue"
6 |
7 | type IUseVirtualListProps = Required>
13 |
14 | export const useVirtualList = (nonReactive: INonReactiveData, props: IUseVirtualListProps) => {
15 | const scrollArea = ref()
16 | const renderNodes = ref([])
17 | const blockLength = ref(0)
18 | const blockAreaHeight = ref(0)
19 | const topSpaceHeight = ref(0)
20 | const bottomSpaceHeight = ref(0)
21 | const renderAmount = ref(0)
22 | const renderAmountCache = ref(0)
23 | const renderStart = ref(0)
24 | const renderStartCache = ref(0)
25 | const debounceTimer = ref(undefined)
26 |
27 | /**
28 | * 重置空白与滚动高度
29 | */
30 | const resetSpaceHeights = (): void => {
31 | topSpaceHeight.value = 0
32 | bottomSpaceHeight.value = 0
33 | if (scrollArea.value) scrollArea.value.scrollTop = 0
34 | }
35 |
36 | /**
37 | * 计算需要渲染的节点的数量,只要容器高度(clientHeight)不变,这个数量一般就不会变
38 | */
39 | const updateRenderAmount = (): void => {
40 | const clientHeight = scrollArea.value.clientHeight
41 | renderAmount.value = Math.max(
42 | props.renderNodeAmount,
43 | Math.ceil(clientHeight / props.nodeMinHeight) + props.bufferNodeAmount
44 | )
45 | }
46 |
47 | /**
48 | * 计算渲染的节点,基于 scrollTop 计算当前应该渲染哪些节点
49 | */
50 | const updateRenderNodes = (isScroll: boolean = false): void => {
51 | if (blockLength.value > renderAmount.value) {
52 | const scrollTop = Math.max(scrollArea.value.scrollTop, 0)
53 | /** 当前滚动了多少节点 */
54 | const scrollNodeAmount = Math.floor(scrollTop / props.nodeMinHeight)
55 | renderStart.value =
56 | Math.floor(scrollNodeAmount / props.bufferNodeAmount) *
57 | props.bufferNodeAmount
58 | } else {
59 | renderStart.value = 0
60 | }
61 | if (
62 | isScroll &&
63 | renderAmountCache.value === renderAmount.value &&
64 | renderStartCache.value === renderStart.value
65 | )
66 | return
67 | renderNodes.value = nonReactive.blockNodes
68 | .slice(renderStart.value, renderStart.value + renderAmount.value)
69 | .map(blockNode => {
70 | return Object.assign({}, blockNode, {
71 | _parent: null,
72 | children: []
73 | })
74 | }) as TreeNode[]
75 | topSpaceHeight.value = renderStart.value * props.nodeMinHeight
76 | bottomSpaceHeight.value =
77 | blockAreaHeight.value -
78 | (topSpaceHeight.value + renderNodes.value.length * props.nodeMinHeight)
79 | }
80 |
81 | /**
82 | * 计算渲染节点数量,并计算渲染节点
83 | */
84 | const updateRender = (): void => {
85 | updateRenderAmount()
86 | updateRenderNodes()
87 | }
88 |
89 | /**
90 | * 计算可见节点
91 | */
92 | const updateBlockNodes = (): void => {
93 | nonReactive.blockNodes = nonReactive.store.flatData.filter(
94 | node => node.visible
95 | )
96 | updateBlockData()
97 | updateRender()
98 | }
99 |
100 | /**
101 | * 更新 block 数据相关信息
102 | */
103 | const updateBlockData = (): void => {
104 | blockLength.value = nonReactive.blockNodes.length
105 | blockAreaHeight.value = props.nodeMinHeight * blockLength.value
106 | }
107 |
108 | const handleTreeScroll = (): void => {
109 | if (debounceTimer.value) {
110 | window.cancelAnimationFrame(debounceTimer.value)
111 | }
112 | renderAmountCache.value = renderAmount.value
113 | renderStartCache.value = renderStart.value
114 | debounceTimer.value = window.requestAnimationFrame(
115 | updateRenderNodes.bind(null, true)
116 | )
117 | }
118 |
119 | /**
120 | * 滚动到指定节点位置
121 | * @param key 要滚动的节点
122 | * @param verticalPosition 滚动的垂直位置,可选为 'top', 'center', 'bottom' 或距离容器可视顶部距离的数字,默认为 'top'
123 | */
124 | const scrollTo = (
125 | key: TreeNodeKeyType,
126 | verticalPosition: VerticalPositionType | number = verticalPositionEnum.top
127 | ): void => {
128 | const node = nonReactive.store.mapData[key]
129 | if (!node || !node.visible) return
130 | let index: number = -1
131 | for (let i = 0; i < blockLength.value; i++) {
132 | if (nonReactive.blockNodes[i][props.keyField] === key) {
133 | index = i
134 | break
135 | }
136 | }
137 | if (index === -1) return
138 | let scrollTop = index * props.nodeMinHeight
139 | if (verticalPosition === verticalPositionEnum.center) {
140 | const clientHeight = scrollArea.value.clientHeight
141 | scrollTop = scrollTop - (clientHeight - props.nodeMinHeight) / 2
142 | } else if (verticalPosition === verticalPositionEnum.bottom) {
143 | const clientHeight = scrollArea.value.clientHeight
144 | scrollTop = scrollTop - (clientHeight - props.nodeMinHeight)
145 | } else if (typeof verticalPosition === 'number') {
146 | scrollTop = scrollTop - verticalPosition
147 | }
148 | if (scrollArea.value) scrollArea.value.scrollTop = scrollTop
149 | }
150 |
151 | return {
152 | scrollArea,
153 | renderNodes,
154 | blockLength,
155 | blockAreaHeight,
156 | topSpaceHeight,
157 | bottomSpaceHeight,
158 | renderAmount,
159 | renderAmountCache,
160 | renderStart,
161 | renderStartCache,
162 | resetSpaceHeights,
163 | updateRenderAmount,
164 | updateRenderNodes,
165 | updateRender,
166 | updateBlockNodes,
167 | updateBlockData,
168 | handleTreeScroll,
169 | scrollTo,
170 | }
171 | }
172 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import VTree from './components/Tree.vue'
2 | export { default as VTreeNode } from './components/TreeNode.vue'
3 | export { default as VTreeSearch } from './components/TreeSearch.vue'
4 | export { default as VTreeDrop } from './components/TreeDrop.vue'
5 | export { TreeNode } from './store'
6 | export { default as TreeStore } from './store'
7 | import './styles/index.less'
8 | export * from './types'
9 |
10 | export default VTree
11 |
--------------------------------------------------------------------------------
/src/store/index.ts:
--------------------------------------------------------------------------------
1 | import TreeStore from './tree-store'
2 | import TreeNode from './tree-node'
3 |
4 | export default TreeStore
5 |
6 | export { TreeNode }
7 |
--------------------------------------------------------------------------------
/src/store/tree-event-target.ts:
--------------------------------------------------------------------------------
1 | import { TreeNodeKeyType } from '../types'
2 | import TreeNode from './tree-node'
3 |
4 | interface IListenersMap {
5 | [eventName: string]: Function[]
6 | }
7 |
8 | type NodeGeneralListenerType = (node: TreeNode) => void
9 |
10 | export type ListenerType = IEventNames[T]
11 |
12 | export interface IEventNames {
13 | 'set-data': () => void
14 | 'visible-data-change': () => void
15 | 'render-data-change': () => void
16 | expand: NodeGeneralListenerType
17 | select: NodeGeneralListenerType
18 | unselect: NodeGeneralListenerType
19 | 'selected-change': (
20 | node: TreeNode | null,
21 | key: TreeNodeKeyType | null
22 | ) => void
23 | check: NodeGeneralListenerType
24 | uncheck: NodeGeneralListenerType
25 | 'checked-change': (nodes: TreeNode[], keys: TreeNodeKeyType[]) => void
26 | }
27 |
28 | export default class TreeEventTarget {
29 | /** 事件 listeners */
30 | private listenersMap: IListenersMap = {}
31 |
32 | on(
33 | eventName: T,
34 | listener: ListenerType | Array>
35 | ): void {
36 | if (!this.listenersMap[eventName]) {
37 | this.listenersMap[eventName] = []
38 | }
39 | let listeners: Array> = []
40 | if (!Array.isArray(listener)) {
41 | listeners = [listener]
42 | } else {
43 | listeners = listener
44 | }
45 | listeners.forEach(listener => {
46 | if (this.listenersMap[eventName].indexOf(listener) === -1) {
47 | this.listenersMap[eventName].push(listener)
48 | }
49 | })
50 | }
51 |
52 | off(
53 | eventName: T,
54 | listener?: ListenerType
55 | ): void {
56 | if (!this.listenersMap[eventName]) return
57 | if (!listener) {
58 | this.listenersMap[eventName] = []
59 | } else {
60 | const index = this.listenersMap[eventName].indexOf(listener)
61 | if (index > -1) {
62 | this.listenersMap[eventName].splice(index, 1)
63 | }
64 | }
65 | }
66 |
67 | emit(
68 | eventName: T,
69 | ...args: Parameters
70 | ): void {
71 | if (!this.listenersMap[eventName]) return
72 | const length: number = this.listenersMap[eventName].length
73 | for (let i: number = 0; i < length; i++) {
74 | this.listenersMap[eventName][i](...args)
75 | }
76 | }
77 |
78 | disposeListeners(): void {
79 | for (const eventName in this.listenersMap) {
80 | this.listenersMap[eventName] = []
81 | }
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/src/store/tree-node.ts:
--------------------------------------------------------------------------------
1 | import { TreeNodeKeyType } from '../types'
2 |
3 | interface IKeyOption {
4 | [key: string]: TreeNodeKeyType
5 | }
6 |
7 | export interface ITreeNodeOptions extends IKeyOption {
8 | [key: string]: any
9 | }
10 |
11 | const notAllowOverrideList: string[] = [
12 | '_level',
13 | '_filterVisible',
14 | '_parent',
15 | '_loading',
16 | '_loaded',
17 | '_remote',
18 | '_keyField',
19 | 'children',
20 | 'setChildren'
21 | ]
22 |
23 | export default class TreeNode {
24 | //#region Properties
25 |
26 | [key: string]: any | TreeNodeKeyType
27 |
28 | /** 节点层级 */
29 | _level: number = 0
30 |
31 | /** 多选是否选中 */
32 | checked: boolean = false
33 |
34 | /** 单选是否选中 */
35 | selected: boolean = false
36 |
37 | /** 是否半选状态 */
38 | indeterminate: boolean = false
39 |
40 | /** 是否禁用 */
41 | disabled: boolean = false
42 |
43 | /** 是否展开 */
44 | expand: boolean = false
45 |
46 | /** 是否可见 */
47 | visible: boolean = true
48 |
49 | /** 过滤后是否可见,如果为 false 则在其他可见情况下也是不可见的 */
50 | _filterVisible: boolean = true
51 |
52 | /** 父节点 */
53 | _parent: null | TreeNode = null
54 |
55 | /** 子节点 */
56 | children: TreeNode[] = []
57 |
58 | /** 是否是子节点 */
59 | isLeaf: boolean = false
60 |
61 | /** 节点是否正在加载 */
62 | _loading: boolean = false
63 |
64 | /** 子节点是否已加载 */
65 | _loaded: boolean = false
66 |
67 | //#endregion Properties
68 |
69 | constructor(
70 | options: ITreeNodeOptions,
71 | parent: null | TreeNode = null,
72 | readonly _keyField: string = 'id',
73 | readonly _remote: boolean = false
74 | ) {
75 | for (let option in options) {
76 | if (notAllowOverrideList.indexOf(option) === -1) {
77 | this[option] = options[option]
78 | }
79 | }
80 |
81 | if (this[_keyField] == null) {
82 | // 如果没有 id 字段,随机赋值一个
83 | this[_keyField] = Math.random().toString(36).substring(2)
84 | }
85 |
86 | this._parent = parent
87 |
88 | if (this._parent) {
89 | this._level = this._parent._level + 1
90 | }
91 |
92 | this.visible =
93 | this._parent === null || (this._parent.expand && this._parent.visible)
94 |
95 | if (Array.isArray(options.children)) {
96 | this.setChildren(options.children)
97 | }
98 |
99 | if (this.children.length) {
100 | this._loaded = true
101 | }
102 |
103 | if (!this._remote) {
104 | this.isLeaf = !this.children.length
105 | }
106 | }
107 |
108 | /**
109 | * 设置子节点
110 | * @param children 子节点数据数组
111 | */
112 | setChildren(children: ITreeNodeOptions[]): void {
113 | this.children = children.map(child => {
114 | return new TreeNode(
115 | Object.assign({}, child),
116 | this,
117 | this._keyField,
118 | this._remote
119 | )
120 | })
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/src/styles/index.less:
--------------------------------------------------------------------------------
1 | body {
2 | -webkit-tap-highlight-color: transparent; // 为了解决移动端,点击展开icon有凸显背景色的问题
3 | }
4 | @import 'variables';
5 | @import 'loading-icon';
6 | @import 'tree';
7 | @import 'tree-search';
8 | @import 'tree-drop';
9 |
--------------------------------------------------------------------------------
/src/styles/loading-icon.less:
--------------------------------------------------------------------------------
1 | #vtree-loading-icon-styles (@size: 30px, @color: @vtree-color-primary) {
2 | display: inline-block;
3 | width: @size;
4 | height: @size;
5 | animation: vtree-animation-spin 2s linear infinite;
6 |
7 | .@{vtree-prefix}-loading-icon__circle {
8 | stroke: @color;
9 | stroke-linecap: round;
10 | animation: vtree-animation-svg-circle-spin 1.5s ease-in-out infinite;
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/styles/tree-drop.less:
--------------------------------------------------------------------------------
1 | #vtree-tree-drop-styles () {
2 | // prefix
3 | @tree-drop-prefix: ~'@{vtree-prefix}-tree-drop';
4 | @tree-search-prefix: ~'@{vtree-prefix}-tree-search';
5 | @tree-dropdown-prefix: ~'@{vtree-prefix}-dropdown';
6 |
7 | // TreeDrop
8 | .@{tree-drop-prefix} {
9 | // 容器
10 | &__wrapper {
11 | position: relative;
12 | }
13 |
14 | // 下拉框触发区域
15 | &__reference {
16 | cursor: pointer;
17 | }
18 |
19 | // 触发区域
20 | &__display {
21 | // 输入框
22 | &-input {
23 | position: relative;
24 | user-select: none;
25 | padding-right: 20px;
26 |
27 | &-text {
28 | display: block;
29 | height: 100%;
30 | line-height: 22px;
31 | }
32 |
33 | &-placeholder {
34 | color: @vtree-color-input-placeholder;
35 | }
36 |
37 | &_focus {
38 | border-color: @vtree-color-input-border;
39 | box-shadow: 0 0 0 2px fade(@vtree-color-primary, 20%);
40 | }
41 | }
42 |
43 | // 图标
44 | &-icon {
45 | @icon-size: 14px;
46 | .icon () {
47 | position: absolute;
48 | top: 8px;
49 | right: 3px;
50 | width: @icon-size;
51 | height: @icon-size;
52 | box-sizing: border-box;
53 | }
54 |
55 | // 下拉图标
56 | &-drop {
57 | @scale: 0.5;
58 | @translate: -(@icon-size / 2) * @scale;
59 | @transform-rest: scale(@scale) translateX(@translate)
60 | translateY(@translate);
61 |
62 | .icon();
63 | border: 3px solid @vtree-color-sub;
64 | border-top: none;
65 | border-left: none;
66 | transform: rotate(45deg) @transform-rest;
67 | transition: transform 0.2s ease-in-out;
68 |
69 | &_active {
70 | transform: rotate(225deg) @transform-rest;
71 | }
72 | }
73 |
74 | // 清除图标
75 | &-clear {
76 | @scale: 0.9;
77 | .pseudo () {
78 | @thick: 1px;
79 | @offset: ((@icon-size - @thick) / 2);
80 | @diff: 7px;
81 |
82 | content: '';
83 | width: @thick;
84 | height: (@icon-size - @diff);
85 | display: block;
86 | background-color: #fff;
87 | position: absolute;
88 | top: (@diff / 2);
89 | left: @offset;
90 | }
91 |
92 | .icon();
93 | border-radius: 50%;
94 | background-color: @vtree-color-sub;
95 | overflow: hidden;
96 | transform: scale(@scale) rotate(45deg);
97 | display: none;
98 |
99 | &::before {
100 | .pseudo();
101 | }
102 |
103 | &::after {
104 | .pseudo();
105 | transform: rotate(90deg);
106 | }
107 | }
108 | }
109 |
110 | @media(any-hover:hover) {
111 | &-input:hover &-icon-clear {
112 | display: block;
113 | }
114 | }
115 | }
116 |
117 | // 下拉框
118 | &__dropdown {
119 | position: absolute;
120 | top: -999px;
121 | left: -999px;
122 | box-shadow: 0 1px 6px fade(#000, 20%);
123 | border-radius: 4px;
124 | margin: 5px 0;
125 | padding: 5px 0;
126 | transform-origin: center top 0px;
127 | background-color: #fff;
128 | z-index: 9999;
129 |
130 | .@{tree-search-prefix} {
131 | // TreeSearch 搜索区域
132 | &__search {
133 | border-bottom: 1px solid @vtree-color-border;
134 | padding-bottom: 5px;
135 | height: 38px;
136 | }
137 |
138 | // TreeSearch 树区域
139 | &__tree-wrapper {
140 | padding-top: 5px;
141 | }
142 | }
143 | }
144 | }
145 |
146 | // Dropdown animation
147 | .@{tree-dropdown-prefix} {
148 | &-enter-from,
149 | &-leave-to {
150 | opacity: 0;
151 | transform: scaleY(0.8);
152 | }
153 |
154 | &-enter-active,
155 | &-leave-active {
156 | transition: opacity 0.3s, transform 0.3s;
157 | }
158 | }
159 | }
160 |
161 | #vtree-tree-drop-styles();
162 |
--------------------------------------------------------------------------------
/src/styles/tree-search.less:
--------------------------------------------------------------------------------
1 | #vtree-tree-search-styles () {
2 | // prefix
3 | @tree-search-prefix: ~'@{vtree-prefix}-tree-search';
4 |
5 | // TreeSearch
6 | .@{tree-search-prefix} {
7 | @search-height: 42px;
8 |
9 | .search-wrapper () {
10 | // height: @search-height;
11 | display: flex;
12 | align-items: center;
13 | justify-content: center;
14 | }
15 |
16 | // 容器
17 | &__wrapper {
18 | width: 100%;
19 | height: 100%;
20 | position: relative;
21 | display: flex;
22 | flex-direction: column;
23 | }
24 |
25 | // 搜索区域
26 | &__search {
27 | height: @search-height;
28 | display: flex;
29 | box-sizing: border-box;
30 | }
31 |
32 | // 全选框容器
33 | &__check-all-wrapper {
34 | .search-wrapper();
35 | justify-content: initial;
36 | width: 40px;
37 | }
38 |
39 | // 全选框
40 | &__check-all {
41 | margin-left: 23px;
42 | }
43 |
44 | // 搜索框容器
45 | &__input-wrapper {
46 | .search-wrapper();
47 | padding-left: 5px;
48 | flex: 1;
49 | }
50 |
51 | // 搜索框
52 | &__input {
53 | color: @vtree-color-content;
54 | box-sizing: border-box;
55 | width: 100%;
56 | height: 32px;
57 | border: 1px solid @vtree-color-border;
58 | border-radius: 4px;
59 | padding: 4px 7px;
60 | outline: none;
61 | transition: border 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
62 |
63 | &::placeholder {
64 | color: @vtree-color-input-placeholder;
65 | }
66 |
67 | @media(any-hover:hover) {
68 | &:hover {
69 | border-color: @vtree-color-input-border;
70 | }
71 | }
72 |
73 | &:focus {
74 | border-color: @vtree-color-input-border;
75 | box-shadow: 0 0 0 2px fade(@vtree-color-primary, 20%);
76 | }
77 |
78 | &_disabled {
79 | cursor: not-allowed;
80 | color: @vtree-color-input-disabled;
81 | background-color: @vtree-color-input-background-disabled;
82 |
83 | @media(any-hover:hover) {
84 | &:hover {
85 | border-color: @vtree-color-border;
86 | }
87 | }
88 | }
89 | }
90 |
91 | // 按钮容器
92 | &__action-wrapper {
93 | .search-wrapper();
94 | padding: 0 10px;
95 | }
96 |
97 | // 已选按钮
98 | &__checked-button {
99 | cursor: pointer;
100 |
101 | @media(any-hover:hover) {
102 | &:hover {
103 | color: @vtree-color-light-primary;
104 | }
105 | }
106 |
107 | &_active {
108 | color: @vtree-color-primary;
109 | }
110 | }
111 |
112 | // 树区域
113 | &__tree-wrapper {
114 | width: 100%;
115 | flex: 1;
116 | overflow-y: auto;
117 | }
118 |
119 | // 底部信息
120 | &__footer {
121 | padding: 5px;
122 | }
123 | }
124 | }
125 |
126 | #vtree-tree-search-styles();
127 |
--------------------------------------------------------------------------------
/src/styles/variables.less:
--------------------------------------------------------------------------------
1 | @vtree-prefix: ~'vtree';
2 |
3 | // Colors
4 | // Thanks to iviewui
5 | // https://www.iviewui.com/components/color
6 | @vtree-color-primary: #2d8cf0;
7 | @vtree-color-light-primary: #5cadff;
8 | @vtree-color-dark-primary: #2b85e4;
9 |
10 | @vtree-color-info: #2db7f5;
11 | @vtree-color-success: #19be6b;
12 | @vtree-color-warning: #ff9900;
13 | @vtree-color-error: #ed4014;
14 |
15 | @vtree-color-title: #17233d;
16 | @vtree-color-content: #515a6e;
17 | @vtree-color-sub: #808695;
18 | @vtree-color-disabled: #c5c8ce;
19 | @vtree-color-border: #dcdee2;
20 | @vtree-color-divider: #e8eaec;
21 | @vtree-color-background: #f8f8f9;
22 |
23 | @vtree-color-input-border: #57a3f3;
24 | @vtree-color-input-background-disabled: #f3f3f3;
25 | @vtree-color-input-disabled: #ccc;
26 | @vtree-color-input-placeholder: #c5c8ce;
27 |
28 | // Animations
29 | @keyframes vtree-animation-spin {
30 | from {
31 | // 使用 translate3d 强行启用 GPU 加速,缓解 CPU 计算 SVG 的压力
32 | transform: rotate(0deg) translate3d(0, 0, 0);
33 | }
34 | to {
35 | transform: rotate(360deg) translate3d(0, 0, 0);
36 | }
37 | }
38 |
39 | @keyframes vtree-animation-svg-circle-spin {
40 | 0% {
41 | stroke-dasharray: 1 130;
42 | stroke-dashoffset: 0;
43 | }
44 | 50% {
45 | stroke-dasharray: 90 130;
46 | stroke-dashoffset: -30;
47 | }
48 | 100% {
49 | stroke-dasharray: 90 130;
50 | stroke-dashoffset: -124;
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/types/index.ts:
--------------------------------------------------------------------------------
1 | import { placementEnum, ignoreEnum, showLineType } from '../constants'
2 |
3 | import TreeStore, { TreeNode } from '../store'
4 |
5 | export type PlacementType = keyof typeof placementEnum
6 |
7 | export type TreeNodeKeyType = string | number
8 |
9 | export type GetNodeFn = (key: TreeNodeKeyType) => TreeNode | null
10 |
11 | export type IgnoreType = keyof typeof ignoreEnum
12 |
13 | export type LoadFn = (node: null | TreeNode, resolve: Function, reject: Function) => any
14 |
15 | export interface TreeDropSlotProps {
16 | /** 多选选中的节点 */
17 | checkedNodes: TreeNode[]
18 | /** 多选选中的节点 key */
19 | checkedKeys: TreeNodeKeyType[]
20 | /** 单选选中的节点 */
21 | selectedNode?: TreeNode
22 |
23 | /** 单选选中的节点 key */
24 | selectedKey?: TreeNodeKeyType
25 | }
26 |
27 | export type AnyPropsArrayType = Array<{ [key: string]: any }>
28 |
29 | export interface INonReactiveData {
30 | store: TreeStore
31 | blockNodes: TreeNode[]
32 | }
33 |
34 | // Utils to generate types like 1 | 2 | 3
35 | type Enumerate = Acc['length'] extends N
36 | ? Acc[number]
37 | : Enumerate
38 |
39 | type IntRange = Exclude, Enumerate>
40 |
41 | export interface ShowLine {
42 | /** 连接线宽度,svg stroke-width, 默认 1px */
43 | width?: number
44 | type?: showLineType
45 | color?: string
46 | polyline?: boolean
47 | dashDensity?: IntRange<1, 11>
48 | }
49 |
--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------
1 | import { Ref } from "vue";
2 |
3 | export const getVTreeMethods = >(apiMethods: readonly T[], ref: Ref): Pick => {
4 | return apiMethods.reduce((prev, cur) => {
5 | const fn = (...args: any[]) => {
6 | const value = ref.value?.[cur]
7 | if (typeof value !== 'function') return
8 | return value(...args)
9 | }
10 | prev[cur] = fn as K[T]
11 | return prev
12 | }, {} as Pick)
13 | }
14 |
15 | type PickReadonly = {
16 | readonly [P in K]: T[P];
17 | };
18 |
19 | export const pickReadonly = (
20 | obj: Readonly,
21 | ...keys: K[]
22 | ): PickReadonly => {
23 | const picked: Partial> = {}
24 | keys.forEach(key => {
25 | if (obj.hasOwnProperty(key)) {
26 | picked[key] = obj[key]
27 | }
28 | })
29 | return picked as PickReadonly
30 | }
31 |
--------------------------------------------------------------------------------
/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/tests/tree-data-generator.ts:
--------------------------------------------------------------------------------
1 | import { faker } from '@faker-js/faker'
2 |
3 | export interface ITreeNodeData {
4 | title?: string | number
5 | id?: string | number
6 | checked?: boolean
7 | indeterminate?: boolean
8 | selected?: boolean
9 | disabled?: boolean
10 | children?: ITreeNodeData[]
11 | [key: string]: any
12 | }
13 |
14 | interface IGeneratorOptions {
15 | treeDepth?: number
16 | nodesPerLevel?: number
17 | sameIdTitle?: boolean
18 | inOrder?: boolean
19 | forceString?: boolean
20 | useNanoID?: boolean
21 | }
22 |
23 | const genRandomStr = ({ index, useNanoID }: { index?: number; useNanoID?: boolean }): string => {
24 | if (useNanoID) {
25 | return faker.string.nanoid()
26 | }
27 | const randomStr = faker.person.lastName()
28 | return index == null ? randomStr : `${randomStr} - (${index})`
29 | }
30 |
31 | export default ({
32 | treeDepth = 5,
33 | nodesPerLevel,
34 | sameIdTitle = false,
35 | inOrder = false,
36 | forceString = false,
37 | useNanoID = false,
38 | }: IGeneratorOptions = {}): { data: ITreeNodeData[]; total: number } => {
39 | let data: ITreeNodeData[] = []
40 | let total = 0
41 | let orderCount = 0
42 | const genNodeData = (root: ITreeNodeData[], level: number = 0): void => {
43 | if (level >= treeDepth) return
44 | const len: number = nodesPerLevel
45 | ? nodesPerLevel
46 | : Math.floor(Math.random() * 100)
47 | for (let i: number = 0; i < len; i++) {
48 | let title = inOrder ? orderCount : genRandomStr({ index: orderCount })
49 | if (forceString) title = title.toString()
50 | const id = sameIdTitle ? title : genRandomStr({ index: orderCount, useNanoID })
51 | const node = {
52 | title,
53 | id,
54 | children: []
55 | }
56 | root.push(node)
57 | total++
58 | orderCount++
59 | genNodeData(node.children, level + 1)
60 | }
61 | }
62 | genNodeData(data)
63 | return {
64 | data,
65 | total
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/tests/unit/tree-search.spec.ts:
--------------------------------------------------------------------------------
1 | import { describe, it, expect } from 'vitest'
2 |
3 | import { shallowMount, mount } from '@vue/test-utils'
4 | import VTreeSearch from '../../src/components/TreeSearch.vue'
5 | import VTree from '../../src/components/Tree.vue'
6 | import treeDataGenerator, { ITreeNodeData } from '../tree-data-generator'
7 | import TreeStore, { TreeNode } from '../../src/store'
8 |
9 | //#region 通用方法
10 |
11 | const genData = (extra: object = {}) => {
12 | return treeDataGenerator(
13 | Object.assign(
14 | {
15 | treeDepth: 3,
16 | nodesPerLevel: 5,
17 | sameIdTitle: true
18 | },
19 | extra
20 | )
21 | )
22 | }
23 |
24 | const asyncLoadData = (
25 | node: TreeNode | null,
26 | resolve: Function,
27 | reject: Function
28 | ) => {
29 | setTimeout(() => {
30 | let result = [] as any[]
31 | if (node === null) {
32 | result = genData({
33 | treeDepth: 1,
34 | nodesPerLevel: 5,
35 | sameIdTitle: true
36 | }).data
37 | } else {
38 | result = genData({
39 | treeDepth: 1,
40 | nodesPerLevel: 2,
41 | sameIdTitle: true
42 | }).data
43 | }
44 | resolve(result)
45 | }, 100)
46 | }
47 |
48 | //#endregion 通用方法
49 |
50 | describe('树搜索测试', () => {
51 | it('本地搜索', () => new Promise(done => {
52 | const data = genData({ inOrder: true }).data
53 | const wrapper = mount(VTreeSearch as any, {
54 | propsData: { data }
55 | })
56 | const vm = wrapper.vm
57 | const tree = wrapper.findComponent({ ref: 'treeRef' }).vm
58 |
59 | const input = wrapper.find('.vtree-tree-search__input')
60 | const inputElement = input.element as HTMLInputElement
61 | inputElement.value = '30'
62 | input.trigger('input')
63 |
64 | setTimeout(() => {
65 | vm.$nextTick(() => {
66 | expect(
67 | (tree as any).nonReactive.store.flatData
68 | .filter((node: TreeNode) => node.visible)
69 | .map((node: TreeNode) => node.id)
70 | ).toEqual([0, 25, 30, 124, 125, 130])
71 |
72 | inputElement.value = ''
73 | input.trigger('input')
74 |
75 | setTimeout(() => {
76 | vm.$nextTick(() => {
77 | expect(
78 | (tree as any).nonReactive.store.flatData
79 | .filter((node: TreeNode) => node.visible)
80 | .map((node: TreeNode) => node.id)
81 | ).toEqual(
82 | (tree as any).nonReactive.store.flatData.map(
83 | (node: TreeNode) => node.id
84 | )
85 | )
86 |
87 | done()
88 | })
89 | }, 300)
90 | })
91 | }, 300)
92 | }))
93 | })
94 |
95 | describe('树远程搜索增强测试包', () => {
96 | it('远程搜索', () => new Promise(done => {
97 | const times = [3, 2, 5]
98 | let index = 0
99 | const load = (
100 | node: TreeNode | null,
101 | resolve: Function,
102 | reject: Function
103 | ) => {
104 | setTimeout(() => {
105 | const data = genData({
106 | inOrder: true,
107 | forceString: true,
108 | nodesPerLevel: times[index]
109 | }).data
110 | resolve(data)
111 | }, 10)
112 | }
113 | const wrapper = mount(VTreeSearch as any, {
114 | propsData: {
115 | load,
116 | searchRemote: true,
117 | checkable: true,
118 | modelValue: ['93', '124']
119 | },
120 | attrs: {
121 | 'onUpdate:modelValue': (emitValue: Array) => {
122 | wrapper.setProps({ modelValue: emitValue })
123 | }
124 | }
125 | }) as any
126 | const vm = wrapper.vm
127 | const treeWrapper = wrapper.findComponent({ ref: 'treeRef' }) as any
128 |
129 | const input = wrapper.find('.vtree-tree-search__input')
130 | const inputElement = input.element as HTMLInputElement
131 | inputElement.value = '30'
132 |
133 | const initValue = [
134 | '93',
135 | '94',
136 | '95',
137 | '96',
138 | '97',
139 | '98',
140 | '99',
141 | '100',
142 | '101',
143 | '102',
144 | '103',
145 | '104',
146 | '105',
147 | '106',
148 | '107',
149 | '108',
150 | '109',
151 | '110',
152 | '111',
153 | '112',
154 | '113',
155 | '114',
156 | '115',
157 | '116',
158 | '117',
159 | '118',
160 | '119',
161 | '120',
162 | '121',
163 | '122',
164 | '123',
165 | '124',
166 | '125',
167 | '126',
168 | '127',
169 | '128',
170 | '129',
171 | '130',
172 | '131',
173 | '132',
174 | '133',
175 | '134',
176 | '135',
177 | '136',
178 | '137',
179 | '138',
180 | '139',
181 | '140',
182 | '141',
183 | '142',
184 | '143',
185 | '144',
186 | '145',
187 | '146',
188 | '147',
189 | '148',
190 | '149',
191 | '150',
192 | '151',
193 | '152',
194 | '153',
195 | '154'
196 | ]
197 |
198 | setTimeout(() => {
199 | vm.$nextTick(() => {
200 | // index = 0
201 | expect(treeWrapper.props('modelValue')).toEqual(['93', '124'])
202 |
203 | index = 1
204 |
205 | input.trigger('input')
206 |
207 | setTimeout(() => {
208 | vm.$nextTick(() => {
209 | expect(treeWrapper.props('modelValue')).toEqual(['93', '124'])
210 | setTimeout(() => {
211 | vm.setChecked('4', true)
212 | vm.$nextTick(() => {
213 | expect(treeWrapper.vm.modelValue).toEqual([
214 | '4',
215 | '5',
216 | '6',
217 | '93',
218 | '124'
219 | ])
220 |
221 | index = 2
222 |
223 | input.trigger('input')
224 |
225 | setTimeout(() => {
226 | vm.$nextTick(() => {
227 | // 5
228 | expect(treeWrapper.props('modelValue')).toEqual(
229 | ['4', '5', '6'].concat(initValue)
230 | )
231 |
232 | done()
233 | })
234 | }, 340)
235 | })
236 | }, 50)
237 | })
238 | }, 320)
239 | })
240 | }, 15)
241 | }))
242 | })
243 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "target": "ESNext",
5 | "useDefineForClassFields": true,
6 | "module": "ESNext",
7 | "moduleResolution": "Node",
8 | "strict": true,
9 | "noImplicitAny": true,
10 | "jsx": "preserve",
11 | "jsxImportSource": "vue",
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "esModuleInterop": true,
15 | "lib": ["ESNext", "DOM"],
16 | "skipLibCheck": true,
17 | "declaration": true,
18 | "rootDir": "./src",
19 | "declarationDir": "types"
20 | },
21 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
22 | "references": [{ "path": "./tsconfig.node.json" }]
23 | }
24 |
--------------------------------------------------------------------------------
/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "module": "ESNext",
5 | "moduleResolution": "Node",
6 | "allowSyntheticDefaultImports": true
7 | },
8 | "include": ["vite.config.ts"]
9 | }
10 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "defaultSeverity": "error",
3 | "extends": [
4 | "tslint:recommended"
5 | ],
6 | "linterOptions": {
7 | "exclude": [
8 | "node_modules/**"
9 | ]
10 | },
11 | "rules": {
12 | "quotemark": [true, "single"],
13 | "indent": [true, "spaces", 2],
14 | "interface-name": false,
15 | "ordered-imports": false,
16 | "object-literal-sort-keys": false,
17 | "no-consecutive-blank-lines": false,
18 | "semicolon": [true, "never"],
19 | "space-before-function-paren": false,
20 | "curly": false,
21 | "no-unused-expression": false,
22 | "max-line-length": false,
23 | "member-access": false,
24 | "forin": false,
25 | "prefer-const": false,
26 | "no-shadowed-variable": false,
27 | "object-literal-shorthand": false,
28 | "member-ordering": false,
29 | "no-empty": false,
30 | "variable-name": false,
31 | "ban-types": false
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/types/components/LoadingIcon.vue.d.ts:
--------------------------------------------------------------------------------
1 | declare const _default: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly>, {}, {}>;
2 | export default _default;
3 |
--------------------------------------------------------------------------------
/types/components/TreeNode.vue.d.ts:
--------------------------------------------------------------------------------
1 | import { TreeNode } from '../store';
2 | import { TreeProps } from './Tree.vue';
3 | import { GetNodeFn } from '../types';
4 | type PickedProps = Required> & Pick;
5 | export type TreeNodeProps = PickedProps & {
6 | data: TreeNode;
7 | getNode: GetNodeFn;
8 | noSiblingNodeMap: Record;
9 | };
10 | declare const _default: __VLS_WithTemplateSlots, {}, unknown, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
11 | [x: string]: (...args: any[]) => void;
12 | }, string, import("vue").PublicProps, Readonly>>, {}, {}>, {
13 | default?(_: {
14 | node: TreeNode;
15 | }): any;
16 | }>;
17 | export default _default;
18 | type __VLS_WithTemplateSlots = T & {
19 | new (): {
20 | $slots: S;
21 | };
22 | };
23 | type __VLS_NonUndefinedable = T extends undefined ? never : T;
24 | type __VLS_TypePropsToOption = {
25 | [K in keyof T]-?: {} extends Pick ? {
26 | type: import('vue').PropType<__VLS_NonUndefinedable>;
27 | } : {
28 | type: import('vue').PropType;
29 | required: true;
30 | };
31 | };
32 |
--------------------------------------------------------------------------------
/types/const.d.ts:
--------------------------------------------------------------------------------
1 | import { IEventNames } from "./store/tree-store";
2 | export declare enum ignoreEnum {
3 | none = "none",
4 | parents = "parents",
5 | children = "children"
6 | }
7 | export declare const TREE_API_METHODS: readonly ["setData", "setChecked", "setCheckedKeys", "checkAll", "clearChecked", "setSelected", "clearSelected", "setExpand", "setExpandKeys", "setExpandAll", "getCheckedNodes", "getCheckedKeys", "getIndeterminateNodes", "getSelectedNode", "getSelectedKey", "getExpandNodes", "getExpandKeys", "getCurrentVisibleNodes", "getNode", "getTreeData", "getFlatData", "getNodesCount", "insertBefore", "insertAfter", "append", "prepend", "remove", "filter", "showCheckedNodes", "loadRootNodes", "scrollTo"];
8 | export declare const TREE_SEARCH_API_METHODS: readonly ["setData", "setChecked", "setCheckedKeys", "checkAll", "clearChecked", "setSelected", "clearSelected", "setExpand", "setExpandKeys", "setExpandAll", "getCheckedNodes", "getCheckedKeys", "getIndeterminateNodes", "getSelectedNode", "getSelectedKey", "getExpandNodes", "getExpandKeys", "getCurrentVisibleNodes", "getNode", "getTreeData", "getFlatData", "getNodesCount", "insertBefore", "insertAfter", "append", "prepend", "remove", "filter", "showCheckedNodes", "loadRootNodes", "scrollTo", "clearKeyword", "getKeyword", "search"];
9 | export declare enum placementEnum {
10 | 'bottom-start' = "bottom-start",
11 | 'bottom-end' = "bottom-end",
12 | 'bottom' = "bottom",
13 | 'top-start' = "top-start",
14 | 'top-end' = "top-end",
15 | 'top' = "top"
16 | }
17 | export declare enum verticalPositionEnum {
18 | top = "top",
19 | center = "center",
20 | bottom = "bottom"
21 | }
22 | export type VerticalPositionType = keyof typeof verticalPositionEnum;
23 | export declare enum dragHoverPartEnum {
24 | before = "before",
25 | body = "body",
26 | after = "after"
27 | }
28 | export declare const TREE_NODE_EVENTS: string[];
29 | export declare const storeEvents: Array;
30 |
--------------------------------------------------------------------------------
/types/constants/events.d.ts:
--------------------------------------------------------------------------------
1 | import { IEventNames } from "../store/tree-event-target";
2 | export declare const TREE_NODE_EVENTS: string[];
3 | export declare const STORE_EVENTS: Array;
4 | export declare const TREE_EVENTS: string[];
5 | export declare const TREE_SEARCH_EVENTS: string[];
6 | export declare const TREE_DROP_EVENTS: string[];
7 |
--------------------------------------------------------------------------------
/types/constants/index.d.ts:
--------------------------------------------------------------------------------
1 | export declare enum ignoreEnum {
2 | none = "none",
3 | parents = "parents",
4 | children = "children"
5 | }
6 | export declare const TREE_API_METHODS: readonly ["setData", "setChecked", "setCheckedKeys", "checkAll", "clearChecked", "setSelected", "clearSelected", "setExpand", "setExpandKeys", "setExpandAll", "getCheckedNodes", "getCheckedKeys", "getIndeterminateNodes", "getSelectedNode", "getSelectedKey", "getExpandNodes", "getExpandKeys", "getCurrentVisibleNodes", "getNode", "getTreeData", "getFlatData", "getNodesCount", "insertBefore", "insertAfter", "append", "prepend", "remove", "filter", "showCheckedNodes", "loadRootNodes", "updateNode", "updateNodes", "scrollTo"];
7 | export declare const TREE_SEARCH_API_METHODS: readonly ["setData", "setChecked", "setCheckedKeys", "checkAll", "clearChecked", "setSelected", "clearSelected", "setExpand", "setExpandKeys", "setExpandAll", "getCheckedNodes", "getCheckedKeys", "getIndeterminateNodes", "getSelectedNode", "getSelectedKey", "getExpandNodes", "getExpandKeys", "getCurrentVisibleNodes", "getNode", "getTreeData", "getFlatData", "getNodesCount", "insertBefore", "insertAfter", "append", "prepend", "remove", "filter", "showCheckedNodes", "loadRootNodes", "updateNode", "updateNodes", "scrollTo", "clearKeyword", "getKeyword", "search"];
8 | export declare enum placementEnum {
9 | 'bottom-start' = "bottom-start",
10 | 'bottom-end' = "bottom-end",
11 | 'bottom' = "bottom",
12 | 'top-start' = "top-start",
13 | 'top-end' = "top-end",
14 | 'top' = "top"
15 | }
16 | export declare enum verticalPositionEnum {
17 | top = "top",
18 | center = "center",
19 | bottom = "bottom"
20 | }
21 | export type VerticalPositionType = keyof typeof verticalPositionEnum;
22 | export declare enum dragHoverPartEnum {
23 | before = "before",
24 | body = "body",
25 | after = "after"
26 | }
27 | export declare enum showLineType {
28 | dashed = "dashed",
29 | solid = "solid"
30 | }
31 |
--------------------------------------------------------------------------------
/types/hooks/useExpandAnimation.d.ts:
--------------------------------------------------------------------------------
1 | import { Ref } from "vue";
2 | import { TreeNode } from "../store";
3 | import { TreeProps } from "../components/Tree.vue";
4 | type IUseExpandAnimationProps = Required>;
5 | export declare const useExpandAnimation: (renderNodesRef: Ref, renderStartRef: Ref, props: IUseExpandAnimationProps) => {
6 | ready: Ref;
7 | currentExpandState: Ref;
8 | topNodes: Ref<{
9 | [x: string]: any;
10 | _level: number;
11 | checked: boolean;
12 | selected: boolean;
13 | indeterminate: boolean;
14 | disabled: boolean;
15 | expand: boolean;
16 | visible: boolean;
17 | _filterVisible: boolean;
18 | _parent: any | null;
19 | children: any[];
20 | isLeaf: boolean;
21 | _loading: boolean;
22 | _loaded: boolean;
23 | readonly _keyField: string;
24 | readonly _remote: boolean;
25 | setChildren: (children: import("../store/tree-node").ITreeNodeOptions[]) => void;
26 | }[]>;
27 | middleNodes: Ref<{
28 | [x: string]: any;
29 | _level: number;
30 | checked: boolean;
31 | selected: boolean;
32 | indeterminate: boolean;
33 | disabled: boolean;
34 | expand: boolean;
35 | visible: boolean;
36 | _filterVisible: boolean;
37 | _parent: any | null;
38 | children: any[];
39 | isLeaf: boolean;
40 | _loading: boolean;
41 | _loaded: boolean;
42 | readonly _keyField: string;
43 | readonly _remote: boolean;
44 | setChildren: (children: import("../store/tree-node").ITreeNodeOptions[]) => void;
45 | }[]>;
46 | bottomNodes: Ref<{
47 | [x: string]: any;
48 | _level: number;
49 | checked: boolean;
50 | selected: boolean;
51 | indeterminate: boolean;
52 | disabled: boolean;
53 | expand: boolean;
54 | visible: boolean;
55 | _filterVisible: boolean;
56 | _parent: any | null;
57 | children: any[];
58 | isLeaf: boolean;
59 | _loading: boolean;
60 | _loaded: boolean;
61 | readonly _keyField: string;
62 | readonly _remote: boolean;
63 | setChildren: (children: import("../store/tree-node").ITreeNodeOptions[]) => void;
64 | }[]>;
65 | updateBeforeExpand: (nodeToExpand: TreeNode) => void;
66 | updateAfterExpand: () => void;
67 | onExpandAnimationFinish: () => void;
68 | };
69 | export {};
70 |
--------------------------------------------------------------------------------
/types/hooks/useIframeResize.d.ts:
--------------------------------------------------------------------------------
1 | export declare const useIframeResize: (resizeCallback: () => void) => void;
2 |
--------------------------------------------------------------------------------
/types/hooks/usePublicTreeAPI.d.ts:
--------------------------------------------------------------------------------
1 | import { AnyPropsArrayType, INonReactiveData, IgnoreType, TreeNodeKeyType } from "../types";
2 | import { TreeNode } from "..";
3 | import { ITreeNodeOptions } from "../store/tree-node";
4 | import { FilterFunctionType } from "../store/tree-store";
5 | import { TreeProps } from "../components/Tree.vue";
6 | type IUsePublicTreeAPIProps = Required> & Pick;
7 | export declare const usePublicTreeAPI: (nonReactive: INonReactiveData, props: IUsePublicTreeAPIProps, options: {
8 | resetSpaceHeights: () => void;
9 | updateExpandedKeys: () => void;
10 | updateBlockData: () => void;
11 | updateRender: () => void;
12 | }) => {
13 | unloadCheckedNodes: import("vue").Ref<{
14 | [x: string]: any;
15 | _level: number;
16 | checked: boolean;
17 | selected: boolean;
18 | indeterminate: boolean;
19 | disabled: boolean;
20 | expand: boolean;
21 | visible: boolean;
22 | _filterVisible: boolean;
23 | _parent: any | null;
24 | children: any[];
25 | isLeaf: boolean;
26 | _loading: boolean;
27 | _loaded: boolean;
28 | readonly _keyField: string;
29 | readonly _remote: boolean;
30 | setChildren: (children: ITreeNodeOptions[]) => void;
31 | }[]>;
32 | isRootLoading: import("vue").Ref;
33 | setData: (data: AnyPropsArrayType) => void;
34 | setChecked: (key: TreeNodeKeyType, value: boolean) => void;
35 | setCheckedKeys: (keys: TreeNodeKeyType[], value: boolean) => void;
36 | checkAll: () => void;
37 | clearChecked: () => void;
38 | setSelected: (key: TreeNodeKeyType, value: boolean) => void;
39 | clearSelected: () => void;
40 | setExpand: (key: TreeNodeKeyType, value: boolean, expandParent?: boolean) => void;
41 | setExpandKeys: (keys: TreeNodeKeyType[], value: boolean) => void;
42 | setExpandAll: (value: boolean) => void;
43 | getCheckedNodes: (ignoreMode?: IgnoreType) => TreeNode[];
44 | getCheckedKeys: (ignoreMode?: IgnoreType) => TreeNodeKeyType[];
45 | getIndeterminateNodes: () => TreeNode[];
46 | getSelectedNode: () => TreeNode | null;
47 | getSelectedKey: () => TreeNodeKeyType | null;
48 | getExpandNodes: () => TreeNode[];
49 | getExpandKeys: () => TreeNodeKeyType[];
50 | getCurrentVisibleNodes: () => TreeNode[];
51 | getNode: (key: TreeNodeKeyType) => TreeNode | null;
52 | getTreeData: () => TreeNode[];
53 | getFlatData: () => TreeNode[];
54 | getNodesCount: () => number;
55 | insertBefore: (insertedNode: TreeNodeKeyType | ITreeNodeOptions, referenceKey: TreeNodeKeyType) => TreeNode | null;
56 | insertAfter: (insertedNode: TreeNodeKeyType | ITreeNodeOptions, referenceKey: TreeNodeKeyType) => TreeNode | null;
57 | append: (insertedNode: TreeNodeKeyType | ITreeNodeOptions, parentKey: TreeNodeKeyType) => TreeNode | null;
58 | prepend: (insertedNode: TreeNodeKeyType | ITreeNodeOptions, parentKey: TreeNodeKeyType) => TreeNode | null;
59 | remove: (removedKey: TreeNodeKeyType) => TreeNode | null;
60 | filter: (keyword: string, filterMethod?: FilterFunctionType) => void;
61 | showCheckedNodes: (showUnloadCheckedNodes?: boolean) => void;
62 | loadRootNodes: () => Promise;
63 | updateNode: (key: TreeNodeKeyType, newNode: ITreeNodeOptions) => void;
64 | updateNodes: (newNodes: ITreeNodeOptions[]) => void;
65 | };
66 | export {};
67 |
--------------------------------------------------------------------------------
/types/hooks/useTreeCls.d.ts:
--------------------------------------------------------------------------------
1 | declare const prefixCls = "vtree-tree";
2 | export { prefixCls as TREE_PREFIX_CLS };
3 | export declare const useTreeCls: () => {
4 | wrapperCls: import("vue").ComputedRef;
5 | scrollAreaCls: import("vue").ComputedRef;
6 | blockAreaCls: import("vue").ComputedRef;
7 | emptyCls: import("vue").ComputedRef;
8 | emptyTextDefaultCls: import("vue").ComputedRef;
9 | loadingCls: import("vue").ComputedRef;
10 | loadingWrapperCls: import("vue").ComputedRef;
11 | loadingIconCls: import("vue").ComputedRef;
12 | iframeCls: import("vue").ComputedRef;
13 | };
14 |
--------------------------------------------------------------------------------
/types/hooks/useTreeDropCls.d.ts:
--------------------------------------------------------------------------------
1 | import { Ref } from 'vue';
2 | import { TreeDropProps } from '../components/TreeDrop.vue';
3 | declare const prefixCls = "vtree-tree-drop";
4 | export { prefixCls as TREE_DROP_PREFIX_CLS };
5 | export declare const useTreeDropCls: (props: TreeDropProps, options: {
6 | dropdownVisible: Ref;
7 | checkedCount: Ref;
8 | selectedTitle: Ref;
9 | }) => {
10 | wrapperCls: import("vue").ComputedRef;
11 | referenceCls: import("vue").ComputedRef;
12 | displayInputCls: import("vue").ComputedRef<(string | {
13 | "vtree-tree-drop__display-input_focus": boolean;
14 | "vtree-tree-search__input_disabled": boolean | undefined;
15 | })[]>;
16 | displayInputTextCls: import("vue").ComputedRef<(string | {
17 | "vtree-tree-drop__display-input-placeholder": boolean;
18 | })[]>;
19 | dropIconCls: import("vue").ComputedRef<(string | {
20 | "vtree-tree-drop__display-icon-drop_active": boolean;
21 | })[]>;
22 | clearIconCls: import("vue").ComputedRef;
23 | dropdownCls: import("vue").ComputedRef<(string | undefined)[]>;
24 | };
25 |
--------------------------------------------------------------------------------
/types/hooks/useTreeNodeCls.d.ts:
--------------------------------------------------------------------------------
1 | import { Ref } from "vue";
2 | import { TreeNodeProps } from "../components/TreeNode.vue";
3 | declare const prefixCls = "vtree-tree-node";
4 | export { prefixCls as TREE_NODE_PREFIX_CLS };
5 | export declare const useTreeNodeCls: (props: TreeNodeProps, dragoverRefs: {
6 | dragoverBody: Ref;
7 | dragoverBefore: Ref;
8 | dragoverAfter: Ref;
9 | }) => {
10 | indentWrapperCls: import("vue").ComputedRef;
11 | wrapperCls: import("vue").ComputedRef<(string | {
12 | "vtree-tree-node__wrapper_is-leaf": boolean;
13 | "vtree-tree-node_disabled": boolean;
14 | "vtree-tree-node_checked"?: undefined;
15 | "vtree-tree-node_indeterminate"?: undefined;
16 | "vtree-tree-node_selected"?: undefined;
17 | } | {
18 | "vtree-tree-node_checked": boolean;
19 | "vtree-tree-node_indeterminate": boolean;
20 | "vtree-tree-node__wrapper_is-leaf"?: undefined;
21 | "vtree-tree-node_disabled"?: undefined;
22 | "vtree-tree-node_selected"?: undefined;
23 | } | {
24 | "vtree-tree-node_selected": boolean;
25 | "vtree-tree-node__wrapper_is-leaf"?: undefined;
26 | "vtree-tree-node_disabled"?: undefined;
27 | "vtree-tree-node_checked"?: undefined;
28 | "vtree-tree-node_indeterminate"?: undefined;
29 | })[]>;
30 | nodeBodyCls: import("vue").ComputedRef<(string | {
31 | "vtree-tree-node__drop_active": boolean;
32 | })[]>;
33 | dropBeforeCls: import("vue").ComputedRef<(string | {
34 | "vtree-tree-node__drop_active": boolean;
35 | })[]>;
36 | dropAfterCls: import("vue").ComputedRef<(string | {
37 | "vtree-tree-node__drop_active": boolean;
38 | })[]>;
39 | checkboxWrapperCls: import("vue").ComputedRef;
40 | expandCls: import("vue").ComputedRef<(string | {
41 | "vtree-tree-node__expand_active": boolean;
42 | })[]>;
43 | loadingIconCls: import("vue").ComputedRef;
44 | checkboxCls: import("vue").ComputedRef<(string | {
45 | "vtree-tree-node__checkbox_checked": boolean;
46 | "vtree-tree-node__checkbox_indeterminate": boolean;
47 | "vtree-tree-node__checkbox_disabled": boolean;
48 | })[]>;
49 | titleCls: import("vue").ComputedRef<(string | {
50 | "vtree-tree-node__title_selected": boolean;
51 | "vtree-tree-node__title_disabled": boolean;
52 | })[]>;
53 | };
54 |
--------------------------------------------------------------------------------
/types/hooks/useTreeSearchCls.d.ts:
--------------------------------------------------------------------------------
1 | import { Ref } from 'vue';
2 | import { TreeSearchProps } from '../components/TreeSearch.vue';
3 | declare const prefixCls = "vtree-tree-search";
4 | export { prefixCls as TREE_SEARCH_PREFIX_CLS };
5 | export declare const useTreeSearchCls: (props: TreeSearchProps, options: {
6 | checkAllStatus: {
7 | checked: boolean;
8 | indeterminate: boolean;
9 | disabled: boolean;
10 | };
11 | isShowingChecked: Ref;
12 | }) => {
13 | wrapperCls: import("vue").ComputedRef;
14 | searchCls: import("vue").ComputedRef;
15 | checkAllWrapperCls: import("vue").ComputedRef;
16 | checkboxCls: import("vue").ComputedRef<(string | {
17 | "vtree-tree-node__checkbox_checked": boolean;
18 | "vtree-tree-node__checkbox_indeterminate": boolean;
19 | "vtree-tree-node__checkbox_disabled": boolean;
20 | })[]>;
21 | inputWrapperCls: import("vue").ComputedRef;
22 | inputCls: import("vue").ComputedRef<(string | {
23 | "vtree-tree-search__input_disabled": boolean | undefined;
24 | })[]>;
25 | actionWrapperCls: import("vue").ComputedRef;
26 | checkedButtonCls: import("vue").ComputedRef<(string | {
27 | "vtree-tree-search__checked-button_active": boolean;
28 | })[]>;
29 | treeWrapperCls: import("vue").ComputedRef;
30 | footerCls: import("vue").ComputedRef;
31 | };
32 |
--------------------------------------------------------------------------------
/types/hooks/useVirtualList.d.ts:
--------------------------------------------------------------------------------
1 | import { INonReactiveData, TreeNodeKeyType } from "../types";
2 | import { VerticalPositionType } from "../constants";
3 | import { TreeProps } from "../components/Tree.vue";
4 | type IUseVirtualListProps = Required>;
5 | export declare const useVirtualList: (nonReactive: INonReactiveData, props: IUseVirtualListProps) => {
6 | scrollArea: import("vue").Ref;
7 | renderNodes: import("vue").Ref<{
8 | [x: string]: any;
9 | _level: number;
10 | checked: boolean;
11 | selected: boolean;
12 | indeterminate: boolean;
13 | disabled: boolean;
14 | expand: boolean;
15 | visible: boolean;
16 | _filterVisible: boolean;
17 | _parent: any | null;
18 | children: any[];
19 | isLeaf: boolean;
20 | _loading: boolean;
21 | _loaded: boolean;
22 | readonly _keyField: string;
23 | readonly _remote: boolean;
24 | setChildren: (children: import("../store/tree-node").ITreeNodeOptions[]) => void;
25 | }[]>;
26 | blockLength: import("vue").Ref;
27 | blockAreaHeight: import("vue").Ref;
28 | topSpaceHeight: import("vue").Ref;
29 | bottomSpaceHeight: import("vue").Ref;
30 | renderAmount: import("vue").Ref;
31 | renderAmountCache: import("vue").Ref;
32 | renderStart: import("vue").Ref;
33 | renderStartCache: import("vue").Ref;
34 | resetSpaceHeights: () => void;
35 | updateRenderAmount: () => void;
36 | updateRenderNodes: (isScroll?: boolean) => void;
37 | updateRender: () => void;
38 | updateBlockNodes: () => void;
39 | updateBlockData: () => void;
40 | handleTreeScroll: () => void;
41 | scrollTo: (key: TreeNodeKeyType, verticalPosition?: VerticalPositionType | number) => void;
42 | };
43 | export {};
44 |
--------------------------------------------------------------------------------
/types/index.d.ts:
--------------------------------------------------------------------------------
1 | import VTree from './components/Tree.vue';
2 | export { default as VTreeNode } from './components/TreeNode.vue';
3 | export { default as VTreeSearch } from './components/TreeSearch.vue';
4 | export { default as VTreeDrop } from './components/TreeDrop.vue';
5 | export { TreeNode } from './store';
6 | export { default as TreeStore } from './store';
7 | import './styles/index.less';
8 | export * from './types';
9 | export default VTree;
10 |
--------------------------------------------------------------------------------
/types/store/index.d.ts:
--------------------------------------------------------------------------------
1 | import TreeStore from './tree-store';
2 | import TreeNode from './tree-node';
3 | export default TreeStore;
4 | export { TreeNode };
5 |
--------------------------------------------------------------------------------
/types/store/tree-event-target.d.ts:
--------------------------------------------------------------------------------
1 | import { TreeNodeKeyType } from '../types';
2 | import TreeNode from './tree-node';
3 | type NodeGeneralListenerType = (node: TreeNode) => void;
4 | export type ListenerType = IEventNames[T];
5 | export interface IEventNames {
6 | 'set-data': () => void;
7 | 'visible-data-change': () => void;
8 | 'render-data-change': () => void;
9 | expand: NodeGeneralListenerType;
10 | select: NodeGeneralListenerType;
11 | unselect: NodeGeneralListenerType;
12 | 'selected-change': (node: TreeNode | null, key: TreeNodeKeyType | null) => void;
13 | check: NodeGeneralListenerType;
14 | uncheck: NodeGeneralListenerType;
15 | 'checked-change': (nodes: TreeNode[], keys: TreeNodeKeyType[]) => void;
16 | }
17 | export default class TreeEventTarget {
18 | /** 事件 listeners */
19 | private listenersMap;
20 | on(eventName: T, listener: ListenerType | Array>): void;
21 | off(eventName: T, listener?: ListenerType): void;
22 | emit(eventName: T, ...args: Parameters): void;
23 | disposeListeners(): void;
24 | }
25 | export {};
26 |
--------------------------------------------------------------------------------
/types/store/tree-node.d.ts:
--------------------------------------------------------------------------------
1 | import { TreeNodeKeyType } from '../types';
2 | interface IKeyOption {
3 | [key: string]: TreeNodeKeyType;
4 | }
5 | export interface ITreeNodeOptions extends IKeyOption {
6 | [key: string]: any;
7 | }
8 | export default class TreeNode {
9 | readonly _keyField: string;
10 | readonly _remote: boolean;
11 | [key: string]: any | TreeNodeKeyType;
12 | /** 节点层级 */
13 | _level: number;
14 | /** 多选是否选中 */
15 | checked: boolean;
16 | /** 单选是否选中 */
17 | selected: boolean;
18 | /** 是否半选状态 */
19 | indeterminate: boolean;
20 | /** 是否禁用 */
21 | disabled: boolean;
22 | /** 是否展开 */
23 | expand: boolean;
24 | /** 是否可见 */
25 | visible: boolean;
26 | /** 过滤后是否可见,如果为 false 则在其他可见情况下也是不可见的 */
27 | _filterVisible: boolean;
28 | /** 父节点 */
29 | _parent: null | TreeNode;
30 | /** 子节点 */
31 | children: TreeNode[];
32 | /** 是否是子节点 */
33 | isLeaf: boolean;
34 | /** 节点是否正在加载 */
35 | _loading: boolean;
36 | /** 子节点是否已加载 */
37 | _loaded: boolean;
38 | constructor(options: ITreeNodeOptions, parent?: null | TreeNode, _keyField?: string, _remote?: boolean);
39 | /**
40 | * 设置子节点
41 | * @param children 子节点数据数组
42 | */
43 | setChildren(children: ITreeNodeOptions[]): void;
44 | }
45 | export {};
46 |
--------------------------------------------------------------------------------
/types/types.d.ts:
--------------------------------------------------------------------------------
1 | import { placementEnum, ignoreEnum } from './const';
2 | import { TreeNode } from './store';
3 | export type PlacementType = keyof typeof placementEnum;
4 | export type TreeNodeKeyType = string | number;
5 | export type GetNodeFn = (key: TreeNodeKeyType) => TreeNode | null;
6 | export type IgnoreType = keyof typeof ignoreEnum;
7 | export interface TreeDropSlotProps {
8 | /** 多选选中的节点 */
9 | checkedNodes: TreeNode[];
10 | /** 多选选中的节点 key */
11 | checkedKeys: TreeNodeKeyType[];
12 | /** 单选选中的节点 */
13 | selectedNode?: TreeNode;
14 | /** 单选选中的节点 key */
15 | selectedKey?: TreeNodeKeyType;
16 | }
17 |
--------------------------------------------------------------------------------
/types/types/index.d.ts:
--------------------------------------------------------------------------------
1 | import { placementEnum, ignoreEnum, showLineType } from '../constants';
2 | import TreeStore, { TreeNode } from '../store';
3 | export type PlacementType = keyof typeof placementEnum;
4 | export type TreeNodeKeyType = string | number;
5 | export type GetNodeFn = (key: TreeNodeKeyType) => TreeNode | null;
6 | export type IgnoreType = keyof typeof ignoreEnum;
7 | export type LoadFn = (node: null | TreeNode, resolve: Function, reject: Function) => any;
8 | export interface TreeDropSlotProps {
9 | /** 多选选中的节点 */
10 | checkedNodes: TreeNode[];
11 | /** 多选选中的节点 key */
12 | checkedKeys: TreeNodeKeyType[];
13 | /** 单选选中的节点 */
14 | selectedNode?: TreeNode;
15 | /** 单选选中的节点 key */
16 | selectedKey?: TreeNodeKeyType;
17 | }
18 | export type AnyPropsArrayType = Array<{
19 | [key: string]: any;
20 | }>;
21 | export interface INonReactiveData {
22 | store: TreeStore;
23 | blockNodes: TreeNode[];
24 | }
25 | type Enumerate = Acc['length'] extends N ? Acc[number] : Enumerate;
26 | type IntRange = Exclude, Enumerate>;
27 | export interface ShowLine {
28 | /** 连接线宽度,svg stroke-width, 默认 1px */
29 | width?: number;
30 | type?: showLineType;
31 | color?: string;
32 | polyline?: boolean;
33 | dashDensity?: IntRange<1, 11>;
34 | }
35 | export {};
36 |
--------------------------------------------------------------------------------
/types/utils.d.ts:
--------------------------------------------------------------------------------
1 | import { Ref } from "vue";
2 | export declare const getVTreeMethods: >(apiMethods: readonly T[], ref: Ref) => Pick;
3 | type PickReadonly = {
4 | readonly [P in K]: T[P];
5 | };
6 | export declare const pickReadonly: (obj: Readonly, ...keys: K[]) => PickReadonly;
7 | export {};
8 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig, UserConfig } from 'vite'
2 | import vue from '@vitejs/plugin-vue'
3 | import { resolve, join } from 'path'
4 |
5 |
6 | // https://vitejs.dev/config/
7 | export default defineConfig((): UserConfig => {
8 | return {
9 | plugins: [vue()],
10 | server: {
11 | open:true,
12 | hmr:true
13 | },
14 | build: {
15 | lib: {
16 | entry: resolve(__dirname,'src/index.ts'),
17 | name:'Vtree',
18 | fileName: 'vue-tree'
19 | },
20 | rollupOptions: {
21 | external: ['vue'],
22 | output: {
23 | dir: join(__dirname, 'dist'),
24 | exports: 'named',
25 | globals: {
26 | vue: 'Vue',
27 | }
28 | }
29 | }
30 | }
31 | }
32 | })
--------------------------------------------------------------------------------
/vitest.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig, mergeConfig } from 'vitest/config'
2 | import viteConfig from './vite.config'
3 |
4 | export default defineConfig(configEnv => mergeConfig(
5 | viteConfig(configEnv),
6 | defineConfig({
7 | test: {
8 | environment: 'happy-dom',
9 | },
10 | })
11 | ))
12 |
--------------------------------------------------------------------------------