Repository: Tencent/tdesign-mobile-vue
Pull request #884 (2023-06-15 15:25:11)

fix(Table): resolved loading invalid
<!--
首先，感谢你的贡献！😄
请阅读并遵循 [TDesign 贡献指南](https://github.com/Tencent/tdesign/blob/main/docs/contributing.md)，填写以下 pull request 的信息。
PR 在维护者审核通过后会合并，谢谢！
-->

### 🤔 这个 PR 的性质是？

- [x] 日常 bug 修复
- [x] 新特性提交
- [ ] 文档改进
- [ ] 演示代码改进
- [x] 组件样式/交互改进
- [ ] CI/CD 改进
- [ ] 重构
- [ ] 代码风格优化
- [x] 测试用例
- [ ] 分支合并
- [ ] 其他

### 🔗 相关 Issue
### 相关 PR
common： https://github.com/Tencent/tdesign-common/pull/1410
<!--
1. 描述相关需求的来源，如相关的 issue 讨论链接。
-->

### 💡 需求背景和解决方案

<!--
1. 要解决的具体问题。
2. 列出最终的 API 实现和用法。
3. 涉及UI/交互变动需要有截图或 GIF。
-->

### 📝 更新日志

<!--
从用户角度描述具体变化，以及可能的 breaking change 和其他风险。
-->
- feat(Table):  新增 `loadingProps` 属性
- fix(Table): 修复 `loading` 属性无效的问题
- other(Table): 补充单元测试

- [ ] 本条 PR 不需要纳入 Changelog

### ☑️ 请求合并前的自查清单

⚠️ 请自检并全部**勾选全部选项**。⚠️

- [ ] 文档已补充或无须补充
- [ ] 代码演示已提供或无须提供
- [ ] TypeScript 定义已补充或无须补充
- [ ] Changelog 已提供或无须提供


github-actions[bot]: <a href="https://preview-pr884-tdesign-mobile-vue.surge.sh"><img height="96" alt="完成" src="https://user-images.githubusercontent.com/15634204/150816437-9f5bb788-cd67-4cbc-9897-b82d74e9aa65.png" /></a>
<!-- [工作流地址](https://github.com/Tencent/tdesign-mobile-vue/actions/runs/5280750504) -->
<!-- AUTO_PREVIEW_HOOK -->
github-actions[bot]: **Size Change:** +30 B (0%) 

**Total Size:** 473 kB

| Filename | Size | Change |
| :--- | :---: | :---: |
| `./dist/tdesign.js` | 277 kB | +6 B (0%) |
| `./dist/tdesign.min.js` | 133 kB | +24 B (0%) |

<details><summary>ℹ️ <strong>View Unchanged</strong></summary>

| Filename | Size |
| :--- | :---: |
| `./dist/reset.css` | 672 B |
| `./dist/tdesign.css` | 36.4 kB |
| `./dist/tdesign.min.css` | 26.2 kB |

</details>


github-actions[bot]: **Size Change:** +29 B (0%) 

**Total Size:** 473 kB

| Filename | Size | Change |
| :--- | :---: | :---: |
| `./dist/tdesign.js` | 276 kB | +4 B (0%) |
| `./dist/tdesign.min.js` | 133 kB | +25 B (0%) |

<details><summary>ℹ️ <strong>View Unchanged</strong></summary>

| Filename | Size |
| :--- | :---: |
| `./dist/reset.css` | 672 B |
| `./dist/tdesign.css` | 36.4 kB |
| `./dist/tdesign.min.css` | 26.2 kB |

</details>


github-actions[bot]: **Size Change:** -18 B (0%) 

**Total Size:** 473 kB

| Filename | Size | Change |
| :--- | :---: | :---: |
| `./dist/tdesign.js` | 276 kB | -28 B (0%) |
| `./dist/tdesign.min.js` | 133 kB | +10 B (0%) |

<details><summary>ℹ️ <strong>View Unchanged</strong></summary>

| Filename | Size |
| :--- | :---: |
| `./dist/reset.css` | 672 B |
| `./dist/tdesign.css` | 36.4 kB |
| `./dist/tdesign.min.css` | 26.2 kB |

</details>


github-actions[bot]: **Size Change:** -18 B (0%) 

**Total Size:** 473 kB

| Filename | Size | Change |
| :--- | :---: | :---: |
| `./dist/tdesign.js` | 276 kB | -28 B (0%) |
| `./dist/tdesign.min.js` | 133 kB | +10 B (0%) |

<details><summary>ℹ️ <strong>View Unchanged</strong></summary>

| Filename | Size |
| :--- | :---: |
| `./dist/reset.css` | 672 B |
| `./dist/tdesign.css` | 36.4 kB |
| `./dist/tdesign.min.css` | 26.2 kB |

</details>



Patch:
diff --git a/.github/workflows/issue-stale-close.temp.yml b/.github/workflows/issue-stale-close.temp.yml
deleted file mode 100644
index 864f210f..00000000
--- a/.github/workflows/issue-stale-close.temp.yml
+++ /dev/null
@@ -1,27 +0,0 @@
-# force copy from tencent/tdesign
-# 国际标准时间+8
-name: Close stale issues and PRs
-on:
-  schedule:
-    - cron: '50 5 * * *'
-
-jobs:
-  close-issues:
-    runs-on: ubuntu-latest
-    permissions:
-      issues: write
-      pull-requests: write
-    steps:
-      - uses: actions/stale@v4
-        with:
-          stale-issue-message: '这个 Issue 被标记为了过时 stale ，因为它已经持续 30 天没有任何活动了。删除 stale 标签或评论，否则将在 7 天内关闭。'
-          stale-pr-message: '这个 PR 已经过时了，因为它已经持续 45 天没有任何活动了。 删除 stale 的标签或评论，否则将在 10 天内关闭。'
-          close-issue-message: '此 Issue 被自动关闭，因为它自被标记为过时 stale 以来已闲置 7 天。'
-          close-pr-message: '此 PR 被自动关闭，因为它已经 stable 停滞了 10 天，没有任何活动。'
-          days-before-stale: 30
-          days-before-close: 7
-          days-before-pr-stale: 45
-          days-before-pr-close: 10
-          repo-token: ${{ secrets.GITHUB_TOKEN }}
-          exempt-issue-labels: 'WIP'
-          exempt-pr-labels: 'WIP'
diff --git a/site/mobile/components/header.vue b/site/mobile/components/header.vue
index 1767ac7f..96dcfef7 100644
--- a/site/mobile/components/header.vue
+++ b/site/mobile/components/header.vue
@@ -1,25 +1,18 @@
 <template>
   <div v-show="title" class="tdesign-demo-topnav">
     <div class="tdesign-demo-topnav-title">{{ title }}</div>
-    <chevron-left-icon class="tdesign-demo-topnav__back" name="chevron-left" @click="onBack" />
+    <chevron-left-icon class="tdesign-demo-topnav__back" @click="onBack" />
   </div>
 </template>
-<script lang="ts">
-import { computed, defineComponent } from 'vue';
+<script lang="ts" setup>
+import { computed } from 'vue';
 import { useRouter } from 'vue-router';
 import { ChevronLeftIcon } from 'tdesign-icons-vue-next';
-export default defineComponent({
-  components: { ChevronLeftIcon },
-  setup() {
-    const router = useRouter();
-    const title = computed(() => router.currentRoute.value.meta.title);
 
-    const onBack = () => {
-      if (history.length > 1) {
-        history.back();
-      }
-    };
-    return { onBack, title };
-  },
-});
+const router = useRouter();
+const title = computed(() => router.currentRoute.value.meta.title);
+
+const onBack = () => {
+  router.go(-1);
+};
 </script>
diff --git a/site/mobile/components/home.vue b/site/mobile/components/home.vue
index 4fb462ac..ca1c8445 100644
--- a/site/mobile/components/home.vue
+++ b/site/mobile/components/home.vue
@@ -2,7 +2,7 @@
   <div class="tdesign-mobile-home">
     <div class="tdesign-mobile-logo">
       <img src="https://tdesign.gtimg.com/site/TDesign.png" alt="logo" />
-      <p>TDesign Mobile Vue是由前端通用 UI 组件库 Oteam 发起，协同公司内有组件库开发经验的同学一起建设的组件库</p>
+      <p>TDesign 适配 Vue 的移动端组件库</p>
     </div>
     <t-collapse v-for="doc in docs.filter((x) => x.type === 'component')" :key="doc.title">
       <t-collapse-panel :name="doc.title" :title="doc.title">
diff --git a/site/mobile/router.ts b/site/mobile/router.ts
index 812bef91..d61dcab4 100644
--- a/site/mobile/router.ts
+++ b/site/mobile/router.ts
@@ -6,7 +6,7 @@ const { docs } = siteConfig;
 
 // 引入所有views下.vue文件
 // @ts-ignore
-const demoModules = import.meta.glob('../../src/**/demos/mobile.vue');
+const demoModules = import.meta.glob('../../src/**/demos/*.vue');
 
 function getDocsRoutes(docs: any[]): RouteRecordRaw[] {
   let docsRoutes: Array<RouteRecordRaw> = [];
@@ -32,15 +32,32 @@ function getDocsRoutes(docs: any[]): RouteRecordRaw[] {
       });
     }
   });
+
   return docsRoutes;
 }
 
+const componentInnerRoutes: RouteRecordRaw[] = [];
+const demoRouteGenerator = (compName, demos, title) => {
+  const ans = demos.map((demo) => ({
+    path: `/${compName}/${demo}`,
+    meta: { title },
+    component: demoModules[`../../src/${compName}/demos/${demo}.vue`],
+  }));
+
+  componentInnerRoutes.push(...ans);
+};
+
+// 组件需要多页面展示时，需要多个路由
+demoRouteGenerator('side-bar', ['base', 'switch', 'with-icon', 'custom'], 'SideBar 侧边栏');
+demoRouteGenerator('indexes', ['base', 'custom'], 'Indexes 索引');
+
 const routes: Array<RouteRecordRaw> = [
   {
     path: '/',
     component: Home,
   },
   ...getDocsRoutes(docs),
+  ...componentInnerRoutes,
 ];
 const router = createRouter({
   history: createWebHashHistory('/mobile.html'),
diff --git a/src/indexes/demos/mobile.vue b/src/indexes/demos/mobile.vue
index 06122ee3..66ea9389 100644
--- a/src/indexes/demos/mobile.vue
+++ b/src/indexes/demos/mobile.vue
@@ -1,5 +1,5 @@
 <template>
-  <div v-if="!showIndexes" class="tdesign-mobile-demo">
+  <div class="tdesign-mobile-demo">
     <h1 class="title">Indexes 索引</h1>
     <p class="summary">用于页面中信息快速检索，可以根据目录中的页码快速找到所需的内容。</p>
     <tdesign-demo-block title="01 组件类型" summary="基础索引类型">
@@ -17,33 +17,15 @@
       </div>
     </tdesign-demo-block>
   </div>
-  <div v-else>
-    <baseDemo v-if="type === 'base'" />
-    <custom v-if="type === 'custom'" />
-    <t-fab
-      :icon="iconFunc"
-      text="返回"
-      :style="{ 'z-index': 100, right: '16px', bottom: '32px' }"
-      :button-props="{ variant: 'outline' }"
-      @click="goBack"
-    />
-  </div>
 </template>
-<script setup lang="ts">
-import { ref, h } from 'vue';
-import { ChevronLeftDoubleIcon } from 'tdesign-icons-vue-next';
-import baseDemo from './base.vue';
-import custom from './custom.vue';
 
-const showIndexes = ref(false);
-const type = ref('letter');
-const iconFunc = () => h(ChevronLeftDoubleIcon);
+<script setup lang="ts">
+import { useRouter } from 'vue-router';
 
-const goBack = () => (showIndexes.value = false);
+const router = useRouter();
 
 const handleClick = (t = 'base') => {
-  showIndexes.value = true;
-  type.value = t;
+  router.push(`/indexes/${t}`);
 };
 </script>
 <style lang="less">
diff --git a/src/side-bar/demos/mobile.vue b/src/side-bar/demos/mobile.vue
index ec7be6b6..a50e037e 100644
--- a/src/side-bar/demos/mobile.vue
+++ b/src/side-bar/demos/mobile.vue
@@ -1,5 +1,5 @@
 <template>
-  <div v-if="!showSidebar" class="tdesign-mobile-demo">
+  <div class="tdesign-mobile-demo">
     <h1 class="title">SideBar 侧边栏</h1>
     <p class="summary">用于内容分类后的展示切换。</p>
     <tdesign-demo-block title="01 组件类型" summary="侧边导航用法" :padding="true">
@@ -21,27 +21,14 @@
       </t-button>
     </tdesign-demo-block>
   </div>
-
-  <div v-else>
-    <BaseDemo v-if="type === 'base'" />
-    <WithIconDemo v-if="type === 'with-icon'" />
-    <SwitchDemo v-if="type === 'switch'" />
-    <CustomDemo v-if="type === 'custom'" />
-  </div>
 </template>
 <script lang="ts" setup>
-import { ref } from 'vue';
-import BaseDemo from './base.vue';
-import SwitchDemo from './switch.vue';
-import WithIconDemo from './with-icon.vue';
-import CustomDemo from './custom.vue';
+import { useRouter } from 'vue-router';
 
-const showSidebar = ref(false);
-const type = ref('');
+const router = useRouter();
 
 const handleClick = (t = 'base') => {
-  showSidebar.value = true;
-  type.value = t;
+  router.push(`/side-bar/${t}`);
 };
 </script>
 
@@ -49,4 +36,8 @@ const handleClick = (t = 'base') => {
 .t-button {
   margin-bottom: 16px;
 }
+
+.tdesign-mobile-demo {
+  background-color: #fff;
+}
 </style>
diff --git a/src/side-bar/side-bar-item.vue b/src/side-bar/side-bar-item.vue
index 8777fa8b..2bb9d464 100644
--- a/src/side-bar/side-bar-item.vue
+++ b/src/side-bar/side-bar-item.vue
@@ -27,14 +27,13 @@ export default defineComponent({
   name,
   components: { TNode },
   props: SideBarItemProps,
-  setup(props, context) {
+  setup(props) {
     const internalInstance = getCurrentInstance();
     const { proxy } = internalInstance as ComponentInternalInstance;
     const sideBarProvide: any = inject('sideBarProvide', undefined);
     sideBarProvide.relation(proxy);
 
-    const index = computed(() => sideBarProvide.state.children.indexOf(proxy));
-
+    const index = computed(() => sideBarProvide.children.value.indexOf(proxy));
     const iconNode = computed(() => renderTNode(internalInstance, 'icon'));
     const isActive = computed(() => index.value === sideBarProvide.currentValue.value);
 
diff --git a/src/side-bar/side-bar.vue b/src/side-bar/side-bar.vue
index 7cf6b0e1..c3e7b19f 100644
--- a/src/side-bar/side-bar.vue
+++ b/src/side-bar/side-bar.vue
@@ -6,7 +6,7 @@
 </template>
 
 <script lang="ts">
-import { defineComponent, reactive, ComponentInternalInstance, provide, toRefs } from 'vue';
+import { defineComponent, ref, Ref, ComponentInternalInstance, provide, toRefs } from 'vue';
 import config from '../config';
 import SideBarProps from './props';
 import { TdSideBarProps } from './type';
@@ -28,16 +28,14 @@ export default defineComponent({
       'change',
     );
 
-    const state = reactive({
-      children: [] as ComponentInternalInstance[],
-    });
+    const children: Ref<ComponentInternalInstance[]> = ref([]);
 
     const relation = (child: ComponentInternalInstance) => {
-      child && state.children.push(child);
+      child && children.value.push(child);
     };
 
     const removeRelation = (child: ComponentInternalInstance) => {
-      state.children = state.children.filter((item) => item !== child);
+      children.value = children.value.filter((item) => item !== child);
     };
 
     const onClickItem = (cur: TdSideBarProps['value'], label: string) => {
@@ -47,7 +45,7 @@ export default defineComponent({
 
     provide('sideBarProvide', {
       ...props,
-      state,
+      children,
       currentValue,
       relation,
       removeRelation,
diff --git a/src/steps/steps.vue b/src/steps/steps.vue
index ef44c62a..63eae6d1 100644
--- a/src/steps/steps.vue
+++ b/src/steps/steps.vue
@@ -32,8 +32,12 @@ export default defineComponent({
       'change',
     );
 
-    const state = reactive({
-      children: [] as ComponentInternalInstance[],
+    interface TState {
+      children: ComponentInternalInstance[];
+    }
+
+    const state: TState = reactive({
+      children: [],
     });
 
     const relation = (child: ComponentInternalInstance) => {
diff --git a/src/table/base-table-props.ts b/src/table/base-table-props.ts
index ee8ee051..8df1a1e6 100644
--- a/src/table/base-table-props.ts
+++ b/src/table/base-table-props.ts
@@ -42,6 +42,10 @@ export default {
     type: [Boolean, Function] as PropType<TdBaseTableProps['loading']>,
     default: undefined,
   },
+  /** 透传加载组件全部属性 */
+  loadingProps: {
+    type: Object as PropType<TdBaseTableProps['loadingProps']>,
+  },
   /** 表格最大高度，超出后会出现滚动条。示例：100, '30%', '300'。值为数字类型，会自动加上单位 px */
   maxHeight: {
     type: [String, Number] as PropType<TdBaseTableProps['maxHeight']>,
diff --git a/src/table/base-table.vue b/src/table/base-table.vue
index 0c9f607d..3eec3bde 100644
--- a/src/table/base-table.vue
+++ b/src/table/base-table.vue
@@ -39,7 +39,7 @@
           </tr>
         </thead>
         <tbody :class="tbodyClasses">
-          <tr v-if="empty" :class="tableBaseClass.emptyRow">
+          <tr v-if="renderContentEmpty" :class="tableBaseClass.emptyRow">
             <td :colspan="columns?.length">
               <div :class="tableBaseClass.empty">
                 <t-node :content="renderContentEmpty"></t-node>
@@ -74,20 +74,23 @@
           </tr>
         </tbody>
       </table>
+      <div v-if="loadingContent" :class="loadingClasses">
+        <t-node :content="loadingContent"></t-node>
+      </div>
     </div>
   </div>
 </template>
 
 <script lang="ts">
-import { defineComponent, computed, toRefs, getCurrentInstance } from 'vue';
+import { defineComponent, computed, toRefs, getCurrentInstance, h } from 'vue';
 import get from 'lodash/get';
 import TdBaseTableProps from './base-table-props';
 import config from '../config';
 import useClassName from './hooks/useClassName';
 import useStyle, { formatCSSUnit } from './hooks/useStyle';
 import { renderTNode, TNode, useEmitEvent } from '../shared';
-import { formatClassNames } from './utils';
 import { TableRowData } from './type';
+import TLoading from '../loading';
 
 const { prefix } = config;
 const name = `${prefix}-base-table`;
@@ -139,6 +142,10 @@ export default defineComponent({
     const internalInstance = getCurrentInstance();
     const renderContentEmpty = computed(() => renderTNode(internalInstance, 'empty'));
     const renderCellEmptyContent = computed(() => renderTNode(internalInstance, 'cellEmptyContent'));
+    const loadingClasses = computed(() => [`${classPrefix}-table__loading--full`]);
+    const loadingContent = computed(() =>
+      renderTNode(internalInstance, 'loading', { defaultNode: h(TLoading, { ...props.loadingProps }) }),
+    );
 
     const onInnerVirtualScroll = (e: WheelEvent) => {
       emitEvent('scroll', { params: e });
@@ -161,8 +168,9 @@ export default defineComponent({
       ellipsisClasses,
       renderContentEmpty,
       renderCellEmptyContent,
+      loadingClasses,
+      loadingContent,
       formatCSSUnit,
-      formatClassNames,
       onInnerVirtualScroll,
       get,
       handleCellClick,
diff --git a/src/table/table.en-US.md b/src/table/table.en-US.md
index 6ce56073..d69dd79c 100644
--- a/src/table/table.en-US.md
+++ b/src/table/table.en-US.md
@@ -13,6 +13,7 @@ empty | String / Slot / Function | '' | empty text or empty element。Typescript
 fixedRows | Array | - | Typescript：`Array<number>` | N
 height | String / Number | - | table height | N
 loading | Boolean / Slot / Function | undefined | loading state table。Typescript：`boolean \| TNode`。[see more ts definition](https://github.com/Tencent/tdesign-mobile-vue/blob/develop/src/common.ts) | N
+loadingProps | Object | - | Typescript：`Partial<LoadingProps>`，[Loading API Documents](./loading?tab=api)。| N
 maxHeight | String / Number | - | table max height | N
 rowKey | String | 'id' | required。unique key for each row data | Y
 showHeader | Boolean | true | show table header | N
diff --git a/src/table/table.md b/src/table/table.md
index a8c83181..504e0ede 100644
--- a/src/table/table.md
+++ b/src/table/table.md
@@ -13,6 +13,7 @@ empty | String / Slot / Function | '' | 空表格呈现样式，支持全局配
 fixedRows | Array | - | 【开发中】固定行（冻结行），示例：[M, N]，表示冻结表头 M 行和表尾 N 行。M 和 N 值为 0 时，表示不冻结行。TS 类型：`Array<number>` | N
 height | String / Number | - | 表格高度，超出后会出现滚动条。示例：100,  '30%',  '300'。值为数字类型，会自动加上单位 px。如果不是绝对固定表格高度，建议使用 `maxHeight` | N
 loading | Boolean / Slot / Function | undefined | 加载中状态。值为 `true` 会显示默认加载中样式，可以通过 Function 和 插槽 自定义加载状态呈现内容和样式。值为 `false` 则会取消加载状态。TS 类型：`boolean \| TNode`。[通用类型定义](https://github.com/Tencent/tdesign-mobile-vue/blob/develop/src/common.ts) | N
+loadingProps | Object | - | 透传加载组件全部属性。TS 类型：`Partial<LoadingProps>`，[Loading API Documents](./loading?tab=api)。 | N
 maxHeight | String / Number | - | 表格最大高度，超出后会出现滚动条。示例：100, '30%', '300'。值为数字类型，会自动加上单位 px | N
 rowKey | String | 'id' | 必需。唯一标识一行数据的字段名，来源于 `data` 中的字段。如果是字段嵌套多层，可以设置形如 `item.a.id` 的方法 | Y
 showHeader | Boolean | true | 是否显示表头 | N
diff --git a/src/table/type.ts b/src/table/type.ts
index d9a9d75b..8e8e2656 100644
--- a/src/table/type.ts
+++ b/src/table/type.ts
@@ -5,6 +5,7 @@
  * */
 
 import { TNode, ClassName } from '../common';
+import { LoadingProps } from '../loading';
 
 export interface TdBaseTableProps<T extends TableRowData = TableRowData> {
   /**
@@ -43,6 +44,10 @@ export interface TdBaseTableProps<T extends TableRowData = TableRowData> {
    * 加载中状态。值为 `true` 会显示默认加载中样式，可以通过 Function 和 插槽 自定义加载状态呈现内容和样式。值为 `false` 则会取消加载状态
    */
   loading?: boolean | TNode;
+  /**
+   * 透传加载组件全部属性
+   */
+  loadingProps?: Partial<LoadingProps>;
   /**
    * 表格最大高度，超出后会出现滚动条。示例：100, '30%', '300'。值为数字类型，会自动加上单位 px
    */
diff --git a/src/table/utils.ts b/src/table/utils.ts
deleted file mode 100644
index 49230162..00000000
--- a/src/table/utils.ts
+++ /dev/null
@@ -1,100 +0,0 @@
-import isFunction from 'lodash/isFunction';
-import get from 'lodash/get';
-import { CellData, TableColumnClassName, TableRowData } from './type';
-
-export function toString(obj: any): string {
-  return Object.prototype.toString.call(obj).slice(8, -1).toLowerCase();
-}
-
-export function debounce<T = any>(fn: Function, delay = 200): () => void {
-  let timer: ReturnType<typeof setTimeout>;
-  return function newFn(this: T, ...args: Array<any>): void {
-    // eslint-disable-next-line @typescript-eslint/no-this-alias
-    const context = this;
-    clearTimeout(timer);
-    timer = setTimeout(() => {
-      fn.apply(context, args);
-    }, delay);
-  };
-}
-
-export interface FormatRowAttributesParams {
-  row: TableRowData;
-  rowIndex: number;
-  type: 'body' | 'foot';
-}
-
-export function formatClassNames(
-  classNames: TableColumnClassName<TableRowData> | TableColumnClassName<TableRowData>[],
-  params: CellData<TableRowData>,
-) {
-  const classes = classNames instanceof Array ? classNames : [classNames];
-  const arr: any[] = [];
-  for (let i = 0, len = classes.length; i < len; i++) {
-    const cls = classes[i];
-    if (isFunction(cls)) {
-      arr.push(cls(params));
-    } else {
-      arr.push(cls);
-    }
-  }
-  return arr;
-}
-
-export const INNER_PRE_NAME = '@@inner-';
-
-export enum SCROLL_DIRECTION {
-  X = 'x',
-  Y = 'y',
-  UNKNOWN = 'unknown',
-}
-
-let preScrollLeft: any;
-let preScrollTop: any;
-
-export const getScrollDirection = (scrollLeft: number, scrollTop: number): SCROLL_DIRECTION => {
-  let direction = SCROLL_DIRECTION.UNKNOWN;
-  if (preScrollTop !== scrollTop) {
-    direction = SCROLL_DIRECTION.Y;
-  } else if (preScrollLeft !== scrollLeft) {
-    direction = SCROLL_DIRECTION.X;
-  }
-  preScrollTop = scrollTop;
-  preScrollLeft = scrollLeft;
-  return direction;
-};
-
-export function getEditableKeysMap(keys: Array<string | number>, list: any[], rowKey: string) {
-  const map: { [key: string | number]: boolean } = {};
-  for (let i = 0, len = list.length; i < len; i++) {
-    const rowValue = get(list[i], rowKey);
-    if (keys.includes(rowValue)) {
-      map[rowValue] = true;
-    }
-  }
-  return map;
-}
-
-export function getColumnDataByKey(columns: any[], colKey: string): any {
-  for (let i = 0, len = columns.length; i < len; i++) {
-    if (columns[i].colKey === colKey) return columns[i];
-    if (columns[i].children?.length) {
-      const t = getColumnDataByKey(columns[i].children, colKey);
-      if (t) return t;
-    }
-  }
-  return null;
-}
-
-export function getColumnIndexByKey(columns: any[], colKey: string): number {
-  for (let i = 0, len = columns.length; i < len; i++) {
-    if (columns[i].colKey === colKey) {
-      return i;
-    }
-    if (columns[i].children?.length) {
-      const t = getColumnDataByKey(columns[i].children, colKey);
-      if (t) return i;
-    }
-  }
-  return -1;
-}


Test patch:
diff --git a/src/table/__test__/__snapshots__/demo.test.jsx.snap b/src/table/__test__/__snapshots__/demo.test.jsx.snap
index dcb3e99f..1979d1a8 100644
--- a/src/table/__test__/__snapshots__/demo.test.jsx.snap
+++ b/src/table/__test__/__snapshots__/demo.test.jsx.snap
@@ -479,6 +479,7 @@ exports[`Table > Table baseVue demo works fine 1`] = `
           
         </tbody>
       </table>
+      <!--v-if-->
     </div>
   </div>
 </div>
@@ -963,6 +964,7 @@ exports[`Table > Table borderedVue demo works fine 1`] = `
           
         </tbody>
       </table>
+      <!--v-if-->
     </div>
   </div>
 </div>
@@ -1481,6 +1483,7 @@ exports[`Table > Table mobileVue demo works fine 1`] = `
                 
               </tbody>
             </table>
+            <!--v-if-->
           </div>
         </div>
       </div>
@@ -1783,6 +1786,7 @@ exports[`Table > Table mobileVue demo works fine 1`] = `
                 
               </tbody>
             </table>
+            <!--v-if-->
           </div>
         </div>
       </div>
@@ -2285,6 +2289,7 @@ exports[`Table > Table mobileVue demo works fine 1`] = `
                 
               </tbody>
             </table>
+            <!--v-if-->
           </div>
         </div>
       </div>
@@ -2786,6 +2791,7 @@ exports[`Table > Table mobileVue demo works fine 1`] = `
                 
               </tbody>
             </table>
+            <!--v-if-->
           </div>
         </div>
       </div>
@@ -3075,6 +3081,7 @@ exports[`Table > Table scrollVue demo works fine 1`] = `
           
         </tbody>
       </table>
+      <!--v-if-->
     </div>
   </div>
 </div>
@@ -3560,6 +3567,7 @@ exports[`Table > Table stripeVue demo works fine 1`] = `
           
         </tbody>
       </table>
+      <!--v-if-->
     </div>
   </div>
 </div>
diff --git a/src/table/__test__/index.test.jsx b/src/table/__test__/index.test.jsx
new file mode 100644
index 00000000..8f8edcdb
--- /dev/null
+++ b/src/table/__test__/index.test.jsx
@@ -0,0 +1,236 @@
+import { mount } from '@vue/test-utils';
+import TTable from '../base-table.vue';
+import config from '../../config';
+import { nextTick } from 'vue';
+
+const { prefix } = config;
+const name = `${prefix}-table`;
+
+const data = new Array(5).fill(null).map((item, index) => ({
+  index: index + 1,
+  applicant: `内容${index + 1}`,
+  status: index % 2,
+  channel: '内容',
+  detail: {
+    email: '内容',
+  },
+}));
+
+const columns = [
+  { colKey: 'applicant', title: '标题', align: 'center', ellipsis: true },
+  {
+    colKey: 'status',
+    title: '标题',
+    ellipsis: true,
+  },
+  { colKey: 'channel', title: '标题', ellipsis: true },
+  { colKey: 'detail.email', title: '标题', ellipsis: true },
+];
+
+describe('tableLayout', () => {
+  it(': tableLayout', async () => {
+    const wrapper = mount({
+      render() {
+        return <TTable rowKey="index" data={data} columns={columns}></TTable>;
+      },
+    });
+    // default value is fixed
+    expect(wrapper.find(`.${name}--layout-fixed`).exists()).toBeTruthy();
+  });
+});
+
+describe('bordered', () => {
+  it(': bordered', async () => {
+    const wrapper = mount({
+      render() {
+        return <TTable rowKey="index" data={data} columns={columns} bordered></TTable>;
+      },
+    });
+    expect(wrapper.find(`.${name}--bordered`).exists()).toBeTruthy();
+  });
+});
+
+describe('empty', () => {
+  it(': empty', async () => {
+    const wrapper = mount({
+      render() {
+        return <TTable rowKey="index" data={[]} columns={columns} empty="暂无数据"></TTable>;
+      },
+    });
+    expect(wrapper.find('.t-table__empty').text()).toBe('暂无数据');
+  });
+
+  it(': empty works fine as a function ', () => {
+    const emptyText = 'Empty Data Rendered By Function';
+    const wrapper = mount({
+      render() {
+        return (
+          <TTable
+            rowKey="index"
+            data={[]}
+            empty={() => <div class="render-function-class">{emptyText}</div>}
+            columns={columns}
+          ></TTable>
+        );
+      },
+    });
+    expect(wrapper.find('.t-table__empty').exists()).toBeTruthy();
+    expect(wrapper.find('.render-function-class').exists()).toBeTruthy();
+    expect(wrapper.find('.t-table__empty').text()).toBe(emptyText);
+  });
+
+  it(': empty works fine as slot', () => {
+    const emptyText = 'Empty Data Rendered By Slots';
+    const wrapper = mount({
+      render() {
+        return (
+          <TTable
+            rowKey="index"
+            data={[]}
+            v-slots={{ empty: () => <div class="slots-empty-class">{emptyText}</div> }}
+            columns={columns}
+          ></TTable>
+        );
+      },
+    });
+    expect(wrapper.find('.t-table__empty').exists()).toBeTruthy();
+    expect(wrapper.find('.slots-empty-class').exists()).toBeTruthy();
+    expect(wrapper.find('.t-table__empty').text()).toBe(emptyText);
+  });
+});
+
+describe('loading', () => {
+  it(': loading', async () => {
+    const wrapper = mount({
+      render() {
+        return <TTable rowKey="index" loading={true} data={data} columns={columns}></TTable>;
+      },
+    });
+    expect(wrapper.find('.t-loading').exists()).toBeTruthy();
+    expect(wrapper.find('.t-icon-loading').exists()).toBeTruthy();
+    expect(wrapper.find('.t-loading__text').exists()).toBeFalsy();
+  });
+
+  it(': loading works fine as function', async () => {
+    const wrapper = mount({
+      render() {
+        return <TTable rowKey="index" loading={() => 'loading text'} data={data} columns={columns}></TTable>;
+      },
+    });
+    expect(wrapper.find('.t-loading').exists()).toBeFalsy();
+    expect(wrapper.find('.t-icon-loading').exists()).toBeFalsy();
+    expect(wrapper.find('.t-table__loading--full').text()).toBe('loading text');
+  });
+
+  it(': loading works fine as slot', async () => {
+    const loadingText = 'loading text';
+    const wrapper = mount({
+      render() {
+        return (
+          <TTable
+            rowKey="index"
+            v-slots={{ loading: () => <div class="slots-loading-class">{loadingText}</div> }}
+            data={data}
+            columns={columns}
+          ></TTable>
+        );
+      },
+    });
+    expect(wrapper.find('.t-loading').exists()).toBeFalsy();
+    expect(wrapper.find('.t-icon-loading').exists()).toBeFalsy();
+    expect(wrapper.find('.t-table__loading--full').text()).toBe('loading text');
+  });
+
+  it(': loadingProps', () => {
+    const wrapper = mount({
+      render() {
+        return (
+          <TTable
+            rowKey="index"
+            data={data}
+            columns={columns}
+            loading={true}
+            loadingProps={{ indicator: false, text: 'function loading' }}
+          ></TTable>
+        );
+      },
+    });
+    expect(wrapper.find('.t-loading').exists()).toBeTruthy();
+    expect(wrapper.find('.t-icon-loading').exists()).toBeFalsy();
+    expect(wrapper.find('.t-loading__text').exists()).toBeTruthy();
+    expect(wrapper.find('.t-loading__text').text()).toBe('function loading');
+  });
+});
+
+describe('verticalAlign', () => {
+  // 行内容上下方向对齐
+  it(': verticalAlign', async () => {
+    const wrapper = mount({
+      render() {
+        return <TTable rowKey="index" data={data} columns={columns}></TTable>;
+      },
+    });
+    // default value is middle
+    expect(wrapper.classes('t-vertical-align-middle')).toBeTruthy();
+  });
+});
+
+describe('columns', () => {
+  it(': columns.align', () => {
+    const columns = [
+      { title: 'Index', colKey: 'index', align: 'center' },
+      { title: 'Instance', colKey: 'instance', align: 'left' },
+      { title: 'description', colKey: 'instance' },
+      { title: 'Owner', colKey: 'owner', align: 'right' },
+    ];
+    const wrapper = mount({
+      render() {
+        return <TTable rowKey="index" data={data} columns={columns}></TTable>;
+      },
+    });
+    const firstTrWrapper = wrapper.find('tbody > tr');
+    const tdList = firstTrWrapper.findAll('td');
+    expect(tdList[0].classes('t-align-center')).toBeTruthy();
+    expect(tdList[1].classes('t-align-left')).toBeFalsy();
+    expect(tdList[2].classes('t-align-left')).toBeFalsy();
+    expect(tdList[3].classes('t-align-right')).toBeTruthy();
+  });
+});
+
+describe('event', () => {
+  it(': onCellClick', async () => {
+    const fn = vi.fn();
+    const wrapper = mount({
+      render() {
+        return <TTable rowKey="index" data={data} onCellClick={fn} columns={columns}></TTable>;
+      },
+    });
+    wrapper.find('td').trigger('click');
+    await wrapper.vm.$nextTick();
+    expect(fn).toHaveBeenCalled();
+  });
+
+  it(': onRowClick', async () => {
+    const fn = vi.fn();
+    const wrapper = mount({
+      render() {
+        return <TTable rowKey="index" data={data} onRowClick={fn} columns={columns}></TTable>;
+      },
+    });
+    wrapper.find('tbody').find('tr').trigger('click');
+    await wrapper.vm.$nextTick();
+    expect(fn).toHaveBeenCalled();
+  });
+
+  it(': onScroll', async () => {
+    const fn = vi.fn();
+    const wrapper = mount({
+      render() {
+        return <TTable rowKey="index" data={data} onScroll={fn} columns={columns}></TTable>;
+      },
+    });
+    wrapper.find('.t-table__content').trigger('wheel');
+    await wrapper.vm.$nextTick();
+    expect(fn).toHaveBeenCalled();
+  });
+});