├── .eslintrc.cjs
├── .github
└── workflows
│ └── static.yml
├── .gitignore
├── .npmignore
├── .npmrc
├── .prettierrc.cjs
├── .vscode
├── extensions.json
└── settings.json
├── README.md
├── docs
├── .vitepress
│ ├── config.mts
│ └── theme
│ │ ├── Layout.vue
│ │ ├── Playground.vue
│ │ ├── custom.scss
│ │ └── index.ts
├── api
│ └── index.md
├── auto-imports.d.ts
├── components.d.ts
├── examples
│ ├── base
│ │ ├── align
│ │ │ ├── AlignColumnView.vue
│ │ │ ├── AlignView.vue
│ │ │ └── index.md
│ │ ├── basic
│ │ │ ├── BasicView.vue
│ │ │ └── index.md
│ │ ├── border
│ │ │ ├── BorderView.vue
│ │ │ └── index.md
│ │ ├── checkbox
│ │ │ ├── CheckboxCellView.vue
│ │ │ ├── CheckboxView.vue
│ │ │ └── index.md
│ │ ├── empty
│ │ │ ├── EmptySlotView.vue
│ │ │ ├── EmptyView.vue
│ │ │ └── index.md
│ │ ├── fixed
│ │ │ ├── FixedLeftView.vue
│ │ │ ├── FixedRightView.vue
│ │ │ ├── FixedView.vue
│ │ │ └── index.md
│ │ ├── highlight
│ │ │ ├── HighlightHoverView.vue
│ │ │ ├── HighlightSelectCellView.vue
│ │ │ ├── HighlightSelectView.vue
│ │ │ ├── SelectionHighlightView.vue
│ │ │ ├── SelectionView.vue
│ │ │ └── index.md
│ │ ├── index-view
│ │ │ ├── CustomIndexView.vue
│ │ │ ├── IndexView.vue
│ │ │ └── index.md
│ │ ├── no-header
│ │ │ ├── NoHeaderView.vue
│ │ │ └── index.md
│ │ ├── overflow
│ │ │ ├── OverflowHeaderView.vue
│ │ │ ├── OverflowView.vue
│ │ │ └── index.md
│ │ ├── radio
│ │ │ ├── RadioCellView.vue
│ │ │ ├── RadioView.vue
│ │ │ └── index.md
│ │ ├── stripe
│ │ │ ├── StripeView.vue
│ │ │ └── index.md
│ │ └── wrap
│ │ │ ├── WrapView.vue
│ │ │ └── index.md
│ ├── cells-3rd
│ │ ├── Cells.vue
│ │ └── index.md
│ ├── cells
│ │ ├── Cells.vue
│ │ └── index.md
│ ├── column
│ │ ├── ColumnResize.vue
│ │ ├── MinMaxColumn.vue
│ │ └── index.md
│ ├── custom-class-style
│ │ ├── BodyCellView.vue
│ │ ├── BodyRowView.vue
│ │ ├── HeaderCellView.vue
│ │ ├── HeaderRowView.vue
│ │ └── index.md
│ ├── custom
│ │ ├── CustomCell.vue
│ │ ├── CustomView.vue
│ │ └── index.md
│ ├── events
│ │ ├── EventsView.vue
│ │ └── index.md
│ ├── expand
│ │ ├── ExpandAllView.vue
│ │ ├── ExpandView.vue
│ │ └── index.md
│ ├── group
│ │ ├── GroupView.vue
│ │ └── index.md
│ ├── index.md
│ ├── merge
│ │ ├── MergeAndFixedView.vue
│ │ ├── MergeHeaderView.vue
│ │ ├── body
│ │ │ ├── MergeView.vue
│ │ │ └── index.md
│ │ └── header
│ │ │ ├── MergeHeaderAndFixedView.vue
│ │ │ └── index.md
│ ├── performance
│ │ ├── Performance.vue
│ │ └── index.md
│ ├── spreadsheet
│ │ ├── Spreadsheet.vue
│ │ └── index.md
│ ├── table
│ │ ├── ExpandRow.vue
│ │ ├── MergeView.vue
│ │ ├── TableView.vue
│ │ └── index.md
│ └── tree
│ │ ├── TreeLineView.vue
│ │ ├── TreeView.vue
│ │ └── index.md
├── guide
│ ├── start
│ │ ├── QuickStart.vue
│ │ └── index.md
│ └── theme
│ │ └── index.md
├── index.html
├── index.md
├── playground
│ └── index.md
└── public
│ └── favicon.ico
├── env.d.ts
├── package.json
├── pnpm-lock.yaml
├── scripts
└── build.ts
├── src
├── components
│ └── Placement.vue
├── hooks
│ ├── useCalcVisibleColumns.ts
│ ├── useEvent
│ │ ├── eventEmitter.ts
│ │ ├── index.ts
│ │ ├── useContentEvent.ts
│ │ └── useTableEvent.ts
│ └── useResizeColumn
│ │ ├── index.scss
│ │ └── index.ts
├── index.ts
├── interaction
│ ├── scrollZone.ts
│ └── selection.ts
├── popper
│ ├── popper.scss
│ └── popper.ts
├── store
│ ├── column.ts
│ ├── group.ts
│ ├── index.ts
│ ├── interaction.ts
│ ├── merges.ts
│ └── popperStore.ts
├── styles
│ ├── index.scss
│ └── theme.scss
├── table
│ ├── VeriTable.vue
│ ├── cell
│ │ ├── CheckboxCell.vue
│ │ ├── ExpandCell.vue
│ │ ├── IndexCell.vue
│ │ ├── LinkView.vue
│ │ ├── PersonView.vue
│ │ ├── RadioCell.vue
│ │ ├── TextCell.vue
│ │ ├── TreeCell.vue
│ │ ├── date
│ │ │ ├── DateCover.vue
│ │ │ ├── DateDropdown.vue
│ │ │ └── DateView.vue
│ │ ├── link
│ │ │ ├── LinkCover.vue
│ │ │ ├── LinkDropdown.vue
│ │ │ └── LinkView.vue
│ │ └── select
│ │ │ ├── SelectCover.vue
│ │ │ ├── SelectDropdown.vue
│ │ │ └── SelectView.vue
│ ├── header
│ │ ├── Header.vue
│ │ ├── HeaderRow.vue
│ │ └── cell
│ │ │ ├── HeaderCheckboxCell.vue
│ │ │ ├── HeaderIndexCell.vue
│ │ │ ├── HeaderOrderCell.vue
│ │ │ └── HeaderTextCell.vue
│ └── row
│ │ ├── BaseRow.tsx
│ │ ├── ExpandRow.vue
│ │ └── GroupRow.vue
├── template
│ ├── GridTable.vue
│ └── GridTableColumn.vue
├── type.ts
└── utils
│ ├── column.ts
│ ├── getCellFromEvent.ts
│ └── merge.ts
├── todo.md
├── tsconfig.app.json
├── tsconfig.build.json
├── tsconfig.json
├── tsconfig.node.json
├── tsconfig.vitest.json
└── vitest.config.ts
/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 | require('@rushstack/eslint-patch/modern-module-resolution')
3 |
4 | module.exports = {
5 | root: true,
6 | extends: [
7 | 'plugin:vue/vue3-essential',
8 | 'eslint:recommended',
9 | '@vue/eslint-config-typescript',
10 | '@vue/eslint-config-prettier/skip-formatting'
11 | ],
12 | parserOptions: {
13 | ecmaVersion: 'latest'
14 | },
15 | rules: {
16 | 'vue/multi-word-component-names': 'off'
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/.github/workflows/static.yml:
--------------------------------------------------------------------------------
1 | # Simple workflow for deploying static content to GitHub Pages
2 | name: Deploy static content to Pages
3 |
4 | on:
5 | # Runs on pushes targeting the default branch
6 | push:
7 | branches: ['master']
8 |
9 | # Allows you to run this workflow manually from the Actions tab
10 | workflow_dispatch:
11 |
12 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
13 | permissions:
14 | contents: read
15 | pages: write
16 | id-token: write
17 |
18 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
19 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
20 | concurrency:
21 | group: 'pages'
22 | cancel-in-progress: false
23 |
24 | jobs:
25 | # Single deploy job since we're just deploying
26 | deploy:
27 | environment:
28 | name: github-pages
29 | url: ${{ steps.deployment.outputs.page_url }}
30 | runs-on: ubuntu-latest
31 | steps:
32 | - name: Checkout
33 | uses: actions/checkout@v4
34 | - name: Setup Pages
35 | uses: actions/configure-pages@v4
36 |
37 | # pnpm
38 | - uses: pnpm/action-setup@v3
39 | with:
40 | version: 8
41 |
42 | # install dependency
43 | - run: pnpm install
44 |
45 | # 打包
46 | - run: npm run build:docs
47 |
48 | - name: Upload artifact
49 | uses: actions/upload-pages-artifact@v3
50 | with:
51 | # Upload entire repository
52 | path: './docs/.vitepress/dist'
53 |
54 | - name: Deploy to GitHub Pages
55 | id: deployment
56 | uses: actions/deploy-pages@v4
57 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 |
11 | # Editor directories and files
12 | .idea
13 | *.suo
14 | *.ntvs*
15 | *.njsproj
16 | *.sln
17 | *.sw?
18 | tsconfig.build.tsbuildinfo
19 |
20 | # custom
21 | node_modules
22 | .DS_Store
23 | dist
24 | dist-ssr
25 | coverage
26 | *.local
27 |
28 | /cypress/videos/
29 | /cypress/screenshots/
30 |
31 | /docs/.vitepress/cache
32 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .github/
2 | .vscode
3 | docs
4 | node_modules/
5 | scripts/
6 | .eslintignore
7 | .eslintrc.cjs
8 | .gitignore
9 | .prettierrc.js
10 | tsconfig.json
11 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | registry="https://registry.npmjs.org/"
--------------------------------------------------------------------------------
/.prettierrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | $schema: 'https://json.schemastore.org/prettierrc',
3 | semi: true,
4 | tabWidth: 2,
5 | singleQuote: true,
6 | printWidth: 100,
7 | trailingComma: 'all', // 末尾加逗号 默认none es5 包括es5中的数组、对象 all 包括函数对象等所有可选
8 | arrowParens: 'always', // (x) => {} 箭头函数参数只有一个时是否要有小括号。always: 加;avoid: 省略括号
9 | wrapAttributes: true,
10 | sortAttributes: true, // 属性自动排序
11 | };
12 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["Vue.volar", "dbaeumer.vscode-eslint", "esbenp.prettier-vscode"]
3 | }
4 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "workbench.iconTheme": "material-icon-theme",
3 | "editor.tabSize": 2,
4 | "editor.formatOnPaste": false, // required
5 | "editor.formatOnType": false, // required
6 | "editor.formatOnSave": true, // optional
7 | "editor.formatOnSaveMode": "file", // required to format on save
8 | // "files.autoSave": "onFocusChange",
9 | "editor.defaultFormatter": "esbenp.prettier-vscode",
10 | "[vue]": {
11 | "editor.defaultFormatter": "esbenp.prettier-vscode"
12 | },
13 | "[javascript]": {
14 | "editor.defaultFormatter": "esbenp.prettier-vscode"
15 | },
16 | "[typescript]": {
17 | "editor.defaultFormatter": "esbenp.prettier-vscode"
18 | },
19 | "[mdx]": {
20 | "editor.defaultFormatter": "esbenp.prettier-vscode"
21 | },
22 | "[github-actions-workflow]": {
23 | "editor.defaultFormatter": "esbenp.prettier-vscode"
24 | },
25 |
26 | "files.associations": {
27 | "*.vue": "vue",
28 | "*.wpy": "vue",
29 | "*.md": "markdown"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # vue-virt-grid
2 |
3 | 一个同时支持 合并单元格、纵向虚拟列表、横向虚拟列表 的 vue3表格组件。(仍在开发中)
4 |
5 | ## Documentation
6 |
7 | To check out docs, visit vue-virt-grid
8 |
9 | ## 想名字
10 |
11 | veritable
12 |
13 |
14 |
15 | Virtual (虚拟的): xx
16 | Efficient (高效的): 具备高效的数据处理和加载能力,在处理大量数据时,能快速响应操作指令,提升用户工作效率。
17 | Responsive (响应迅速的): 对用户交互操作,如筛选、排序、点击等,能即时给出反馈,拥有流畅的交互体验。
18 | Intuitive (直观的): 界面设计直观易懂,无需复杂学习过程,用户就能轻松掌握表格操作,快速获取所需数据 。
19 |
20 | s-table STable
21 |
22 | x-table XTable
23 |
24 | da-table DaTable
25 | data-table
26 |
27 | portable Portable
28 | 便携式的
29 |
30 | vue-virt-table VirtTable
31 |
--------------------------------------------------------------------------------
/docs/.vitepress/theme/Layout.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
27 |
40 |
--------------------------------------------------------------------------------
/docs/.vitepress/theme/Playground.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
32 |
--------------------------------------------------------------------------------
/docs/.vitepress/theme/custom.scss:
--------------------------------------------------------------------------------
1 | body {
2 | overscroll-behavior: none;
3 | }
4 |
5 | :root {
6 | --vp-layout-max-width: 2440px;
7 | }
8 |
9 | .VPDoc.has-aside .content-container {
10 | max-width: 1920px !important;
11 | }
12 |
13 | @media (min-width: 960px) {
14 | .VPNavBar.has-sidebar .wrapper {
15 | padding: 0 32px !important;
16 | }
17 |
18 | .VPNavBar.has-sidebar .content {
19 | padding-left: var(--vp-sidebar-width) !important;
20 | padding-right: 32px !important;
21 | }
22 | }
23 |
24 | @media (min-width: 1440px) {
25 | .VPSidebar {
26 | width: var(--vp-sidebar-width) !important;
27 | padding-left: 32px !important;
28 | }
29 | .Layout .VPContent.has-sidebar {
30 | padding-left: var(--vp-sidebar-width) !important;
31 | }
32 |
33 | .VPNavBar.has-sidebar div.title {
34 | width: var(--vp-sidebar-width) !important;
35 | padding: 0 32px !important;
36 | }
37 | }
38 |
39 | // 用来初始化table样式
40 | .page-examples {
41 | table {
42 | display: unset;
43 | border-collapse: unset;
44 | margin: unset;
45 | overflow-x: unset;
46 | }
47 |
48 | tr {
49 | background-color: unset;
50 | border-top: unset;
51 | transition: unset;
52 | }
53 |
54 | tr:nth-child(2n) {
55 | background-color: unset;
56 | }
57 |
58 | th,
59 | td {
60 | border: unset;
61 | padding: unset;
62 | }
63 |
64 | th {
65 | text-align: unset;
66 | font-size: unset;
67 | font-weight: unset;
68 | color: unset;
69 | background-color: unset;
70 | }
71 |
72 | td {
73 | font-size: 14px;
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/docs/.vitepress/theme/index.ts:
--------------------------------------------------------------------------------
1 | import { type Theme, useRoute } from 'vitepress';
2 | import DefaultTheme from 'vitepress/theme';
3 | import Layout from './Layout.vue';
4 | import './custom.scss';
5 | import 'resize-observer-polyfill/dist/ResizeObserver.global';
6 |
7 | export default {
8 | extends: DefaultTheme,
9 | Layout,
10 | } satisfies Theme;
11 |
--------------------------------------------------------------------------------
/docs/auto-imports.d.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | /* prettier-ignore */
3 | // @ts-nocheck
4 | // noinspection JSUnusedGlobalSymbols
5 | // Generated by unplugin-auto-import
6 | // biome-ignore lint: disable
7 | export {}
8 | declare global {
9 |
10 | }
11 |
--------------------------------------------------------------------------------
/docs/components.d.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | // @ts-nocheck
3 | // Generated by unplugin-vue-components
4 | // Read more: https://github.com/vuejs/core/pull/3399
5 | export {}
6 |
7 | /* prettier-ignore */
8 | declare module 'vue' {
9 | export interface GlobalComponents {
10 | RouterLink: typeof import('vue-router')['RouterLink']
11 | RouterView: typeof import('vue-router')['RouterView']
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/docs/examples/base/align/AlignColumnView.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
19 |
20 |
21 |
22 |
63 |
75 |
--------------------------------------------------------------------------------
/docs/examples/base/align/AlignView.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
19 |
20 |
21 |
22 |
57 |
69 |
--------------------------------------------------------------------------------
/docs/examples/base/align/index.md:
--------------------------------------------------------------------------------
1 | # 对齐方式
2 |
3 | ## 全局对齐方式
4 |
5 | ```ts
6 | interface Options {
7 | align: 'left' | 'center' | 'right';
8 | headerAlign: 'left' | 'center' | 'right';
9 |
10 | verticalAlign: 'top' | 'middle' | 'bottom';
11 | headerVerticalAlign: 'top' | 'middle' | 'bottom';
12 | }
13 | ```
14 |
15 |
2 |
7 |
8 |
37 |
48 |
--------------------------------------------------------------------------------
/docs/examples/base/basic/index.md:
--------------------------------------------------------------------------------
1 | # 基础示例
2 |
3 |
2 |
13 |
14 |
43 |
54 |
--------------------------------------------------------------------------------
/docs/examples/base/border/index.md:
--------------------------------------------------------------------------------
1 | # 带边框表格(TODO: 支持一个外边框 border=outer)
2 |
3 | ```ts
4 | interface Options {
5 | border: boolean;
6 | }
7 | ```
8 |
9 |
2 |
13 |
14 |
45 |
56 |
--------------------------------------------------------------------------------
/docs/examples/base/checkbox/CheckboxView.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
行: {{ list.length }} 列: {{ columns.length }}
4 |
5 |
12 |
13 |
14 |
15 |
43 |
55 |
--------------------------------------------------------------------------------
/docs/examples/base/checkbox/index.md:
--------------------------------------------------------------------------------
1 | # 复选框
2 |
3 | ## 复选框
4 |
5 |
2 |
3 |
4 |
5 | 怎么没有数据呢?
6 |
7 |
8 |
9 |
10 |
24 |
35 |
--------------------------------------------------------------------------------
/docs/examples/base/empty/EmptyView.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
22 |
33 |
--------------------------------------------------------------------------------
/docs/examples/base/empty/index.md:
--------------------------------------------------------------------------------
1 | # 空态
2 |
3 | ## 默认空态
4 |
5 |
2 |
14 |
15 |
47 |
58 |
--------------------------------------------------------------------------------
/docs/examples/base/fixed/FixedRightView.vue:
--------------------------------------------------------------------------------
1 |
2 |
14 |
15 |
47 |
58 |
--------------------------------------------------------------------------------
/docs/examples/base/fixed/FixedView.vue:
--------------------------------------------------------------------------------
1 |
2 |
14 |
15 |
49 |
60 |
--------------------------------------------------------------------------------
/docs/examples/base/fixed/index.md:
--------------------------------------------------------------------------------
1 | # 冻结列
2 |
3 | ## 左侧-冻结列
4 |
5 |
2 |
14 |
15 |
43 |
54 |
--------------------------------------------------------------------------------
/docs/examples/base/highlight/HighlightSelectCellView.vue:
--------------------------------------------------------------------------------
1 |
2 |
16 |
17 |
45 |
56 |
--------------------------------------------------------------------------------
/docs/examples/base/highlight/HighlightSelectView.vue:
--------------------------------------------------------------------------------
1 |
2 |
14 |
15 |
43 |
54 |
--------------------------------------------------------------------------------
/docs/examples/base/highlight/SelectionHighlightView.vue:
--------------------------------------------------------------------------------
1 |
2 |
15 |
16 |
44 |
55 |
--------------------------------------------------------------------------------
/docs/examples/base/highlight/SelectionView.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
10 | - 选中单元格后拖动快速创建区域
11 | - 按住command/ctrl可以选中多个区域
12 | - 按住shift选中区域对角点快速创建区域
13 |
14 |
15 |
16 |
17 |
146 |
157 |
--------------------------------------------------------------------------------
/docs/examples/base/highlight/index.md:
--------------------------------------------------------------------------------
1 | # 高亮
2 |
3 | > 行列高亮区分3种:
4 | >
5 | > 1. 鼠标悬浮行列(hover)
6 | > 2. 选中行列(select)
7 | > 3. 区域选中(selection)
8 |
9 | ## 悬浮行(hover)
10 |
11 |
2 |
3 |
4 |
5 |
6 |
14 |
15 |
16 |
17 |
47 |
59 |
--------------------------------------------------------------------------------
/docs/examples/base/index-view/IndexView.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
14 |
15 |
16 |
17 |
48 |
60 |
--------------------------------------------------------------------------------
/docs/examples/base/index-view/index.md:
--------------------------------------------------------------------------------
1 | # 索引
2 |
3 | ## 常规索引
4 |
5 | ```ts
6 | const columns = [
7 | {
8 | type: 'index',
9 | },
10 | ];
11 | ```
12 |
13 | `x-${index}`,
22 | },
23 | ];
24 | ```
25 |
26 |
2 |
7 |
8 |
36 |
47 |
--------------------------------------------------------------------------------
/docs/examples/base/no-header/index.md:
--------------------------------------------------------------------------------
1 | # 无表头
2 |
3 | ```ts
4 | interface Options {
5 | showHeader: boolean;
6 | }
7 | ```
8 |
9 |
2 |
7 |
8 |
38 |
49 |
--------------------------------------------------------------------------------
/docs/examples/base/overflow/OverflowView.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
38 |
49 |
--------------------------------------------------------------------------------
/docs/examples/base/overflow/index.md:
--------------------------------------------------------------------------------
1 | # 溢出隐藏(TODO: tooltip 还没实现)
2 |
3 | ## body-cell
4 |
5 | ```ts
6 | // ellipsis 当内容溢出时显示为省略号
7 | // title [推荐!性能好!] 当内容溢出时显示为省略号并用原生 title 显示
8 | // tooltip 当内容溢出时显示为省略号并用 tooltip 显示
9 | interface Options {
10 | textOverflow: 'ellipsis' | 'title' | 'tooltip';
11 | }
12 | ```
13 |
14 |
2 |
13 |
14 |
45 |
56 |
--------------------------------------------------------------------------------
/docs/examples/base/radio/RadioView.vue:
--------------------------------------------------------------------------------
1 |
2 |
13 |
14 |
45 |
56 |
--------------------------------------------------------------------------------
/docs/examples/base/radio/index.md:
--------------------------------------------------------------------------------
1 | # 单选框
2 |
3 | ## 单选框
4 |
5 |
2 |
13 |
14 |
42 |
53 |
--------------------------------------------------------------------------------
/docs/examples/base/stripe/index.md:
--------------------------------------------------------------------------------
1 | # 斑马纹表格
2 |
3 | ```ts
4 | interface Options {
5 | stripe: true;
6 | }
7 | ```
8 |
9 |
2 |
7 |
8 |
39 |
50 |
--------------------------------------------------------------------------------
/docs/examples/base/wrap/index.md:
--------------------------------------------------------------------------------
1 | # 自动换行
2 |
3 | > 默认情况下单元格内容是自动换行的
4 |
5 |
2 |
13 |
14 |
43 |
--------------------------------------------------------------------------------
/docs/examples/column/MinMaxColumn.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
37 |
--------------------------------------------------------------------------------
/docs/examples/column/index.md:
--------------------------------------------------------------------------------
1 | # 列宽拖拽
2 |
3 | ## 开启列宽拖拽
4 |
5 | ```ts
6 | interface Column {
7 | resizable: boolean;
8 | }
9 | ```
10 |
11 |
2 |
14 |
15 |
60 |
79 |
--------------------------------------------------------------------------------
/docs/examples/custom-class-style/BodyRowView.vue:
--------------------------------------------------------------------------------
1 |
2 |
14 |
15 |
60 |
79 |
--------------------------------------------------------------------------------
/docs/examples/custom-class-style/HeaderCellView.vue:
--------------------------------------------------------------------------------
1 |
2 |
14 |
15 |
54 |
69 |
--------------------------------------------------------------------------------
/docs/examples/custom-class-style/HeaderRowView.vue:
--------------------------------------------------------------------------------
1 |
2 |
14 |
15 |
51 |
66 |
--------------------------------------------------------------------------------
/docs/examples/custom-class-style/index.md:
--------------------------------------------------------------------------------
1 | # 自定义类/样式
2 |
3 | ## 表头行自定义
4 |
5 |
2 | test
3 |
4 |
11 |
--------------------------------------------------------------------------------
/docs/examples/custom/CustomView.vue:
--------------------------------------------------------------------------------
1 |
2 |
14 |
15 |
70 |
81 |
--------------------------------------------------------------------------------
/docs/examples/custom/index.md:
--------------------------------------------------------------------------------
1 | # 自定义
2 |
3 |
2 |
25 |
26 |
108 |
119 |
--------------------------------------------------------------------------------
/docs/examples/events/index.md:
--------------------------------------------------------------------------------
1 | # 事件
2 |
3 |
2 |
13 |
14 |
102 |
113 |
--------------------------------------------------------------------------------
/docs/examples/expand/ExpandView.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
93 |
104 |
--------------------------------------------------------------------------------
/docs/examples/expand/index.md:
--------------------------------------------------------------------------------
1 | # 展开行
2 |
3 | ## 基础示例
4 |
5 |
2 |
15 |
16 |
207 |
--------------------------------------------------------------------------------
/docs/examples/merge/MergeHeaderView.vue:
--------------------------------------------------------------------------------
1 |
2 |
14 |
15 |
216 |
227 |
--------------------------------------------------------------------------------
/docs/examples/merge/body/index.md:
--------------------------------------------------------------------------------
1 | # 合并单元格
2 |
3 | ## 表身合并
4 |
5 |
2 |
16 |
17 |
78 |
89 |
--------------------------------------------------------------------------------
/docs/examples/spreadsheet/index.md:
--------------------------------------------------------------------------------
1 | # Spreadsheet
2 |
3 |
2 |
10 |
18 | expand row {{ row.key }}
19 |
20 |
21 | cell: {{ row[column.field] }}
22 | header: {{ column.field }}
23 |
24 | 暂无数据
25 |
26 |
27 |
59 |
60 |
--------------------------------------------------------------------------------
/docs/examples/table/MergeView.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
58 |
--------------------------------------------------------------------------------
/docs/examples/table/TableView.vue:
--------------------------------------------------------------------------------
1 |
2 |
10 |
18 | cell: {{ row[column.field] }}
19 | header: {{ column.field }}
20 |
21 | 暂无数据
22 |
23 |
24 |
58 |
59 |
--------------------------------------------------------------------------------
/docs/examples/table/index.md:
--------------------------------------------------------------------------------
1 | # template
2 |
3 | 使用template模版方式,类似于自定义渲染,所以所有单元格都不会有默认样式中的padding,用户需要自行设置样式或者使用`vue-virt-grid-cell`的类名
4 |
5 | ## 自定义插槽
6 |
7 |
2 |
15 |
16 |
123 |
134 |
--------------------------------------------------------------------------------
/docs/examples/tree/TreeView.vue:
--------------------------------------------------------------------------------
1 |
2 |
14 |
15 |
121 |
132 |
--------------------------------------------------------------------------------
/docs/examples/tree/index.md:
--------------------------------------------------------------------------------
1 | # 树形结构(TODO: 提示线有样式bug)
2 |
3 | ## 基础示例
4 |
5 |
2 |
3 |
4 |
63 |
--------------------------------------------------------------------------------
/docs/guide/start/index.md:
--------------------------------------------------------------------------------
1 | # 开始使用
2 |
3 | ## 安装
4 |
5 | ::: code-group
6 |
7 | ```sh [npm]
8 | $ npm add vue-virt-grid
9 | ```
10 |
11 | ```sh [pnpm]
12 | $ pnpm add vue-virt-grid
13 | ```
14 |
15 | ```sh [yarn]
16 | $ yarn add vue-virt-grid
17 | ```
18 |
19 | :::
20 |
21 | ## 依赖
22 |
23 | - `"vue": ">=3.0.0"`
24 |
25 | ## Quick start
26 |
27 |
30 | .vp-doc a {
31 | text-decoration-style: dotted;
32 | text-underline-offset: 4px;
33 | }
34 | .vp-doc a:hover {
35 | text-decoration-style: solid;
36 | }
37 |
38 |
--------------------------------------------------------------------------------
/docs/guide/theme/index.md:
--------------------------------------------------------------------------------
1 | # 主题
2 |
3 | ## 白天模式
4 |
5 | ```scss
6 | // 表格元素层级
7 | --vue-virt-grid-index-normal: 1;
8 | // 表格边框颜色
9 | --vue-virt-grid-border-color: #ebeef5;
10 | // 表格边框
11 | --vue-virt-grid-border: 1px solid var(--vue-virt-grid-border-color);
12 | // 表格字体颜色
13 | --vue-virt-grid-text-color: #606266;
14 | // 表头字体颜色
15 | --vue-virt-grid-header-text-color: #909399;
16 | // 表格行hover高亮背景
17 | --vue-virt-grid-row-hover-bg-color: #f5f7fa;
18 | // 表格行斑马纹高亮背景
19 | --vue-virt-grid-row-stripe-bg-color: #fafafa;
20 | // 行选中背景
21 | --vue-virt-grid-current-row-bg-color: #fff7e6;
22 | // 列选中背景
23 | --vue-virt-grid-current-column-bg-color: #fff7e6;
24 | // 表头背景
25 | --vue-virt-grid-header-bg-color: #ccc;
26 | // 表格固定列阴影
27 | --vue-virt-grid-fixed-box-shadow: 0px 0px 12px rgba(0, 0, 0, 0.12);
28 | // 表格背景颜色
29 | --vue-virt-grid-bg-color: #ffffff;
30 | // 表格行背景颜色
31 | --vue-virt-grid-tr-bg-color: #ffffff;
32 | // 表格展开列背景颜色
33 | --vue-virt-grid-expanded-cell-bg-color: #ffffff;
34 | // 左固定列边框样式
35 | --vue-virt-grid-fixed-left-column: inset 10px 0 10px -10px rgba(0, 0, 0, 0.15);
36 | // 右固定列边框样式
37 | --vue-virt-grid-fixed-right-column: inset -10px 0 10px -10px rgba(0, 0, 0, 0.15);
38 | // 索引列层级
39 | --vue-virt-grid-index: var(--vue-virt-grid-index-normal);
40 | // 区域选中边框样式
41 | --vue-virt-grid-select-border-color: #409eff;
42 | // 区域选中背景
43 | --vue-virt-grid-select-bg-color: #ecf5ff;
44 | // 滚动条滑块颜色
45 | --vue-virt-grid-scrollbar-thumb-color: rgba(182, 185, 192, 0.3);
46 | // 滚动条滑块hover颜色
47 | --vue-virt-grid-scrollbar-thumb-color-hover: rgba(182, 185, 192, 0.5);
48 | // 树状连线颜色
49 | --vue-virt-grid-tree-line-color: #303133;
50 | ```
51 |
52 | ## 暗夜模式
53 |
54 | ```scss
55 | // 表格边框颜色
56 | --vue-virt-grid-border-color: #363637;
57 | // 表格边框
58 | --vue-virt-grid-border: 1px solid var(--vue-virt-grid-border-color);
59 | // 表格字体颜色
60 | --vue-virt-grid-text-color: #cfd3dc;
61 | // 表头字体颜色
62 | --vue-virt-grid-header-text-color: #a3a6ad;
63 | // 表格行hover高亮背景
64 | --vue-virt-grid-row-hover-bg-color: #262727;
65 | // 表格行斑马纹高亮背景
66 | --vue-virt-grid-row-stripe-bg-color: #1d1d1d;
67 | // 行选中背景
68 | --vue-virt-grid-current-row-bg-color: #2b2211;
69 | // 行选中背景
70 | --vue-virt-grid-current-column-bg-color: #2b2211;
71 | // 表头背景
72 | --vue-virt-grid-header-bg-color: #141414;
73 | // 表格固定列阴影
74 | --vue-virt-grid-fixed-box-shadow: 0px 0px 12px rgba(0, 0, 0, 0.72);
75 | // 表格背景颜色
76 | --vue-virt-grid-bg-color: #141414;
77 | // 表格行背景颜色
78 | --vue-virt-grid-tr-bg-color: #141414;
79 | // 表格展开列背景颜色
80 | --vue-virt-grid-expanded-cell-bg-color: #141414;
81 | // 左固定列边框样式
82 | --vue-virt-grid-fixed-left-column: inset 10px 0 10px -10px rgba(0, 0, 0, 0.15);
83 | // 右固定列边框样式
84 | --vue-virt-grid-fixed-right-column: inset -10px 0 10px -10px rgba(0, 0, 0, 0.15);
85 | // 索引列层级
86 | --vue-virt-grid-index: var(--vue-virt-grid-index-normal);
87 | // 区域选中边框样式
88 | --vue-virt-grid-select-border-color: #409eff;
89 | // 区域选中背景
90 | --vue-virt-grid-select-bg-color: #18222c;
91 | // 滚动条滑块颜色
92 | --vue-virt-grid-scrollbar-thumb-color: rgba(163, 166, 173, 0.3);
93 | // 滚动条滑块hover颜色
94 | --vue-virt-grid-scrollbar-thumb-color-hover: rgba(163, 166, 173, 0.5);
95 | // 树状连线颜色
96 | --vue-virt-grid-tree-line-color: #4c4d4f;
97 | ```
98 |
--------------------------------------------------------------------------------
/docs/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | vue-virt-gird
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | # https://vitepress.dev/reference/default-theme-home-page
3 | layout: home
4 |
5 | hero:
6 | name: 'vue-virt-grid'
7 | text: '一个vue3表格组件'
8 | tagline: '同时支持: 横向虚拟列表渲染、纵向虚拟列表渲染、合并单元格'
9 | actions:
10 |
11 | features:
12 | - title: 特性A
13 | details: 横向虚拟渲染
14 | - title: 特性B
15 | details: 纵向虚拟渲染
16 | - title: 特性C
17 | details: 合并单元格
18 | - title: 特性D
19 | details: 支持基础表格Api
20 | ---
21 |
--------------------------------------------------------------------------------
/docs/playground/index.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kolarorz/vue-virt-grid/ebd9b780a3ba6ae38a785cdf3ed81d01540ddf3d/docs/playground/index.md
--------------------------------------------------------------------------------
/docs/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kolarorz/vue-virt-grid/ebd9b780a3ba6ae38a785cdf3ed81d01540ddf3d/docs/public/favicon.ico
--------------------------------------------------------------------------------
/env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vue-virt-grid",
3 | "version": "0.0.1",
4 | "type": "module",
5 | "main": "lib/index.js",
6 | "module": "lib/index.js",
7 | "types": "lib/index.d.ts",
8 | "author": "kolarorz",
9 | "contributors": [],
10 | "scripts": {
11 | "dev": "vitepress dev docs --host --port=5222",
12 | "build:docs": "vitepress build docs",
13 | "build": "vite build -c ./scripts/build.ts && vue-tsc -p tsconfig.build.json",
14 | "lint": "eslint ./docs --ext .vue,.ts,.tsx,.js,.jsx --fix"
15 | },
16 | "peerDependencies": {
17 | "vue": ">=3.0.0"
18 | },
19 | "dependencies": {
20 | "lodash-es": "^4.17.21",
21 | "nanoid": "^5.0.4",
22 | "vue-virt-list": "^1.5.8"
23 | },
24 | "devDependencies": {
25 | "@rushstack/eslint-patch": "^1.3.3",
26 | "@tsconfig/node18": "^18.2.2",
27 | "@types/jsdom": "^21.1.3",
28 | "@types/lodash-es": "^4.17.12",
29 | "@types/node": "^18.18.5",
30 | "@vitejs/plugin-vue": "^4.4.0",
31 | "@vitejs/plugin-vue-jsx": "^3.1.0",
32 | "@vue/eslint-config-prettier": "^8.0.0",
33 | "@vue/eslint-config-typescript": "^12.0.0",
34 | "@vue/repl": "^3.4.0",
35 | "@vue/test-utils": "^2.4.1",
36 | "@vue/tsconfig": "^0.4.0",
37 | "element-plus": "^2.9.1",
38 | "eslint": "^8.49.0",
39 | "eslint-plugin-vue": "^9.17.0",
40 | "jsdom": "^22.1.0",
41 | "markdown-it-container": "^4.0.0",
42 | "npm-run-all2": "^6.1.1",
43 | "prettier": "^3.0.3",
44 | "resize-observer-polyfill": "^1.5.1",
45 | "sass": "^1.69.5",
46 | "typescript": "~5.2.0",
47 | "unplugin-auto-import": "^0.19.0",
48 | "unplugin-element-plus": "^0.9.0",
49 | "unplugin-vue-components": "^0.28.0",
50 | "vite": "^4.4.11",
51 | "vitepress": "1.5.0",
52 | "vitest": "^0.34.6",
53 | "vue": "^3.3.13",
54 | "vue-router": "^4.2.5",
55 | "vue-tsc": "^1.8.19"
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/scripts/build.ts:
--------------------------------------------------------------------------------
1 | import { fileURLToPath, URL } from 'node:url';
2 |
3 | import { defineConfig } from 'vite';
4 | // import legacy from '@vitejs/plugin-legacy';
5 | import vue from '@vitejs/plugin-vue';
6 | import vueJsx from '@vitejs/plugin-vue-jsx';
7 |
8 | // https://vitejs.dev/config/
9 | export default defineConfig(({ command }) => ({
10 | plugins: [vue(), vueJsx()],
11 | build: {
12 | emptyOutDir: true,
13 | minify: false,
14 | sourcemap: true,
15 | target: ['chrome62'],
16 |
17 | rollupOptions: {
18 | external: ['vue', 'vue-demi'],
19 | preserveEntrySignatures: 'strict',
20 |
21 | input: './src/index.ts',
22 | output: {
23 | manualChunks: undefined,
24 | format: 'esm',
25 | dir: './lib',
26 | // preserveModules: true,
27 | entryFileNames: '[name].js',
28 | assetFileNames: '[name][extname]',
29 | // intro: 'import "./index.css";'
30 | },
31 | },
32 | },
33 | resolve: {
34 | alias: {
35 | '@': fileURLToPath(new URL('../', import.meta.url)),
36 | },
37 | },
38 | }));
39 |
--------------------------------------------------------------------------------
/src/components/Placement.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
23 |
--------------------------------------------------------------------------------
/src/hooks/useCalcVisibleColumns.ts:
--------------------------------------------------------------------------------
1 | export const useCalcVisibleColumns = () => {};
2 |
--------------------------------------------------------------------------------
/src/hooks/useEvent/eventEmitter.ts:
--------------------------------------------------------------------------------
1 | type EventListener = (...args: any[]) => void;
2 |
3 | export class EventEmitter {
4 | private events: Record = {};
5 |
6 | // 订阅事件
7 | on(event: string, listener: EventListener): void {
8 | if (!this.events[event]) {
9 | this.events[event] = [];
10 | }
11 | this.events[event].push(listener);
12 | }
13 |
14 | // 取消订阅事件
15 | off(event: string, listener: EventListener): void {
16 | if (!this.events[event]) return;
17 | this.events[event] = this.events[event].filter((l) => l !== listener);
18 | }
19 |
20 | // 发布事件
21 | emit(event: string, ...args: any[]): void {
22 | if (!this.events[event]) return;
23 | this.events[event].forEach((listener) => listener(...args));
24 | }
25 |
26 | // 订阅一次性事件
27 | once(event: string, listener: EventListener): void {
28 | const onceWrapper = (...args: any[]) => {
29 | listener(...args);
30 | this.off(event, onceWrapper);
31 | };
32 | this.on(event, onceWrapper);
33 | }
34 |
35 | offAll(): void {
36 | this.events = {};
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/hooks/useEvent/index.ts:
--------------------------------------------------------------------------------
1 | export * from './useTableEvent';
2 | export * from './useContentEvent';
3 | export * from './eventEmitter';
4 |
--------------------------------------------------------------------------------
/src/hooks/useEvent/useContentEvent.ts:
--------------------------------------------------------------------------------
1 | import type { GridStore } from '@/src/store';
2 | import { CellEventEnum, RowEventEnum, HeaderEventEnum, type Column } from '@/src/type';
3 |
4 | /**
5 | * @desc 检查并获取表头信息
6 | */
7 | const checkAndGetThInfo = (e: MouseEvent, gridStore: GridStore) => {
8 | const composedPath = e.composedPath();
9 | const thEl = composedPath.find((el) => (el as HTMLElement).classList?.contains('vtg-th'));
10 |
11 | if (thEl) {
12 | const colId = (thEl as HTMLElement).dataset.id;
13 | if (colId === undefined) return;
14 | const targetColumnData = gridStore.columnModule.columnsInfo.headerCellInfo[colId];
15 | return {
16 | event: e,
17 | column: targetColumnData,
18 | };
19 | }
20 | return null;
21 | };
22 |
23 | /**
24 | * @desc 检查并获取单元格信息
25 | */
26 | const checkAndGetTdInfo = (event: MouseEvent, gridStore: GridStore) => {
27 | const composedPath = event.composedPath();
28 | const tdEl = composedPath.find((el) =>
29 | (el as HTMLElement).classList?.contains('vtg-td'),
30 | ) as HTMLElement;
31 |
32 | if (tdEl) {
33 | const rowIdx = (tdEl as HTMLElement).dataset.rowidx;
34 | const colIdx = (tdEl as HTMLElement).dataset.colidx;
35 | if (rowIdx === undefined || colIdx === undefined) return;
36 | const rowIdxNum = +rowIdx;
37 | const colIdxNum = +colIdx;
38 | const targetColumn = gridStore.columnModule.flattedColumns[colIdxNum];
39 | const targetRow = gridStore.originList[rowIdxNum];
40 | if (targetColumn && targetRow) {
41 | return {
42 | event,
43 | column: targetColumn,
44 | columnIndex: colIdxNum,
45 | row: gridStore.originList[rowIdxNum],
46 | rowIndex: rowIdxNum,
47 | cell: targetColumn.field ? targetRow[targetColumn.field] : null,
48 | rect: tdEl.getBoundingClientRect(),
49 | el: tdEl,
50 | };
51 | }
52 | return null;
53 | }
54 | };
55 |
56 | /**
57 | * @desc 表格内容区域相关事件,包含单元格、行、表头
58 | */
59 | export const useContentEvent = (gridStore: GridStore) => {
60 | const onClick = (e: MouseEvent) => {
61 | const thData = checkAndGetThInfo(e, gridStore);
62 | if (thData) {
63 | gridStore.eventEmitter.emit(HeaderEventEnum.HeaderClick, thData);
64 | return;
65 | }
66 | const tdData = checkAndGetTdInfo(e, gridStore);
67 | if (tdData) {
68 | gridStore.eventEmitter.emit(CellEventEnum.CellClick, tdData);
69 | gridStore.eventEmitter.emit(RowEventEnum.RowClick, tdData);
70 | }
71 | };
72 | const onDblclick = (e: MouseEvent) => {
73 | e.preventDefault();
74 | e.stopPropagation();
75 |
76 | const thData = checkAndGetThInfo(e, gridStore);
77 | if (thData) {
78 | gridStore.eventEmitter.emit(HeaderEventEnum.HeaderDblclick, thData);
79 | return;
80 | }
81 | const tdData = checkAndGetTdInfo(e, gridStore);
82 | if (tdData) {
83 | gridStore.eventEmitter.emit(CellEventEnum.CellDblclick, tdData);
84 | gridStore.eventEmitter.emit(RowEventEnum.RowDblclick, tdData);
85 |
86 | // 双击
87 | // console.log('dblclick', tdData);
88 | }
89 | };
90 | const onContextmenu = (e: MouseEvent) => {
91 | const thData = checkAndGetThInfo(e, gridStore);
92 | if (thData) {
93 | gridStore.eventEmitter.emit(HeaderEventEnum.HeaderContextmenu, thData);
94 | return;
95 | }
96 | const tdData = checkAndGetTdInfo(e, gridStore);
97 | if (tdData) {
98 | gridStore.eventEmitter.emit(CellEventEnum.CellContextmenu, tdData);
99 | gridStore.eventEmitter.emit(RowEventEnum.RowContextmenu, tdData);
100 | }
101 | };
102 | const onMouseDown = (e: MouseEvent) => {
103 | const path = e.composedPath() as HTMLElement[];
104 | // const targetTr = path.find((el) => el.tagName === 'TR');
105 | const targetTd = path.find((el) => el.tagName === 'TD');
106 | // console.log(targetTr, targetTr?.dataset.id);
107 | // console.log(targetTd, targetTd?.dataset.rowidx, targetTd?.dataset.colidx);
108 |
109 | if (targetTd?.dataset.rowidx !== undefined) {
110 | gridStore.interactionModule.setSelectRow(Number(targetTd?.dataset.rowidx));
111 | }
112 | if (targetTd?.dataset.colidx !== undefined) {
113 | gridStore.interactionModule.setSelectCol(Number(targetTd?.dataset.colidx));
114 | }
115 | gridStore.popperModule.remove();
116 | const tdData = checkAndGetTdInfo(e, gridStore);
117 | gridStore.popperModule.coverRender(tdData);
118 | };
119 |
120 | // 添加一个全局的事件监听,用于删除弹出层和选中单元格
121 | document.addEventListener('mousedown', (evt: MouseEvent) => {
122 | const popper = (evt.composedPath() as HTMLElement[]).find(
123 | (el: HTMLElement) =>
124 | el?.classList?.contains('vtg-popper-container') ||
125 | el?.classList?.contains('vtg-td') ||
126 | // TODO 未来用插件化支持
127 | el?.classList?.contains('el-popper'),
128 | );
129 | if (!popper) {
130 | gridStore.interactionModule.clearSelect();
131 | gridStore.popperModule.remove();
132 | // TODO 未来用插件化支持
133 | document.querySelectorAll('.el-popper').forEach((el) => el.remove());
134 | }
135 | });
136 |
137 | return {
138 | onClick,
139 | onDblclick,
140 | onContextmenu,
141 | onMouseDown,
142 | };
143 | };
144 |
--------------------------------------------------------------------------------
/src/hooks/useEvent/useTableEvent.ts:
--------------------------------------------------------------------------------
1 | import type { GridStore } from '@/src/store';
2 | import { TableEventEnum, type SelectedCells } from '@/src/type';
3 |
4 | export const useTableEvent = (gridStore: GridStore) => {
5 | return {
6 | onExpandChange(data: { row: any; expandedRows: any[] }) {
7 | gridStore.eventEmitter.emit(TableEventEnum.ExpandChange, data);
8 | },
9 | onCellSelection(data: { areas: SelectedCells[][]; cells: SelectedCells[] }) {
10 | gridStore.eventEmitter.emit(TableEventEnum.BoxSelection, data);
11 | },
12 | };
13 | };
14 |
--------------------------------------------------------------------------------
/src/hooks/useResizeColumn/index.scss:
--------------------------------------------------------------------------------
1 | @import '../../styles/theme.scss';
2 |
3 | .gird-column-resize-trigger {
4 | position: absolute;
5 | width: 4px;
6 | right: 0;
7 | top: 0;
8 | bottom: 0;
9 | cursor: col-resize;
10 | display: inline-block;
11 |
12 | &.is-opacity {
13 | &::after {
14 | content: '';
15 | position: absolute;
16 | top: 50%;
17 | right: 0;
18 | height: 50%;
19 | transform: translateY(-50%);
20 | background-color: var(--#{$prefix}-border-color);
21 | display: inline-block;
22 | width: 1px;
23 | }
24 | }
25 | }
26 |
27 | .gird-column-resize-line {
28 | pointer-events: none;
29 | position: absolute;
30 | top: 0;
31 | bottom: 0;
32 | left: 0;
33 | display: inline-block;
34 | width: 2px;
35 | background-color: var(--#{$prefix}-select-border-color);
36 | z-index: 100;
37 | }
38 |
--------------------------------------------------------------------------------
/src/hooks/useResizeColumn/index.ts:
--------------------------------------------------------------------------------
1 | import { onUpdated, watch } from 'vue';
2 | import './index.scss';
3 | import type { ColumnItem } from '@/src/type';
4 | import { clamp } from 'lodash-es';
5 |
6 | const ColumnResizeLineClass = 'gird-column-resize-line';
7 |
8 | const ColumnResizeTriggerClass = 'gird-column-resize-trigger';
9 |
10 | function setRelative(el: HTMLElement) {
11 | const style = getComputedStyle(el);
12 | const position = style.getPropertyValue('position');
13 | if (!position || position === 'static') {
14 | el.style.position = 'relative';
15 | }
16 | }
17 |
18 | function isInitialized(el: HTMLElement) {
19 | return !!(el as any).__resizeTrigger;
20 | }
21 |
22 | // 全局唯一,防止重复创建
23 | let resizeLine: HTMLElement | undefined;
24 |
25 | export const clearResizeLine = () => {
26 | if (!resizeLine) return;
27 | resizeLine.remove();
28 | resizeLine = undefined;
29 | };
30 |
31 | export function useResizeColumn(
32 | columnEl: HTMLElement,
33 | headerInfo: ColumnItem,
34 | tableEl: HTMLElement,
35 | cb: (width: number) => void,
36 | ) {
37 | if (isInitialized(columnEl)) return;
38 | const resizeTriggerDom = document.createElement('div');
39 | resizeTriggerDom.className = ColumnResizeTriggerClass;
40 | const data = {
41 | resizing: false,
42 | startX: 0,
43 | endX: 0,
44 | columnLeft: 0,
45 | columnCurrentWidth: 0,
46 | };
47 |
48 | function getWillWidth() {
49 | return clamp(
50 | data.endX - data.startX + data.columnCurrentWidth,
51 | headerInfo.minWidth ?? 0,
52 | headerInfo.maxWidth ?? Infinity,
53 | );
54 | }
55 |
56 | function setupTrigger() {
57 | if (!headerInfo?.resizable) {
58 | resizeTriggerDom.parentElement?.removeChild(resizeTriggerDom);
59 | return;
60 | }
61 | if (columnEl && !columnEl.contains(resizeTriggerDom)) {
62 | columnEl.appendChild(resizeTriggerDom);
63 | setRelative(columnEl);
64 |
65 | const style = getComputedStyle(columnEl);
66 | if (style.getPropertyValue('border-right-width') === '0px') {
67 | resizeTriggerDom.classList.add('is-opacity');
68 | }
69 | }
70 | (columnEl as any).__resizeTrigger = true;
71 | }
72 | onUpdated(() => setupTrigger);
73 | watch(() => [columnEl, headerInfo], setupTrigger, { immediate: true });
74 |
75 | // let resizeLine: HTMLElement | undefined;
76 |
77 | function setupResizeLine(e: MouseEvent) {
78 | if (!tableEl) return;
79 | data.startX = e.clientX;
80 | data.columnLeft =
81 | tableEl.scrollLeft +
82 | columnEl.getBoundingClientRect().left -
83 | tableEl.getBoundingClientRect().x -
84 | 2;
85 | data.endX = e.clientX;
86 | data.columnCurrentWidth = columnEl.getBoundingClientRect().width;
87 |
88 | resizeLine = document.createElement('div');
89 | resizeLine.className = ColumnResizeLineClass;
90 | tableEl.appendChild(resizeLine);
91 | setRelative(tableEl);
92 | resizeLine.style.transform = `translateX(${getWillWidth() + data.columnLeft}px)`;
93 | resizeLine.style.top = `${tableEl.scrollTop}px`;
94 | }
95 |
96 | function resize(e: MouseEvent) {
97 | if (!data.resizing) return;
98 | e.preventDefault();
99 | data.endX = e.clientX;
100 | if (resizeLine)
101 | resizeLine.style.transform = `translateX(${getWillWidth() + data.columnLeft}px)`;
102 | }
103 |
104 | resizeTriggerDom.addEventListener('mouseenter', (e) => {
105 | if (!resizeLine) setupResizeLine(e);
106 | });
107 |
108 | resizeTriggerDom.addEventListener('mouseleave', (e) => {
109 | if (!data.resizing && resizeLine) {
110 | resizeLine.remove();
111 | resizeLine = undefined;
112 | }
113 | });
114 |
115 | resizeTriggerDom.addEventListener('mousedown', (e) => {
116 | e.preventDefault();
117 | if (data.resizing) return;
118 | data.resizing = true;
119 |
120 | document.addEventListener('mousemove', resize);
121 | document.addEventListener(
122 | 'mouseup',
123 | () => {
124 | data.resizing = false;
125 | document.removeEventListener('mousemove', resize);
126 | resizeLine?.remove();
127 | resizeLine = undefined;
128 |
129 | console.log(getWillWidth());
130 | cb(getWillWidth());
131 | },
132 | {
133 | once: true,
134 | },
135 | );
136 | });
137 | }
138 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import '@/src/styles/index.scss';
2 |
3 | import Grid from '@/src/table/VeriTable.vue';
4 |
5 | export { default as GridTable } from '@/src/template/GridTable.vue';
6 | export { default as GridTableColumn } from '@/src/template/GridTableColumn.vue';
7 |
8 | // cells
9 | export { default as SelectView } from '@/src/table/cell/select/SelectView.vue';
10 | export { default as SelectCover } from '@/src/table/cell/select/SelectCover.vue';
11 | export { default as SelectDropdown } from '@/src/table/cell/select/SelectDropdown.vue';
12 |
13 | export { default as DateView } from '@/src/table/cell/date/DateView.vue';
14 | export { default as DateCover } from '@/src/table/cell/date/DateCover.vue';
15 | export { default as DateDropdown } from '@/src/table/cell/date/DateDropdown.vue';
16 |
17 | export { default as LinkView } from '@/src/table/cell/LinkView.vue';
18 | export { default as PersonView } from '@/src/table/cell/PersonView.vue';
19 |
20 | export { Grid };
21 |
22 | export * from '@/src/type';
23 |
--------------------------------------------------------------------------------
/src/interaction/scrollZone.ts:
--------------------------------------------------------------------------------
1 | import type { GridStore } from '@/src/store';
2 |
3 | export class GridScrollZone {
4 | container?: HTMLElement;
5 |
6 | scrollUpZone!: HTMLElement;
7 | scrollDownZone!: HTMLElement;
8 |
9 | scrollIntervalTimer = 0;
10 | speed = 4;
11 |
12 | lastDirection = 0;
13 |
14 | constructor(private store: GridStore) {
15 | this.scrollUpZone = document.createElement('div');
16 | this.scrollUpZone.style.width = '3000px';
17 | this.scrollUpZone.style.height = '64px';
18 | this.scrollUpZone.style.position = 'absolute';
19 | this.scrollUpZone.style.left = '0';
20 | this.scrollUpZone.style.top = '0';
21 | this.scrollUpZone.style.background = 'transparent'; // 'rgba(255,165,0,0.8)';
22 | this.scrollUpZone.style.zIndex = '-100';
23 | this.scrollUpZone.id = 'vtg-scroll-zone__top';
24 |
25 | this.scrollDownZone = document.createElement('div');
26 | this.scrollDownZone.style.width = '3000px';
27 | this.scrollDownZone.style.height = '40px';
28 | this.scrollDownZone.style.position = 'absolute';
29 | this.scrollDownZone.style.left = '0';
30 | this.scrollDownZone.style.bottom = '0';
31 | this.scrollDownZone.style.background = 'transparent'; // 'rgba(255,165,0,0.8)';
32 | this.scrollDownZone.style.zIndex = '-100';
33 | this.scrollUpZone.id = 'vtg-scroll-zone__bottom';
34 | }
35 |
36 | init(container: HTMLElement) {
37 | this.container = container;
38 | }
39 |
40 | scrollUp(offset: number, base = 1) {
41 | this.scrollIntervalTimer = requestAnimationFrame(() => {
42 | this.store.virtualListRef?.scrollToOffset(offset - this.speed * base);
43 | this.scrollUp(offset, base + 1);
44 | });
45 | }
46 |
47 | scrollDown(offset: number, base = 1) {
48 | this.scrollIntervalTimer = requestAnimationFrame(() => {
49 | this.store.virtualListRef?.scrollToOffset(offset + this.speed * base);
50 | this.scrollDown(offset, base + 1);
51 | });
52 | }
53 |
54 | onListScrollUp() {
55 | cancelAnimationFrame(this.scrollIntervalTimer);
56 | const { offset } = this.store.virtualListRef!.reactiveData;
57 | const base = this.lastDirection === -1 ? 2 : 1;
58 | this.scrollUp(offset, base);
59 | }
60 |
61 | onListScrollDown() {
62 | cancelAnimationFrame(this.scrollIntervalTimer);
63 | const { offset } = this.store.virtualListRef!.reactiveData;
64 | const base = this.lastDirection === 1 ? 2 : 1;
65 | this.scrollDown(offset, base);
66 | }
67 |
68 | handler = (e: MouseEvent) => {
69 | const els = document.elementsFromPoint(e.pageX, e.pageY);
70 | if (els.includes(this.scrollUpZone)) {
71 | this.onListScrollUp();
72 | this.lastDirection = -1;
73 | } else if (els.includes(this.scrollDownZone)) {
74 | this.onListScrollDown();
75 | this.lastDirection = 1;
76 | } else {
77 | cancelAnimationFrame(this.scrollIntervalTimer);
78 | this.lastDirection = 0;
79 | }
80 | };
81 |
82 | append() {
83 | this.remove();
84 | this.container?.appendChild(this.scrollUpZone);
85 | this.container?.appendChild(this.scrollDownZone);
86 | this.container?.addEventListener('mousemove', this.handler);
87 | document.body.addEventListener('mouseup', this.handler);
88 | this.lastDirection = 0;
89 | }
90 |
91 | remove() {
92 | cancelAnimationFrame(this.scrollIntervalTimer);
93 | this.scrollUpZone?.remove();
94 | this.scrollDownZone?.remove();
95 | this.container?.removeEventListener('mousemove', this.handler);
96 | document.body.removeEventListener('mouseup', this.handler);
97 | this.lastDirection = 0;
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/src/interaction/selection.ts:
--------------------------------------------------------------------------------
1 | import { cloneDeep, isEqual } from 'lodash-es';
2 | import type { GridStore } from '@/src/store';
3 | import { getCellFromEvent } from '@/src/utils/getCellFromEvent';
4 | import { nanoid } from 'nanoid';
5 |
6 | export class GridSelection {
7 | container?: HTMLElement;
8 |
9 | boxArea = {
10 | left: 0,
11 | top: 0,
12 | right: 0,
13 | bottom: 0,
14 | };
15 |
16 | id: string = '';
17 |
18 | startPos = {
19 | rowIndex: -1,
20 | colIndex: -1,
21 | };
22 |
23 | lastClickPos?: { rowIndex: number; colIndex: number } = undefined;
24 | lastShiftClickPos?: { rowIndex: number; colIndex: number } = undefined;
25 |
26 | callbackFunc: any = () => {};
27 |
28 | isMetaKey = false;
29 | isShiftKey = false;
30 |
31 | constructor(private store: GridStore) {}
32 |
33 | init(el: HTMLElement) {
34 | this.container = el;
35 | this.container.addEventListener('mousedown', this.onMousedown);
36 | }
37 |
38 | on(fn: any) {
39 | this.callbackFunc = fn;
40 | }
41 |
42 | reset() {}
43 |
44 | preventContextMenu = (e: MouseEvent) => {
45 | e.preventDefault();
46 | this.onMouseup();
47 | };
48 |
49 | onMousedown = (e: MouseEvent) => {
50 | if (!this.store.getState('selection')) return;
51 | // 禁用右键响应
52 | if (e.buttons !== 1 || e.button) return;
53 | const cellInfo = getCellFromEvent(e);
54 | this.isMetaKey = e.metaKey || e.ctrlKey;
55 | this.isShiftKey = e.shiftKey;
56 |
57 | if (cellInfo) {
58 | const { colIndex, rowIndex } = cellInfo;
59 |
60 | this.id = nanoid(4);
61 |
62 | this.startPos = {
63 | rowIndex,
64 | colIndex,
65 | };
66 |
67 | if (this.isShiftKey) {
68 | if (!this.lastShiftClickPos) {
69 | this.lastShiftClickPos = cloneDeep(this.lastClickPos || this.startPos);
70 | }
71 | } else {
72 | this.lastShiftClickPos = undefined;
73 | }
74 |
75 | if (this.isShiftKey && this.lastShiftClickPos) {
76 | const { rowIndex: ri, colIndex: ci } = this.lastShiftClickPos;
77 | this.boxArea = {
78 | top: Math.min(rowIndex, ri),
79 | bottom: Math.max(rowIndex, ri),
80 | left: Math.min(colIndex, ci),
81 | right: Math.max(colIndex, ci),
82 | };
83 | } else {
84 | this.boxArea = {
85 | top: rowIndex,
86 | bottom: rowIndex,
87 | left: colIndex,
88 | right: colIndex,
89 | };
90 | }
91 |
92 | this.lastClickPos = cloneDeep(this.startPos);
93 |
94 | this.container!.style.userSelect = 'none';
95 | document.body.addEventListener('mouseup', this.onMouseup);
96 | document.body.addEventListener('mouseover', this.onMouseOver);
97 | document.body.addEventListener('contextmenu', this.preventContextMenu);
98 | this.store.interactionModule.gridScrollZone.append();
99 | this.emitChange();
100 | }
101 | };
102 |
103 | onMouseOver = (e: MouseEvent) => {
104 | if (!this.store.getState('selection')) return;
105 | const cellInfo = getCellFromEvent(e);
106 |
107 | if (cellInfo) {
108 | const { colIndex, rowIndex } = cellInfo;
109 |
110 | const newArea = {
111 | top: Math.min(this.startPos.rowIndex, rowIndex),
112 | bottom: Math.max(this.startPos.rowIndex, rowIndex),
113 | left: Math.min(this.startPos.colIndex, colIndex),
114 | right: Math.max(this.startPos.colIndex, colIndex),
115 | };
116 |
117 | if (!isEqual(newArea, this.boxArea)) {
118 | this.boxArea = newArea;
119 | this.emitChange();
120 | }
121 | }
122 | };
123 |
124 | onMouseup = () => {
125 | if (!this.store.getState('selection')) return;
126 | this.id = '';
127 | this.boxArea = {
128 | left: 0,
129 | top: 0,
130 | right: 0,
131 | bottom: 0,
132 | };
133 | this.container!.style.userSelect = 'auto';
134 | document.body.removeEventListener('mouseover', this.onMouseOver);
135 | document.body.removeEventListener('mouseup', this.onMouseup);
136 | document.body.removeEventListener('contextmenu', this.preventContextMenu);
137 | this.store.interactionModule.gridScrollZone.remove();
138 | };
139 |
140 | emitChange() {
141 | this.callbackFunc(this.id, this.boxArea, this.isMetaKey);
142 | }
143 |
144 | destroy() {
145 | this.container?.removeEventListener('mousedown', this.onMousedown);
146 | document.body.removeEventListener('contextmenu', this.preventContextMenu);
147 | document.body.removeEventListener('mouseup', this.onMouseup);
148 | document.body.removeEventListener('mouseover', this.onMouseOver);
149 | this.store.interactionModule.gridScrollZone.remove();
150 | }
151 | }
152 |
--------------------------------------------------------------------------------
/src/popper/popper.scss:
--------------------------------------------------------------------------------
1 | .hyd-popper {
2 | position: absolute;
3 | left: 0;
4 | top: 0;
5 | z-index: 100000;
6 |
7 | .popper__arrow {
8 | position: absolute;
9 | display: block;
10 | width: 0;
11 | height: 0;
12 | border-color: transparent;
13 | border-style: solid;
14 | border-width: 6px;
15 | filter: drop-shadow(0 2px 12px rgba(0, 0, 0, 3%));
16 |
17 | &::after {
18 | position: absolute;
19 | display: block;
20 | width: 0;
21 | height: 0;
22 | border-color: transparent;
23 | border-style: solid;
24 | content: ' ';
25 | border-width: 6px;
26 | }
27 | }
28 |
29 | --hyd-popper-arrow-left: 50%;
30 | --hyd-popper-arrow-top: calc(50% - 6px);
31 |
32 | &[x-placement$='start'] {
33 | --hyd-popper-arrow-left: 10%;
34 | --hyd-popper-arrow-top: 10%;
35 | }
36 |
37 | &[x-placement$='end'] {
38 | --hyd-popper-arrow-left: calc(90% - 12px);
39 | --hyd-popper-arrow-top: calc(90% - 12px);
40 | }
41 |
42 | --hyd-popper-arrow-color: #ccc;
43 | --hyd-popper-arrow-background-color: #fff;
44 |
45 | &[x-placement^='top'] .popper__arrow {
46 | bottom: -6px;
47 | left: var(--hyd-popper-arrow-left);
48 | margin-right: 2px;
49 | border-top-color: var(--hyd-popper-arrow-color);
50 | border-bottom-width: 0;
51 |
52 | &::after {
53 | bottom: 1px;
54 | margin-left: -6px;
55 | border-top-color: var(--hyd-popper-arrow-background-color);
56 | border-bottom-width: 0;
57 | }
58 | }
59 |
60 | &[x-placement^='bottom'] .popper__arrow {
61 | top: -6px;
62 | left: var(--hyd-popper-arrow-left);
63 | margin-right: 2px;
64 | border-top-width: 0;
65 | border-bottom-color: var(--hyd-popper-arrow-color);
66 |
67 | &::after {
68 | top: 1px;
69 | margin-left: -6px;
70 | border-top-width: 0;
71 | border-bottom-color: var(--hyd-popper-arrow-background-color);
72 | }
73 | }
74 |
75 | &[x-placement^='right'] .popper__arrow {
76 | top: var(--hyd-popper-arrow-top);
77 | left: -6px;
78 | margin-bottom: 2px;
79 | border-right-color: var(--hyd-popper-arrow-color);
80 | border-left-width: 0;
81 |
82 | &::after {
83 | bottom: -6px;
84 | left: 1px;
85 | border-right-color: var(--hyd-popper-arrow-background-color);
86 | border-left-width: 0;
87 | }
88 | }
89 |
90 | &[x-placement^='left'] .popper__arrow {
91 | top: var(--hyd-popper-arrow-top);
92 | right: -6px;
93 | margin-bottom: 2px;
94 | border-right-width: 0;
95 | border-left-color: var(--hyd-popper-arrow-color);
96 |
97 | &::after {
98 | right: 1px;
99 | bottom: -6px;
100 | border-right-width: 0;
101 | border-left-color: var(--hyd-popper-arrow-background-color);
102 | }
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/src/store/column.ts:
--------------------------------------------------------------------------------
1 | import { reactive, shallowReactive } from 'vue';
2 | import type { GridStore } from '.';
3 | import type { Column, ColumnItem } from '@/src/type';
4 | import { formatColumns, type HeaderCellInfo } from '@/src/utils/column';
5 |
6 | export interface IColumnsRenderInfo {
7 | leftFixedColumns: ColumnItem[];
8 | rightFixedColumns: ColumnItem[];
9 | centerNormalHeaderColumns: ColumnItem[][];
10 | leftFixedHeaderColumns: ColumnItem[][];
11 | rightFixedHeaderColumns: ColumnItem[][];
12 | headerCellInfo: HeaderCellInfo;
13 | }
14 |
15 | export class GridColumn {
16 | constructor(private store: GridStore) {
17 | //
18 | }
19 |
20 | // 原始列数据(带 _id),一般不直接用
21 | private originColumns = [] as ColumnItem[];
22 | // 平铺列(子树列)
23 | flattedColumns = [] as ColumnItem[];
24 | // 左侧固定列(子树列)
25 | leftFixedColumns = [] as ColumnItem[];
26 | // 右侧固定列(子树列)
27 | rightFixedColumns = [] as ColumnItem[];
28 | // 中间主要列(子树列)
29 | centerNormalColumns = [] as ColumnItem[];
30 |
31 | // 这3个是给表头用的
32 | leftFixedHeaderColumns = [] as ColumnItem[][];
33 | rightFixedHeaderColumns = [] as ColumnItem[][];
34 | centerNormalHeaderColumns = [] as ColumnItem[][];
35 |
36 | columnState = reactive({
37 | fullWidth: 0,
38 | fixedInfo: {
39 | leftWidth: 0,
40 | rightWidth: 0,
41 | },
42 | });
43 |
44 | columnsInfo = shallowReactive({
45 | leftFixedColumns: [],
46 | rightFixedColumns: [],
47 | leftFixedHeaderColumns: [],
48 | rightFixedHeaderColumns: [],
49 | centerNormalHeaderColumns: [],
50 | headerCellInfo: {},
51 | });
52 |
53 | setColumns(columns: Column[]) {
54 | // 存储最原始的列
55 | // 格式化列信息
56 | const {
57 | leftFixedColumns,
58 | rightFixedColumns,
59 | centerNormalColumns,
60 | flattedColumns,
61 | headerCellInfo,
62 | originColumns,
63 |
64 | leftFixedHeaderColumns,
65 | rightFixedHeaderColumns,
66 | centerNormalHeaderColumns,
67 |
68 | fixedInfo,
69 | } = formatColumns(columns);
70 |
71 | this.leftFixedColumns = leftFixedColumns;
72 | this.rightFixedColumns = rightFixedColumns;
73 | this.centerNormalColumns = centerNormalColumns;
74 | this.flattedColumns = flattedColumns;
75 | this.originColumns = originColumns;
76 |
77 | this.columnState.fixedInfo = fixedInfo;
78 | this.columnsInfo.headerCellInfo = headerCellInfo;
79 | this.columnsInfo.leftFixedColumns = leftFixedColumns;
80 | this.columnsInfo.rightFixedColumns = rightFixedColumns;
81 | this.columnsInfo.leftFixedHeaderColumns = leftFixedHeaderColumns;
82 | this.columnsInfo.rightFixedHeaderColumns = rightFixedHeaderColumns;
83 | this.columnsInfo.centerNormalHeaderColumns = centerNormalHeaderColumns;
84 | // this.flattedColumns = flattedColumns;
85 | // // 拿平铺的列进行遍历
86 | // let leftReduce = 0;
87 | // this.flattedColumns.forEach((col) => {
88 | // if (col.fixed === 'left') {
89 | // this.leftFixedColumns.push(Object.assign(col, { left: leftReduce }));
90 | // leftReduce += col.width;
91 | // } else if (col.fixed === 'right') {
92 | // // TODO right的值是要计算出来的
93 | // this.rightFixedColumns.push(col);
94 | // } else {
95 | // this.centerNormalColumns.push(col);
96 | // }
97 | // });
98 | this.columnState.fullWidth = this.flattedColumns.reduce((a, b) => a + b.width!, 0);
99 | this.store.forceUpdate();
100 | }
101 |
102 | setColumnWidth(id: string, width: number) {
103 | const column = this.flattedColumns.find((col) => col._id === id);
104 | if (column) {
105 | column.width = width;
106 | this.setColumns(this.originColumns);
107 | }
108 | }
109 |
110 | calcVisibleColumns(scrollLeft: number, clientWidth: number) {
111 | // console.log('calcVisibleColumns', scrollLeft, clientWidth);
112 | let colRenderBegin = 0;
113 | let colRenderEnd = 0;
114 | let currentLeft = 0;
115 | let beginFlag = false;
116 | for (let i = 0; i < this.centerNormalColumns.length; i++) {
117 | const currentWidth = this.centerNormalColumns[i].width!;
118 | // console.log('currentWidth', currentLeft, scrollLeft, scrollLeft + clientWidth);
119 | if (currentLeft >= scrollLeft && !beginFlag) {
120 | colRenderBegin = i;
121 | beginFlag = true;
122 | } else if (currentLeft >= scrollLeft + clientWidth) {
123 | colRenderEnd = i;
124 | // console.log('计算结束', colRenderBegin, colRenderEnd);
125 | break;
126 | }
127 | colRenderEnd = i;
128 | currentLeft += currentWidth;
129 | }
130 | // 给首尾各加一个buffer
131 | // TODO 这里可以减少点
132 | colRenderBegin = Math.max(0, colRenderBegin - 1);
133 | colRenderEnd = Math.min(this.centerNormalColumns.length - 1, colRenderEnd + 1);
134 |
135 | if (
136 | colRenderBegin !== this.store.mergeModule.mergeState.originRect.xs ||
137 | colRenderEnd !== this.store.mergeModule.mergeState.originRect.xe
138 | ) {
139 | // console.warn('横向计算结束', colRenderBegin, colRenderEnd);
140 |
141 | this.store.mergeModule.mergeState.originRect.xs = colRenderBegin;
142 | this.store.mergeModule.mergeState.originRect.xe = colRenderEnd;
143 |
144 | this.store.mergeModule.calcRect(true);
145 | }
146 | }
147 | }
148 |
--------------------------------------------------------------------------------
/src/store/group.ts:
--------------------------------------------------------------------------------
1 | import { reactive } from 'vue';
2 | import type { GridStore } from '.';
3 | import { CellType, type ListItem } from '@/src/type';
4 | import { nanoid } from 'nanoid';
5 | import { useTableEvent } from '@/src/hooks/useEvent/useTableEvent';
6 |
7 | export class GridGroup {
8 | constructor(private store: GridStore) {
9 | //
10 | }
11 | groupState = reactive({
12 | // 父子显示的映射
13 | foldMap: {} as Record,
14 | // 展开行显示的映射
15 | expandMap: {} as Record,
16 | });
17 |
18 | groupFoldConstructor(list: ListItem[], conditions: { columnId: string; sort: 'desc' | 'asc' }[]) {
19 | console.log('groupFoldConstructor', list.length, conditions);
20 | return this.constructGroup(list, 0, conditions);
21 | }
22 |
23 | constructGroup(
24 | list: ListItem[],
25 | conditionIndex: number,
26 | conditions: { columnId: string; sort: 'desc' | 'asc' }[],
27 | ) {
28 | if (conditionIndex >= conditions.length) {
29 | return list;
30 | }
31 |
32 | const { columnId, sort } = conditions[conditionIndex];
33 |
34 | const sortedList = list.sort((a, b) => {
35 | if (sort === 'desc') {
36 | return (b[columnId] as string).localeCompare(a[columnId] as string);
37 | }
38 | return (a[columnId] as string).localeCompare(b[columnId] as string);
39 | });
40 |
41 | const res: ListItem[][] = [];
42 | let subGroup: ListItem[] = [];
43 |
44 | for (let i = 0; i < sortedList.length; i++) {
45 | const item = sortedList[i];
46 | if (item[columnId] === subGroup[subGroup.length - 1]?.[columnId]) {
47 | subGroup.push(item);
48 | } else {
49 | if (subGroup.length) {
50 | res.push(subGroup);
51 | }
52 | subGroup = [item];
53 | }
54 | }
55 |
56 | if (subGroup.length) {
57 | res.push(subGroup);
58 | }
59 |
60 | const groupList: ListItem[] = [];
61 | res.forEach((item) => {
62 | const v = item[0][columnId];
63 | groupList.push({
64 | id: `group-${columnId}-${nanoid(4)}`,
65 | type: 'group',
66 | columnId,
67 | name: v,
68 | children: this.constructGroup(item, conditionIndex + 1, conditions),
69 | });
70 | });
71 |
72 | return groupList;
73 | }
74 |
75 | generateFlatList(originList: ListItem[]) {
76 | const flattenList: ListItem[] = [];
77 |
78 | const { foldMap, expandMap } = this.groupState;
79 |
80 | const hasExpandCol = !!this.store.columnModule.flattedColumns.find(
81 | (col) => col.type === CellType.Expand,
82 | );
83 |
84 | const defaultExpandAll = this.store.getState('defaultExpandAll');
85 |
86 | this.store.gridRowMap = {};
87 |
88 | let level = 0;
89 | let groupLevel = 0;
90 | const flat = (list: ListItem[], isGroup = false) => {
91 | list.forEach((item, index) => {
92 | if (isGroup) {
93 | groupLevel += 1;
94 | }
95 |
96 | // const row = { ...item, level, groupLevel, isLastChild: index === list.length - 1 };
97 | // 需要用原始对象,否则不能响应式
98 | const row = Object.assign(item, {
99 | level,
100 | groupLevel,
101 | isLastChild: index === list.length - 1,
102 | });
103 | flattenList.push(row);
104 | this.store.gridRowMap[row.id] = row;
105 |
106 | if (item?.children && item?.children?.length > 0) {
107 | level += 1;
108 | foldMap[item.id] = !defaultExpandAll;
109 | if (defaultExpandAll) {
110 | flat(item.children, item.type === 'group');
111 | }
112 | level -= 1;
113 | }
114 |
115 | if (hasExpandCol) {
116 | expandMap[item.id] = !!defaultExpandAll;
117 | if (defaultExpandAll) {
118 | this.store.gridRowMap[`${item.id}-expand`] = {
119 | id: `${item.id}-expand`,
120 | type: 'expand',
121 | };
122 | }
123 | }
124 |
125 | if (expandMap[item.id]) {
126 | flattenList.push(this.store.gridRowMap[`${item.id}-expand`]);
127 | }
128 | if (isGroup) {
129 | groupLevel -= 1;
130 | }
131 | });
132 | };
133 | flat(originList);
134 |
135 | return flattenList;
136 | }
137 |
138 | resetFlatList(originList: ListItem[]) {
139 | const flattenList: ListItem[] = [];
140 | const { foldMap, expandMap } = this.groupState;
141 |
142 | let level = 0;
143 | let groupLevel = 0;
144 | const flat = (list: ListItem[], isGroup = false) => {
145 | list.forEach((item, index) => {
146 | if (isGroup) {
147 | groupLevel += 1;
148 | }
149 |
150 | // const row = { ...item, level, groupLevel, isLastChild: index === list.length - 1 };
151 | // 需要用原始对象,否则不能响应式
152 | const row = Object.assign(item, {
153 | level,
154 | groupLevel,
155 | isLastChild: index === list.length - 1,
156 | });
157 | flattenList.push(row);
158 | this.store.gridRowMap[row.id] = row;
159 |
160 | if (foldMap[item.id] === false && item?.children && item?.children?.length > 0) {
161 | level += 1;
162 | flat(item.children, item.type === 'group');
163 | level -= 1;
164 | }
165 | if (expandMap[item.id]) {
166 | flattenList.push(this.store.gridRowMap[`${item.id}-expand`]);
167 | }
168 | if (isGroup) {
169 | groupLevel -= 1;
170 | }
171 | });
172 | };
173 | flat(originList);
174 |
175 | this.store.setList(flattenList || []);
176 | }
177 |
178 | toggleFold(id: string) {
179 | const { foldMap } = this.groupState;
180 | foldMap[id] = !foldMap[id];
181 | this.resetFlatList(this.store.originList);
182 | }
183 |
184 | toggleExpand(id: string) {
185 | const tableEvents = useTableEvent(this.store);
186 | const { expandMap } = this.groupState;
187 | expandMap[id] = !expandMap[id];
188 | if (expandMap[id] && !this.store.gridRowMap[`${id}-expand`]) {
189 | this.store.gridRowMap[`${id}-expand`] = { id: `${id}-expand`, type: 'expand' };
190 | }
191 |
192 | // TODO yihuang 优化下性能用一次遍历解决
193 | const expandedRowKeys = Object.keys(expandMap)
194 | .filter((key) => !!expandMap[key])
195 | .map((id) => this.store.gridRowMap[id]);
196 |
197 | tableEvents.onExpandChange({ row: this.store.gridRowMap[id], expandedRows: expandedRowKeys });
198 | this.resetFlatList(this.store.originList);
199 | }
200 | }
201 |
--------------------------------------------------------------------------------
/src/store/popperStore.ts:
--------------------------------------------------------------------------------
1 | import { createApp, nextTick, type App } from 'vue';
2 | import { createPopper } from '@/src/popper/popper';
3 | import { GridStore } from '@/src/store';
4 | import { isFunction } from 'lodash-es';
5 |
6 | class PopperStore {
7 | gridStore: GridStore;
8 | coverEl: HTMLElement | null = null;
9 | coverApp: App | null = null;
10 | dropdownEl: HTMLElement | null = null;
11 | dropdownApp: App | null = null;
12 | tdData: any;
13 |
14 | constructor(gridStore: GridStore) {
15 | this.gridStore = gridStore;
16 |
17 | this.coverEl = document.createElement('div');
18 | this.coverEl.classList.add('vtg-popper-container');
19 | // TODO 需要判断是否有激活单元格选项配置
20 | // this.coverEl.classList.add('vtg-popper-container--cover');
21 | this.coverEl.addEventListener('click', this.toggleDropdownRender.bind(this));
22 |
23 | this.dropdownEl = document.createElement('div');
24 | this.dropdownEl.classList.add('vtg-popper-container');
25 | }
26 |
27 | coverRender(tdData: any) {
28 | this.tdData = tdData;
29 |
30 | if (!this.coverEl || !this.gridStore.clientEl) return;
31 | if (!isFunction(tdData?.column?.cellCoverRender)) {
32 | // 这里判断一下,没有cover,如果只有dropdown,那就直接渲染dropdown
33 | this.dropdownRender(tdData);
34 | return;
35 | }
36 | this.coverApp = createApp({
37 | render: () =>
38 | tdData.column.cellCoverRender?.(tdData, () => {
39 | this.remove();
40 | }),
41 | });
42 | createPopper({
43 | reference: tdData.el,
44 | mountEl: this.gridStore.clientEl,
45 | popperContainer: this.coverEl,
46 | popper: this.coverApp,
47 | isCover: true,
48 | });
49 | }
50 |
51 | toggleDropdownRender() {
52 | console.log('111', this.tdData);
53 | if (!this.dropdownEl) return;
54 | console.log('toggleDropdownRender', this.dropdownEl, this.dropdownApp);
55 |
56 | if (this.dropdownApp && this.gridStore.clientEl?.contains(this.dropdownEl)) {
57 | this.gridStore.clientEl?.removeChild(this.dropdownEl);
58 | } else if (this.dropdownApp) {
59 | this.gridStore.clientEl?.appendChild(this.dropdownEl);
60 | } else {
61 | this.dropdownRender(this.tdData);
62 | }
63 | }
64 |
65 | dropdownRender(tdData: any) {
66 | if (!this.coverEl || !this.dropdownEl || !this.gridStore.clientEl) return;
67 | if (!isFunction(tdData?.column?.cellDropdownRender)) return;
68 |
69 | this.dropdownApp = createApp({
70 | render: () => tdData.column.cellDropdownRender?.(tdData),
71 | });
72 |
73 | createPopper({
74 | reference: this.coverEl,
75 | mountEl: this.gridStore.clientEl,
76 | popperContainer: this.dropdownEl,
77 | popper: this.dropdownApp,
78 | isCover: false,
79 | });
80 | }
81 |
82 | remove() {
83 | console.log('remove');
84 | if (this.coverEl && this.gridStore.clientEl?.contains(this.coverEl)) {
85 | this.gridStore.clientEl?.removeChild(this.coverEl);
86 | this.coverApp = null;
87 | this.coverEl.removeEventListener('click', this.toggleDropdownRender);
88 | }
89 | if (this.dropdownEl && this.gridStore.clientEl?.contains(this.dropdownEl)) {
90 | this.gridStore.clientEl?.removeChild(this.dropdownEl);
91 | this.dropdownApp = null;
92 | }
93 | }
94 | }
95 |
96 | export { PopperStore };
97 |
--------------------------------------------------------------------------------
/src/styles/theme.scss:
--------------------------------------------------------------------------------
1 | $prefix: 'vtg';
2 |
3 | :root {
4 | // 表格元素层级
5 | --#{$prefix}-index-normal: 1;
6 | // 表格边框颜色
7 | --#{$prefix}-border-color: #e5e6eb;
8 | // 表格边框
9 | --#{$prefix}-border: 1px solid var(--#{$prefix}-border-color);
10 | // 表格字体颜色
11 | --#{$prefix}-text-color: #606266;
12 | // 表头字体颜色
13 | --#{$prefix}-header-text-color: #909399;
14 | // 表格行hover高亮背景
15 | --#{$prefix}-row-hover-bg-color: #f5f7fa;
16 | // 表格行斑马纹高亮背景
17 | --#{$prefix}-row-stripe-bg-color: #fafafa;
18 | // 行选中背景
19 | --#{$prefix}-current-row-bg-color: #fff7e6;
20 | // 列选中背景
21 | --#{$prefix}-current-column-bg-color: #fff7e6;
22 | // 表头背景
23 | --#{$prefix}-header-bg-color: #f2f3f5;
24 | // 表格固定列阴影
25 | --#{$prefix}-fixed-box-shadow: 0px 0px 12px rgba(0, 0, 0, 0.12);
26 | // 表格背景颜色
27 | --#{$prefix}-bg-color: #ffffff;
28 | // 表格行背景颜色
29 | --#{$prefix}-tr-bg-color: #ffffff;
30 | // 表格展开列背景颜色
31 | --#{$prefix}-expanded-cell-bg-color: #ffffff;
32 | // 左固定列边框样式
33 | --#{$prefix}-fixed-left-column: inset 10px 0 10px -10px rgba(0, 0, 0, 0.15);
34 | // 右固定列边框样式
35 | --#{$prefix}-fixed-right-column: inset -10px 0 10px -10px rgba(0, 0, 0, 0.15);
36 | // 索引列层级
37 | --#{$prefix}-index: var(--#{$prefix}-index-normal);
38 | // 区域选中边框样式
39 | --#{$prefix}-select-border-color: #409eff;
40 | // 区域选中背景
41 | --#{$prefix}-select-bg-color: #ecf5ff;
42 | // 滚动条滑块颜色
43 | --#{$prefix}-scrollbar-thumb-color: rgba(182, 185, 192, 0.3);
44 | // 滚动条滑块hover颜色
45 | --#{$prefix}-scrollbar-thumb-color-hover: rgba(182, 185, 192, 0.5);
46 | // 树状连线颜色
47 | --#{$prefix}-tree-line-color: #303133;
48 | }
49 |
50 | html.dark {
51 | // 表格边框颜色
52 | --#{$prefix}-border-color: #363637;
53 | // 表格边框
54 | --#{$prefix}-border: 1px solid var(--#{$prefix}-border-color);
55 | // 表格字体颜色
56 | --#{$prefix}-text-color: #cfd3dc;
57 | // 表头字体颜色
58 | --#{$prefix}-header-text-color: #a3a6ad;
59 | // 表格行hover高亮背景
60 | --#{$prefix}-row-hover-bg-color: #262727;
61 | // 表格行斑马纹高亮背景
62 | --#{$prefix}-row-stripe-bg-color: #1d1d1d;
63 | // 行选中背景
64 | --#{$prefix}-current-row-bg-color: #2b2211;
65 | // 行选中背景
66 | --#{$prefix}-current-column-bg-color: #2b2211;
67 | // 表头背景
68 | --#{$prefix}-header-bg-color: #141414;
69 | // 表格固定列阴影
70 | --#{$prefix}-fixed-box-shadow: 0px 0px 12px rgba(0, 0, 0, 0.72);
71 | // 表格背景颜色
72 | --#{$prefix}-bg-color: #141414;
73 | // 表格行背景颜色
74 | --#{$prefix}-tr-bg-color: #141414;
75 | // 表格展开列背景颜色
76 | --#{$prefix}-expanded-cell-bg-color: #141414;
77 | // 左固定列边框样式
78 | --#{$prefix}-fixed-left-column: inset 10px 0 10px -10px rgba(0, 0, 0, 0.15);
79 | // 右固定列边框样式
80 | --#{$prefix}-fixed-right-column: inset -10px 0 10px -10px rgba(0, 0, 0, 0.15);
81 | // 索引列层级
82 | --#{$prefix}-index: var(--#{$prefix}-index-normal);
83 | // 区域选中边框样式
84 | --#{$prefix}-select-border-color: #409eff;
85 | // 区域选中背景
86 | --#{$prefix}-select-bg-color: #18222c;
87 | // 滚动条滑块颜色
88 | --#{$prefix}-scrollbar-thumb-color: rgba(163, 166, 173, 0.3);
89 | // 滚动条滑块hover颜色
90 | --#{$prefix}-scrollbar-thumb-color-hover: rgba(163, 166, 173, 0.5);
91 | // 树状连线颜色
92 | --#{$prefix}-tree-line-color: #4c4d4f;
93 | }
94 |
--------------------------------------------------------------------------------
/src/table/cell/CheckboxCell.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | changeValue(e)"
7 | />
8 | {{
9 | column.field ? row[column.field] : ''
10 | }}
11 |
12 |
13 |
39 |
--------------------------------------------------------------------------------
/src/table/cell/ExpandCell.vue:
--------------------------------------------------------------------------------
1 |
2 |
18 |
19 |
33 |
--------------------------------------------------------------------------------
/src/table/cell/IndexCell.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{ indexValue }}
4 |
5 |
6 |
16 |
--------------------------------------------------------------------------------
/src/table/cell/LinkView.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
12 |
--------------------------------------------------------------------------------
/src/table/cell/PersonView.vue:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
14 |
15 |
44 |
--------------------------------------------------------------------------------
/src/table/cell/RadioCell.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{
5 | column.field ? row[column.field] : ''
6 | }}
7 |
8 |
9 |
26 |
--------------------------------------------------------------------------------
/src/table/cell/TextCell.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{ row?.[column?.field] || '' }}
4 |
5 |
6 |
19 |
--------------------------------------------------------------------------------
/src/table/cell/TreeCell.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 |
40 |
41 |
42 |
43 | {{ column.field ? row[column.field] : '' }}
44 |
45 |
46 |
47 |
48 |
69 |
89 |
--------------------------------------------------------------------------------
/src/table/cell/date/DateCover.vue:
--------------------------------------------------------------------------------
1 |
2 |
21 |
22 |
41 |
42 |
63 |
--------------------------------------------------------------------------------
/src/table/cell/date/DateDropdown.vue:
--------------------------------------------------------------------------------
1 |
2 | 时间选择器
3 |
4 |
23 |
24 |
34 |
--------------------------------------------------------------------------------
/src/table/cell/date/DateView.vue:
--------------------------------------------------------------------------------
1 |
2 | 2024/09/25
3 |
4 |
5 |
6 |
10 |
--------------------------------------------------------------------------------
/src/table/cell/link/LinkCover.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 哈哈哈
6 | x
7 |
8 |
9 | 嘻嘻嘻
10 | x
11 |
12 |
13 | 恩恩恩
14 | x
15 |
16 |
17 |
31 |
32 |
33 |
52 |
53 |
91 |
--------------------------------------------------------------------------------
/src/table/cell/link/LinkDropdown.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
10 |
哈哈哈
11 |
嘻嘻嘻
12 |
恩恩嗯
13 |
14 |
15 |
16 |
35 |
36 |
76 |
--------------------------------------------------------------------------------
/src/table/cell/link/LinkView.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
SelectView - {{ value }}
4 |
5 |
6 |
27 |
28 |
38 |
--------------------------------------------------------------------------------
/src/table/cell/select/SelectCover.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
{{ item.value }}
11 |
29 |
30 |
31 |
32 |
33 |
69 |
70 |
122 |
--------------------------------------------------------------------------------
/src/table/cell/select/SelectDropdown.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
10 |
哈哈哈
11 |
嘻嘻嘻
12 |
恩恩嗯
13 |
14 |
15 |
16 |
35 |
36 |
76 |
--------------------------------------------------------------------------------
/src/table/cell/select/SelectView.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
11 |
{{ item.value }}
12 |
13 |
14 |
15 |
16 |
47 |
48 |
100 |
--------------------------------------------------------------------------------
/src/table/header/Header.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
117 |
--------------------------------------------------------------------------------
/src/table/header/cell/HeaderCheckboxCell.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | changeValue(e)"
8 | />
9 | {{ column.title }}
10 |
11 |
12 |
39 |
40 |
--------------------------------------------------------------------------------
/src/table/header/cell/HeaderIndexCell.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{ column.title }}
4 |
5 |
6 |
12 |
13 |
--------------------------------------------------------------------------------
/src/table/header/cell/HeaderOrderCell.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{ column.title }}
4 |
5 |
6 |
12 |
13 |
--------------------------------------------------------------------------------
/src/table/header/cell/HeaderTextCell.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 | {{ column.title }}
7 |
8 |
9 |
22 |
--------------------------------------------------------------------------------
/src/table/row/ExpandRow.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | |
8 |
9 |
10 |
37 |
--------------------------------------------------------------------------------
/src/table/row/GroupRow.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
10 |
27 |
28 | 分组-{{ props.row.name }}
29 |
30 | |
31 |
32 |
33 |
61 |
--------------------------------------------------------------------------------
/src/template/GridTable.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
121 |
122 |
--------------------------------------------------------------------------------
/src/template/GridTableColumn.vue:
--------------------------------------------------------------------------------
1 |
2 | {{ field }}
3 |
4 |
15 |
20 |
21 |
--------------------------------------------------------------------------------
/src/utils/getCellFromEvent.ts:
--------------------------------------------------------------------------------
1 | export const getCellFromEvent = (e: MouseEvent) => {
2 | const cellRoot = e
3 | .composedPath()
4 | .find(
5 | (v) =>
6 | !(v as HTMLElement).classList?.contains('vtg-cell--unselectable') &&
7 | (v as HTMLElement).dataset?.colidx &&
8 | (v as HTMLElement).dataset?.rowidx,
9 | ) as HTMLElement;
10 |
11 | if (cellRoot && cellRoot.dataset) {
12 | const { colidx, rowidx, colspan, rowspan } = cellRoot.dataset;
13 | return {
14 | rowIndex: +(rowidx || 0),
15 | colIndex: +(colidx || 0),
16 | rowspan: +(rowspan || 1),
17 | colspan: +(colspan || 1),
18 | };
19 | }
20 |
21 | return undefined;
22 | };
23 |
--------------------------------------------------------------------------------
/src/utils/merge.ts:
--------------------------------------------------------------------------------
1 | import { type MergeCell } from '@/src/type';
2 |
3 | export function isInMergeCell(mergeInfo: MergeCell, rowIndex: number, colIndex: number) {
4 | return (
5 | mergeInfo.rowIndex <= rowIndex &&
6 | mergeInfo.rowIndex + mergeInfo.rowspan - 1 >= rowIndex &&
7 | mergeInfo.colIndex <= colIndex &&
8 | mergeInfo.colIndex + mergeInfo.colspan - 1 >= colIndex
9 | );
10 | }
11 |
12 | export const mergeMethods = (
13 | rowIndex: number,
14 | colIndex: number,
15 | ): {
16 | rowIndex: number;
17 | colIndex: number;
18 | rowspan: number;
19 | colspan: number;
20 | } | null => {
21 | if (colIndex === 0) {
22 | if (rowIndex % 2 === 0) {
23 | return {
24 | rowIndex: rowIndex,
25 | colIndex: colIndex,
26 | rowspan: 2,
27 | colspan: 1,
28 | };
29 | } else {
30 | return {
31 | rowIndex: rowIndex - 1,
32 | colIndex: colIndex,
33 | // rowspan: 2,
34 | // colspan: 1,
35 | rowspan: 0,
36 | colspan: 0,
37 | };
38 | }
39 | }
40 | if (colIndex === 1) {
41 | if (rowIndex % 2 === 1) {
42 | return {
43 | rowIndex: rowIndex,
44 | colIndex: colIndex,
45 | rowspan: 2,
46 | colspan: 1,
47 | };
48 | } else if (rowIndex > 0) {
49 | return {
50 | rowIndex: rowIndex - 1,
51 | colIndex: colIndex,
52 | rowspan: 2,
53 | colspan: 1,
54 | };
55 | }
56 | }
57 | return null;
58 | };
59 |
60 | export function getMergeInfo(
61 | merges: MergeCell[],
62 | rowIndex: number,
63 | colIndex: number,
64 | ): MergeCell | null {
65 | // 测试methods
66 | // return mergeMethods(rowIndex, colIndex);
67 | for (let i = 0; i < merges.length; i += 1) {
68 | if (isInMergeCell(merges[i], rowIndex, colIndex)) {
69 | return merges[i];
70 | }
71 | }
72 | return null;
73 | }
74 |
75 | /**
76 | * @description
77 | * calcRenderRect计算实际渲染的区域和边界并收集合并单元格信息
78 | * 该函数会在render之前被调用,以便计算实际渲染的区域和边界合并单元格
79 | * 该函数返回的是一个对象,包含了渲染的区域和边界合并单元格
80 | * @param merges {MergeCell[]} - 合并单元格的信息
81 | * @param originRect {Object} - 可视区域的信息
82 | * @return {
83 | * renderRect: { ys: number; ye: number; xs: number; xe: number },
84 | * merges: { topMerges: MergeCell[], leftMerges: MergeCell[], rightMerges: MergeCell[], bottomMerges: MergeCell[] }
85 | * } - 一个对象,包含了渲染的区域和边界合并单元格
86 | */
87 | export function calcRenderRect(
88 | merges: MergeCell[],
89 | originRect: { ys: number; ye: number; xs: number; xe: number },
90 | ) {
91 | const topMerges: MergeCell[] = [];
92 | const leftMerges: MergeCell[] = [];
93 | const rightMerges: MergeCell[] = [];
94 | const bottomMerges: MergeCell[] = [];
95 | const { ys: oys, ye: oye, xs: oxs, xe: oxe } = originRect;
96 | let rys = originRect.ys;
97 | let rye = originRect.ye;
98 | let rxs = originRect.xs;
99 | let rxe = originRect.xe;
100 |
101 | for (let y = oys; y <= oye; y++) {
102 | for (let x = oxs; x <= oxe; x++) {
103 | // 如果是第一行,那么可以算出rys
104 | if (y === oys) {
105 | const mergeInfo = getMergeInfo(merges, y, x);
106 | // console.warn('第一行', y, x, mergeInfo);
107 | if (mergeInfo) {
108 | const { rowIndex, colIndex, colspan } = mergeInfo;
109 | rys = Math.min(rys, rowIndex);
110 | rxs = Math.min(rxs, colIndex);
111 | rxe = Math.max(rxe, colIndex + colspan - 1);
112 | // 只有被上面的行合并的单元格才需要加进来
113 | if (y > rowIndex) {
114 | topMerges.push(mergeInfo);
115 | }
116 | }
117 | }
118 |
119 | // 左边列,不包含第一行和最后一行
120 | if (x === oxs) {
121 | const mergeInfo = getMergeInfo(merges, y, x);
122 | // console.warn('左边列', y, x, mergeInfo);
123 | if (mergeInfo) {
124 | const { colIndex } = mergeInfo;
125 | rxs = Math.min(rxs, colIndex);
126 | // 只有被左边的列合并的单元格才需要加进来
127 | if (x > colIndex) {
128 | leftMerges.push(mergeInfo);
129 | }
130 | }
131 | }
132 |
133 | // 右边列,不包含第一行和最后一行
134 | if (x === oxe) {
135 | const mergeInfo = getMergeInfo(merges, y, x);
136 | // console.warn('右边列', y, x, mergeInfo);
137 | if (mergeInfo) {
138 | const { colIndex, colspan } = mergeInfo;
139 | rxe = Math.max(rxe, colIndex + colspan - 1);
140 | // 只要是有合并信息的都加进去
141 | rightMerges.push(mergeInfo);
142 | }
143 | }
144 |
145 | // 如果是最后一行,那么可以算出rye
146 | if (y === oye) {
147 | const mergeInfo = getMergeInfo(merges, y, x);
148 | if (mergeInfo) {
149 | // 只要是有合并信息的都加进去
150 | const { rowIndex, rowspan } = mergeInfo;
151 | rye = Math.max(rye, rowIndex + rowspan - 1);
152 | bottomMerges.push(mergeInfo);
153 | }
154 | }
155 | }
156 | }
157 |
158 | return {
159 | renderRect: { ys: rys, ye: rye, xs: rxs, xe: rxe },
160 | merges: {
161 | topMerges,
162 | leftMerges,
163 | rightMerges,
164 | bottomMerges,
165 | },
166 | };
167 | }
168 |
--------------------------------------------------------------------------------
/todo.md:
--------------------------------------------------------------------------------
1 | # todo
2 |
3 | ## done
4 |
5 | - [x] 表头
6 |
7 | ## to-do
8 |
9 | - [ ] 表头合并单元格bug处理
10 |
11 | - [ ] 表格蒙层,全局处理单元格交互,非单元格内部处理,提升性能
12 |
13 | - [ ] 各类单元格
14 | - [ ] select(单元/多选)
15 | - [ ] 日期
16 | - [ ] 人员
17 | - [ ] checkbox
18 |
19 | ## bugfix
20 |
21 | - 高亮
22 |
23 | - 高亮列有bug
24 | - 高亮选中行列(highlight-select-row/col)和高亮hover行(highlight-hover-row/col)要分开
25 |
26 | - 列宽拖拽鼠标样式要改成拖拽
27 |
--------------------------------------------------------------------------------
/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@vue/tsconfig/tsconfig.dom.json",
3 | "include": ["env.d.ts", "src/**/*", "src/**/*.vue", "docs/**/*", "docs/**/*.vue"],
4 | "exclude": ["src/**/__tests__/*", "example/**/__tests__/*"],
5 | "compilerOptions": {
6 | "baseUrl": ".",
7 | "jsx": "preserve",
8 | "composite": true,
9 | "types": ["node"],
10 | "paths": {
11 | "vue-virt-grid": ["./src"],
12 | "@/src/*": ["./src/*"]
13 | }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.app.json",
3 | "exclude": ["docs/**/*"],
4 | "compilerOptions": {
5 | "emitDeclarationOnly": true, // 只输出声明文件(ts 产物)
6 | "declaration": true, // 自动生成声明文件
7 | "outDir": "lib",
8 | "rootDir": "./src",
9 | "jsx": "preserve",
10 | "jsxFactory": "h"
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "files": [],
3 | "references": [
4 | {
5 | "path": "./tsconfig.app.json"
6 | },
7 | {
8 | "path": "./tsconfig.node.json"
9 | },
10 | {
11 | "path": "./tsconfig.vitest.json"
12 | }
13 | ]
14 | }
15 |
--------------------------------------------------------------------------------
/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@tsconfig/node18/tsconfig.json",
3 | "include": [
4 | "vite.config.*",
5 | "vitest.config.*",
6 | "cypress.config.*",
7 | "nightwatch.conf.*",
8 | "playwright.config.*"
9 | ],
10 | "compilerOptions": {
11 | "composite": true,
12 | "module": "ESNext",
13 | "moduleResolution": "Bundler",
14 | "types": ["node"]
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/tsconfig.vitest.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.app.json",
3 | "exclude": [],
4 | "compilerOptions": {
5 | "composite": true,
6 | "lib": [],
7 | "types": ["node", "jsdom"]
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/vitest.config.ts:
--------------------------------------------------------------------------------
1 | import { fileURLToPath } from 'node:url';
2 | import { mergeConfig, defineConfig, configDefaults } from 'vitest/config';
3 | import viteConfig from './scripts/dev';
4 |
5 | export default mergeConfig(
6 | viteConfig,
7 | defineConfig({
8 | test: {
9 | environment: 'jsdom',
10 | exclude: [...configDefaults.exclude, 'e2e/*'],
11 | root: fileURLToPath(new URL('./', import.meta.url)),
12 | },
13 | }),
14 | );
15 |
--------------------------------------------------------------------------------