Przeglądaj źródła

refactor(table): refactor table #150 #148 #146 #130 #76

vben 4 lat temu
rodzic
commit
9c2f3f30bb
35 zmienionych plików z 1099 dodań i 454 usunięć
  1. 51 8
      CHANGELOG.zh_CN.md
  2. 8 0
      mock/demo/table-demo.ts
  3. 3 0
      src/components/Form/index.ts
  4. 4 1
      src/components/Form/src/components/ApiSelect.vue
  5. 5 3
      src/components/Menu/src/useOpenKeys.ts
  6. 1 2
      src/components/Table/index.ts
  7. 34 43
      src/components/Table/src/BasicTable.vue
  8. 4 4
      src/components/Table/src/componentMap.ts
  9. 2 1
      src/components/Table/src/components/EditTableHeaderIcon.vue
  10. 55 0
      src/components/Table/src/components/HeaderCell.vue
  11. 3 4
      src/components/Table/src/components/TableAction.vue
  12. 33 0
      src/components/Table/src/components/editable/CellComponent.ts
  13. 359 0
      src/components/Table/src/components/editable/EditableCell.vue
  14. 26 0
      src/components/Table/src/components/editable/helper.ts
  15. 52 0
      src/components/Table/src/components/editable/index.ts
  16. 0 241
      src/components/Table/src/components/renderEditable.tsx
  17. 0 14
      src/components/Table/src/components/renderExpandIcon.tsx
  18. 1 1
      src/components/Table/src/components/settings/ColumnSetting.vue
  19. 4 0
      src/components/Table/src/const.ts
  20. 65 8
      src/components/Table/src/hooks/useColumns.ts
  21. 59 3
      src/components/Table/src/hooks/useDataSource.ts
  22. 38 10
      src/components/Table/src/hooks/useTable.ts
  23. 5 2
      src/components/Table/src/hooks/useTableScroll.ts
  24. 9 1
      src/components/Table/src/props.ts
  25. 0 39
      src/components/Table/src/style/editable-cell.less
  26. 10 6
      src/components/Table/src/style/index.less
  27. 1 2
      src/components/Table/src/types/componentType.ts
  28. 42 14
      src/components/Table/src/types/table.ts
  29. 5 0
      src/layouts/default/sider/MixSider.vue
  30. 9 0
      src/router/menus/modules/demo/comp.ts
  31. 7 4
      src/utils/dateUtil.ts
  32. 100 26
      src/views/demo/table/EditCellTable.vue
  33. 96 15
      src/views/demo/table/EditRowTable.vue
  34. 1 1
      src/views/demo/table/FixedColumn.vue
  35. 7 1
      src/views/demo/table/tableData.tsx

+ 51 - 8
CHANGELOG.zh_CN.md

@@ -1,5 +1,55 @@
 ## Wip
 
+### ✨ 表格破坏性更新
+
+- 重构了可编辑单元格及可编辑行。具体看示例。写法已改变。针对可编辑表格。
+
+- 表格编辑支持表单校验
+
+- 在表格列配置增加了以下配置
+
+```bash
+{
+
+  # 默认是否显示列。不显示的可以在列配置打开
+  defaultHidden?: boolean;
+  # 列头右侧帮助文本
+  helpMessage?: string | string[];
+  # 自定义格式化 单元格内容。 支持时间/枚举自动转化
+  format?: CellFormat;
+
+  # Editable
+  # 是否是可编辑单元格
+  edit?: boolean;
+  # 是否是可编辑行
+  editRow?: boolean;
+  # 编辑状态。
+  editable?: boolean;
+  #  编辑组件
+  editComponent?: ComponentType;
+  # 所对应组件的参数
+  editComponentProps?: Recordable;
+  # 校验
+  editRule?: boolean | ((text: string, record: Recordable) => Promise<string>);
+  # 值枚举转化
+  editValueMap?: (value: any) => string;
+  # 触发编辑正航
+  record.onEditRow?: () => void;
+}
+
+```
+
+### ✨ 表格重构
+
+- 新增`clickToRowSelect`属性。用于控制点击行是否选中勾选框
+- 监听行点击事件
+- 表格列配置按钮增加 列拖拽,列固定功能。
+- 表格列配置新增`defaultHidden` 属性。用于默认隐藏。可在表格列配置勾选显示
+- 更强大的列配置
+- useTable:支持动态改变参数。可以传入`Ref`类型与`Computed`类型进行动态更改
+- useTable:新增返回 `getForm`函数。可以用于操作表格内的表单
+- 修复表格已知的问题
+
 ### ✨ Features
 
 - 新增 `v-ripple`水波纹指令
@@ -12,14 +62,6 @@
 - form: 新增远程下拉`ApiSelect`及示例
 - form: 新增`autoFocusFirstItem`配置。用于配置是否聚焦表单第一个输入框
 - useForm: 支持动态改变参数。可以传入`Ref`类型与`Computed`类型进行动态更改
-- table: 新增`clickToRowSelect`属性。用于控制点击行是否选中勾选狂
-- table: 监听行点击事件
-- table: 表格列配置按钮增加 列拖拽,列固定功能。
-- table:表格列配置新增`defaultHidden` 属性。用于默认隐藏。可在表格列配置勾选显示
-
-### ✨ Refactor
-
-- 重构表单,解决已知 bug
 
 ### ⚡ Performance Improvements
 
@@ -30,6 +72,7 @@
 ### 🎫 Chores
 
 - 升级`ant-design-vue`到`2.0.0-rc.7`
+- 升级`vue`到`3.0.5`
 
 ### 🐛 Bug Fixes
 

+ 8 - 0
mock/demo/table-demo.ts

@@ -10,6 +10,14 @@ const demoList = (() => {
       endTime: '@datetime',
       address: '@city()',
       name: '@cname()',
+      name1: '@cname()',
+      name2: '@cname()',
+      name3: '@cname()',
+      name4: '@cname()',
+      name5: '@cname()',
+      name6: '@cname()',
+      name7: '@cname()',
+      name8: '@cname()',
       'no|100000-10000000': 100000,
       'status|1': ['normal', 'enable', 'disable'],
     });

+ 3 - 0
src/components/Form/index.ts

@@ -9,4 +9,7 @@ export * from './src/types/formItem';
 export { useComponentRegister } from './src/hooks/useComponentRegister';
 export { useForm } from './src/hooks/useForm';
 
+export { default as ApiSelect } from './src/components/ApiSelect.vue';
+export { default as RadioButtonGroup } from './src/components/RadioButtonGroup.vue';
+
 export { BasicForm };

+ 4 - 1
src/components/Form/src/components/ApiSelect.vue

@@ -50,7 +50,8 @@
       labelField: propTypes.string.def('label'),
       valueField: propTypes.string.def('value'),
     },
-    setup(props) {
+    emits: ['options-change', 'change'],
+    setup(props, { emit }) {
       const options = ref<OptionsItem[]>([]);
       const loading = ref(false);
       const attrs = useAttrs();
@@ -86,11 +87,13 @@
           const res = await api(props.params);
           if (Array.isArray(res)) {
             options.value = res;
+            emit('options-change', unref(options));
             return;
           }
           if (props.resultField) {
             options.value = get(res, props.resultField) || [];
           }
+          emit('options-change', unref(options));
         } catch (error) {
           console.warn(error);
         } finally {

+ 5 - 3
src/components/Menu/src/useOpenKeys.ts

@@ -15,7 +15,7 @@ export function useOpenKeys(
   mode: Ref<MenuModeEnum>,
   accordion: Ref<boolean>
 ) {
-  const { getCollapsed } = useMenuSetting();
+  const { getCollapsed, getIsMixSidebar } = useMenuSetting();
 
   function setOpenKeys(path: string) {
     if (mode.value === MenuModeEnum.HORIZONTAL) {
@@ -30,7 +30,9 @@ export function useOpenKeys(
   }
 
   const getOpenKeys = computed(() => {
-    return unref(getCollapsed) ? menuState.collapsedOpenKeys : menuState.openKeys;
+    const collapse = unref(getIsMixSidebar) ? false : unref(getCollapsed);
+
+    return collapse ? menuState.collapsedOpenKeys : menuState.openKeys;
   });
 
   /**
@@ -42,7 +44,7 @@ export function useOpenKeys(
   }
 
   function handleOpenChange(openKeys: string[]) {
-    if (unref(mode) === MenuModeEnum.HORIZONTAL || !unref(accordion)) {
+    if (unref(mode) === MenuModeEnum.HORIZONTAL || !unref(accordion) || unref(getIsMixSidebar)) {
       menuState.openKeys = openKeys;
     } else {
       // const menuList = toRaw(menus.value);

+ 1 - 2
src/components/Table/index.ts

@@ -3,7 +3,6 @@ import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
 export { default as BasicTable } from './src/BasicTable.vue';
 export { default as TableAction } from './src/components/TableAction.vue';
 // export { default as TableImg } from './src/components/TableImg.vue';
-export { renderEditableCell, renderEditableRow } from './src/components/renderEditable';
 export { default as EditTableHeaderIcon } from './src/components/EditTableHeaderIcon.vue';
 
 export const TableImg = createAsyncComponent(() => import('./src/components/TableImg.vue'));
@@ -17,4 +16,4 @@ export { useTable } from './src/hooks/useTable';
 
 export type { FormSchema, FormProps } from '/@/components/Form/src/types/form';
 
-export type { EditRecordRow } from './src/components/renderEditable';
+export type { EditRecordRow } from './src/components/editable';

+ 34 - 43
src/components/Table/src/BasicTable.vue

@@ -34,19 +34,19 @@
       <template #[item]="data" v-for="item in Object.keys($slots)">
         <slot :name="item" v-bind="data" />
       </template>
+      <template #[`header-${column.dataIndex}`] v-for="column in columns" :key="column.dataIndex">
+        <HeaderCell :column="column" />
+      </template>
     </Table>
   </div>
 </template>
 <script lang="ts">
-  import type { BasicTableProps, TableActionType, SizeType, SorterResult } from './types/table';
-  import { PaginationProps } from './types/pagination';
+  import type { BasicTableProps, TableActionType, SizeType } from './types/table';
 
   import { defineComponent, ref, computed, unref } from 'vue';
   import { Table } from 'ant-design-vue';
   import { BasicForm, useForm } from '/@/components/Form/index';
 
-  import { isFunction } from '/@/utils/is';
-
   import { omit } from 'lodash-es';
 
   import { usePagination } from './hooks/usePagination';
@@ -61,15 +61,20 @@
   import { createTableContext } from './hooks/useTableContext';
   import { useTableFooter } from './hooks/useTableFooter';
   import { useTableForm } from './hooks/useTableForm';
+  import { useExpose } from '/@/hooks/core/useExpose';
+  import { useDesign } from '/@/hooks/web/useDesign';
 
   import { basicProps } from './props';
-  import { useExpose } from '/@/hooks/core/useExpose';
+  import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
 
   import './style/index.less';
-  import { useDesign } from '/@/hooks/web/useDesign';
   export default defineComponent({
     props: basicProps,
-    components: { Table, BasicForm },
+    components: {
+      Table,
+      BasicForm,
+      HeaderCell: createAsyncComponent(() => import('./components/HeaderCell.vue')),
+    },
     emits: [
       'fetch-success',
       'fetch-error',
@@ -80,6 +85,8 @@
       'row-contextmenu',
       'row-mouseenter',
       'row-mouseleave',
+      'edit-end',
+      'edit-cancel',
     ],
     setup(props, { attrs, emit, slots }) {
       const tableElRef = ref<ComponentRef>(null);
@@ -96,15 +103,19 @@
 
       const { getLoading, setLoading } = useLoading(getProps);
       const { getPaginationInfo, getPagination, setPagination } = usePagination(getProps);
+
       const {
-        getSortFixedColumns,
-        getColumns,
-        setColumns,
-        getColumnsRef,
-        getCacheColumns,
-      } = useColumns(getProps, getPaginationInfo);
+        getRowSelection,
+        getRowSelectionRef,
+        getSelectRows,
+        clearSelectedRowKeys,
+        getSelectRowKeys,
+        deleteSelectRowByKey,
+        setSelectedRowKeys,
+      } = useRowSelection(getProps, emit);
 
       const {
+        handleTableChange,
         getDataSourceRef,
         getDataSource,
         setTableData,
@@ -112,6 +123,7 @@
         getRowKey,
         reload,
         getAutoCreateKey,
+        updateTableData,
       } = useDataSource(
         getProps,
         {
@@ -119,19 +131,15 @@
           setLoading,
           setPagination,
           getFieldsValue: formActions.getFieldsValue,
+          clearSelectedRowKeys,
         },
         emit
       );
 
-      const {
-        getRowSelection,
-        getRowSelectionRef,
-        getSelectRows,
-        clearSelectedRowKeys,
-        getSelectRowKeys,
-        deleteSelectRowByKey,
-        setSelectedRowKeys,
-      } = useRowSelection(getProps, emit);
+      const { getViewColumns, getColumns, setColumns, getColumnsRef, getCacheColumns } = useColumns(
+        getProps,
+        getPaginationInfo
+      );
 
       const { getScrollRef, redoHeight } = useTableScroll(
         getProps,
@@ -178,7 +186,7 @@
           tableLayout: 'fixed',
           rowSelection: unref(getRowSelectionRef),
           rowKey: unref(getRowKey),
-          columns: unref(getSortFixedColumns),
+          columns: unref(getViewColumns),
           pagination: unref(getPaginationInfo),
           dataSource: unref(getDataSourceRef),
           footer: unref(getFooterProps),
@@ -197,26 +205,6 @@
         return !!unref(getDataSourceRef).length;
       });
 
-      function handleTableChange(
-        pagination: PaginationProps,
-        // @ts-ignore
-        filters: Partial<Recordable<string[]>>,
-        sorter: SorterResult
-      ) {
-        const { clearSelectOnPageChange, sortFn } = unref(getProps);
-        if (clearSelectOnPageChange) {
-          clearSelectedRowKeys();
-        }
-        setPagination(pagination);
-
-        if (sorter && isFunction(sortFn)) {
-          const sortInfo = sortFn(sorter);
-          fetch({ sortInfo });
-          return;
-        }
-        fetch();
-      }
-
       function setProps(props: Partial<BasicTableProps>) {
         innerPropsRef.value = { ...unref(innerPropsRef), ...props };
       }
@@ -239,6 +227,8 @@
         getPaginationRef: getPagination,
         getColumns,
         getCacheColumns,
+        emit,
+        updateTableData,
         getSize: () => {
           return unref(getBindValues).size as SizeType;
         },
@@ -265,6 +255,7 @@
         replaceFormSlotKey,
         getFormSlotKeys,
         prefixCls,
+        columns: getViewColumns,
       };
     },
   });

+ 4 - 4
src/components/Table/src/componentMap.ts

@@ -1,19 +1,19 @@
-import { Component } from 'vue';
+import type { Component } from 'vue';
 
 import { Input, Select, Checkbox, InputNumber, Switch } from 'ant-design-vue';
 
-import { ComponentType } from './types/componentType';
+import type { ComponentType } from './types/componentType';
+import { ApiSelect } from '/@/components/Form';
 
 const componentMap = new Map<ComponentType, Component>();
 
 componentMap.set('Input', Input);
-componentMap.set('InputPassword', Input.Password);
 componentMap.set('InputNumber', InputNumber);
 
 componentMap.set('Select', Select);
+componentMap.set('ApiSelect', ApiSelect);
 componentMap.set('Switch', Switch);
 componentMap.set('Checkbox', Checkbox);
-componentMap.set('CheckboxGroup', Checkbox.Group);
 
 export function add(compName: ComponentType, component: Component) {
   componentMap.set(compName, component);

+ 2 - 1
src/components/Table/src/components/EditTableHeaderIcon.vue

@@ -1,7 +1,8 @@
 <template>
   <span>
+    <slot />
     {{ title }}
-    <FormOutlined class="ml-2" />
+    <FormOutlined />
   </span>
 </template>
 <script lang="ts">

+ 55 - 0
src/components/Table/src/components/HeaderCell.vue

@@ -0,0 +1,55 @@
+<template>
+  <EditTableHeaderCell v-if="getIsEdit">
+    {{ getTitle }}
+  </EditTableHeaderCell>
+  <span v-else>{{ getTitle }}</span>
+  <BasicHelp v-if="getHelpMessage" :text="getHelpMessage" :class="`${prefixCls}__help`" />
+</template>
+<script lang="ts">
+  import type { PropType } from 'vue';
+  import type { BasicColumn } from '../types/table';
+
+  import { defineComponent, computed } from 'vue';
+
+  import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
+  import { useDesign } from '/@/hooks/web/useDesign';
+  export default defineComponent({
+    name: 'TableHeaderCell',
+    components: {
+      EditTableHeaderCell: createAsyncComponent(() => import('./EditTableHeaderIcon.vue')),
+      BasicHelp: createAsyncComponent(() => import('/@/components/Basic/src/BasicHelp.vue')),
+    },
+    props: {
+      column: {
+        type: Object as PropType<BasicColumn>,
+        default: {},
+      },
+    },
+    setup(props) {
+      const { prefixCls } = useDesign('basic-table-header-cell');
+      const getIsEdit = computed(() => {
+        return !!props.column?.edit;
+      });
+
+      const getTitle = computed(() => {
+        return props.column?.customTitle;
+      });
+
+      const getHelpMessage = computed(() => {
+        return props.column?.helpMessage;
+      });
+
+      return { prefixCls, getIsEdit, getTitle, getHelpMessage };
+    },
+  });
+</script>
+<style lang="less">
+  @prefix-cls: ~'@{namespace}-basic-table-header-cell';
+
+  .@{prefix-cls} {
+    &__help {
+      margin-left: 8px;
+      color: rgba(0, 0, 0, 0.65) !important;
+    }
+  }
+</style>

+ 3 - 4
src/components/Table/src/components/TableAction.vue

@@ -1,14 +1,13 @@
 <template>
   <div :class="[prefixCls, getAlign]">
-    <template v-for="(action, index) in getActions" :key="`${index}`">
+    <template v-for="(action, index) in getActions" :key="`${index}-${action.label}`">
       <PopConfirmButton v-bind="action">
         <Icon :icon="action.icon" class="mr-1" v-if="action.icon" />
         {{ action.label }}
       </PopConfirmButton>
       <Divider type="vertical" v-if="divider && index < getActions.length" />
     </template>
-
-    <Dropdown :trigger="['hover']" :dropMenuList="getDropList">
+    <Dropdown :trigger="['hover']" :dropMenuList="getDropList" v-if="dropDownActions">
       <slot name="more" />
       <a-button type="link" size="small" v-if="!$slots.more">
         <MoreOutlined class="icon-more" />
@@ -61,7 +60,7 @@
       });
 
       const getDropList = computed(() => {
-        return props.dropDownActions.map((action, index) => {
+        return (props.dropDownActions || []).map((action, index) => {
           const { label } = action;
           return {
             ...action,

+ 33 - 0
src/components/Table/src/components/editable/CellComponent.ts

@@ -0,0 +1,33 @@
+import type { FunctionalComponent, defineComponent } from 'vue';
+import type { ComponentType } from '../../types/componentType';
+import { componentMap } from '/@/components/Table/src/componentMap';
+
+import { Popover } from 'ant-design-vue';
+import { h } from 'vue';
+
+export interface ComponentProps {
+  component: ComponentType;
+  rule: boolean;
+  popoverVisible: boolean;
+  ruleMessage: string;
+}
+
+export const CellComponent: FunctionalComponent = (
+  { component = 'Input', rule = true, ruleMessage, popoverVisible }: ComponentProps,
+  { attrs }
+) => {
+  const Comp = componentMap.get(component) as typeof defineComponent;
+
+  const DefaultComp = h(Comp, attrs);
+  if (!rule) {
+    return DefaultComp;
+  }
+  return h(
+    Popover,
+    { overlayClassName: 'edit-cell-rule-popover', visible: !!popoverVisible },
+    {
+      default: () => DefaultComp,
+      content: () => ruleMessage,
+    }
+  );
+};

+ 359 - 0
src/components/Table/src/components/editable/EditableCell.vue

@@ -0,0 +1,359 @@
+<template>
+  <div :class="prefixCls">
+    <div v-show="!isEdit" :class="`${prefixCls}__normal`" @click="handleEdit">
+      {{ value || '&nbsp;' }}
+      <FormOutlined :class="`${prefixCls}__normal-icon`" v-if="!column.editRow" />
+    </div>
+
+    <div v-if="isEdit" :class="`${prefixCls}__wrapper`" v-click-outside="onClickOutside">
+      <CellComponent
+        v-bind="getComponentProps"
+        :component="getComponent"
+        :style="getWrapperStyle"
+        :popoverVisible="getRuleVisible"
+        :rule="getRule"
+        :ruleMessage="ruleMessage"
+        size="small"
+        ref="elRef"
+        @change="handleChange"
+        @options-change="handleOptionsChange"
+        @pressEnter="handleSubmit"
+      >
+      </CellComponent>
+      <div :class="`${prefixCls}__action`" v-if="!getRowEditable">
+        <CheckOutlined :class="[`${prefixCls}__icon`, 'mx-2']" @click="handleSubmit" />
+        <CloseOutlined :class="`${prefixCls}__icon `" @click="handleCancel" />
+      </div>
+    </div>
+  </div>
+</template>
+<script lang="ts">
+  import type { CSSProperties, PropType } from 'vue';
+  import type { BasicColumn } from '../../types/table';
+
+  import { defineComponent, ref, unref, nextTick, computed, watchEffect, toRaw } from 'vue';
+  import { FormOutlined, CloseOutlined, CheckOutlined } from '@ant-design/icons-vue';
+
+  import { useDesign } from '/@/hooks/web/useDesign';
+  import { isString, isBoolean, isFunction, isNumber, isArray } from '/@/utils/is';
+  import clickOutside from '/@/directives/clickOutside';
+
+  import { CellComponent } from './CellComponent';
+  import { useTableContext } from '../../hooks/useTableContext';
+  import { propTypes } from '/@/utils/propTypes';
+  import { createPlaceholderMessage } from './helper';
+
+  import type { EditRecordRow } from './index';
+
+  export default defineComponent({
+    name: 'EditableCell',
+    components: { FormOutlined, CloseOutlined, CheckOutlined, CellComponent },
+    props: {
+      value: {
+        type: [String, Number, Boolean, Object] as PropType<string | number | boolean | Recordable>,
+        default: '',
+      },
+      record: {
+        type: Object as PropType<EditRecordRow>,
+      },
+      column: {
+        type: Object as PropType<BasicColumn>,
+        default: {},
+      },
+      index: propTypes.number,
+    },
+    directives: {
+      clickOutside,
+    },
+
+    setup(props) {
+      const table = useTableContext();
+      const isEdit = ref(false);
+      const elRef = ref<any>(null);
+      const ruleVisible = ref(false);
+      const ruleMessage = ref('');
+      const optionsRef = ref<LabelValueOptions>([]);
+      const currentValueRef = ref<any>(props.value);
+      const defaultValueRef = ref<any>(props.value);
+
+      const { prefixCls } = useDesign('editable-cell');
+
+      const getComponent = computed(() => props.column?.editComponent || 'Input');
+      const getRule = computed(() => props.column?.editRule);
+
+      const getRuleVisible = computed(() => {
+        return unref(ruleMessage) && unref(ruleVisible);
+      });
+
+      const getIsCheckComp = computed(() => {
+        const component = unref(getComponent);
+        return ['Checkbox', 'Switch'].includes(component);
+      });
+
+      const getComponentProps = computed(() => {
+        const compProps = props.column?.editComponentProps ?? {};
+        const component = unref(getComponent);
+        const apiSelectProps: Recordable = {};
+        if (component === 'ApiSelect') {
+          apiSelectProps.cache = true;
+        }
+
+        const isCheckValue = unref(getIsCheckComp);
+
+        const valueField = isCheckValue ? 'checked' : 'value';
+        const val = unref(currentValueRef);
+
+        const value = isCheckValue ? (isNumber(val) && isBoolean(val) ? val : !!val) : val;
+
+        return {
+          placeholder: createPlaceholderMessage(unref(getComponent)),
+          ...apiSelectProps,
+          ...compProps,
+          [valueField]: value,
+        };
+      });
+
+      const getValues = computed(() => {
+        const { editComponentProps, editValueMap } = props.column;
+
+        const value = unref(currentValueRef);
+
+        if (editValueMap && isFunction(editValueMap)) {
+          return editValueMap(value);
+        }
+
+        const component = unref(getComponent);
+        if (!component.includes('Select')) {
+          return value;
+        }
+        const options: LabelValueOptions = editComponentProps?.options ?? (unref(optionsRef) || []);
+        const option = options.find((item) => `${item.value}` === `${value}`);
+        return option?.label;
+      });
+
+      const getWrapperStyle = computed(
+        (): CSSProperties => {
+          if (unref(getIsCheckComp) || unref(getRowEditable)) {
+            return {};
+          }
+          return {
+            width: 'calc(100% - 48px)',
+          };
+        }
+      );
+
+      const getRowEditable = computed(() => {
+        const { editable } = props.record || {};
+        return !!editable;
+      });
+
+      watchEffect(() => {
+        defaultValueRef.value = props.value;
+      });
+
+      watchEffect(() => {
+        const { editable } = props.column;
+        if (isBoolean(editable) || isBoolean(unref(getRowEditable))) {
+          isEdit.value = !!editable || unref(getRowEditable);
+        }
+      });
+
+      function handleEdit() {
+        if (unref(getRowEditable) || unref(props.column?.editRow)) return;
+        ruleMessage.value = '';
+        isEdit.value = true;
+        nextTick(() => {
+          const el = unref(elRef);
+          el?.focus?.();
+        });
+      }
+
+      async function handleChange(e: any) {
+        const component = unref(getComponent);
+        if (e?.target && Reflect.has(e.target, 'value')) {
+          currentValueRef.value = (e as ChangeEvent).target.value;
+        }
+        if (component === 'Checkbox') {
+          currentValueRef.value = (e as ChangeEvent).target.checked;
+        } else if (isString(e) || isBoolean(e) || isNumber(e)) {
+          currentValueRef.value = e;
+        }
+        handleSubmiRule();
+      }
+
+      async function handleSubmiRule() {
+        const { column, record } = props;
+        const { editRule } = column;
+        const currentValue = unref(currentValueRef);
+
+        if (editRule) {
+          if (isBoolean(editRule) && !currentValue && !isNumber(currentValue)) {
+            ruleVisible.value = true;
+            const component = unref(getComponent);
+            const message = createPlaceholderMessage(component);
+            ruleMessage.value = message;
+            return false;
+          }
+          if (isFunction(editRule)) {
+            const res = await editRule(currentValue, record as Recordable);
+            if (!!res) {
+              ruleMessage.value = res;
+              ruleVisible.value = true;
+              return false;
+            } else {
+              ruleMessage.value = '';
+              return true;
+            }
+          }
+        }
+        ruleMessage.value = '';
+        return true;
+      }
+
+      async function handleSubmit() {
+        const isPass = await handleSubmiRule();
+        if (!isPass) return false;
+        const { column, index } = props;
+        const { key, dataIndex } = column;
+        // const value = unref(currentValueRef);
+        if (!key || !dataIndex) return;
+        const dataKey = (dataIndex || key) as string;
+
+        const record = await table.updateTableData(index, dataKey, unref(getValues));
+        table.emit?.('edit-end', { record, index, key, value: unref(currentValueRef) });
+        isEdit.value = false;
+      }
+
+      function handleCancel() {
+        isEdit.value = false;
+        currentValueRef.value = defaultValueRef.value;
+        table.emit?.('edit-cancel', unref(currentValueRef));
+      }
+
+      function onClickOutside() {
+        if (props.column?.editable || unref(getRowEditable)) {
+          return;
+        }
+        const component = unref(getComponent);
+
+        if (component.includes('Input')) {
+          handleCancel();
+        }
+      }
+
+      // only ApiSelect
+      function handleOptionsChange(options: LabelValueOptions) {
+        optionsRef.value = options;
+      }
+
+      function initCbs(cbs: 'submitCbs' | 'validCbs' | 'cancelCbs', handle: Fn) {
+        if (props.record) {
+          /* eslint-disable  */
+          isArray(props.record[cbs])
+            ? props.record[cbs].push(handle)
+            : (props.record[cbs] = [handle]);
+        }
+      }
+
+      if (props.record) {
+        initCbs('submitCbs', handleSubmit);
+        initCbs('validCbs', handleSubmiRule);
+        initCbs('cancelCbs', handleCancel);
+
+        /* eslint-disable  */
+        props.record.onCancelEdit = () => {
+          isArray(props.record?.cancelCbs) && props.record?.cancelCbs.forEach((fn) => fn());
+        };
+        /* eslint-disable */
+        props.record.onSubmitEdit = async () => {
+          if (isArray(props.record?.submitCbs)) {
+            const validFns = props.record?.validCbs || [];
+
+            const res = await Promise.all(validFns.map((fn) => fn()));
+            const pass = res.every((item) => !!item);
+
+            if (!pass) return;
+            const submitFns = props.record?.submitCbs || [];
+            submitFns.forEach((fn) => fn());
+            return true;
+          }
+          // isArray(props.record?.submitCbs) && props.record?.submitCbs.forEach((fn) => fn());
+        };
+      }
+
+      return {
+        isEdit,
+        prefixCls,
+        handleEdit,
+        currentValueRef,
+        handleSubmit,
+        handleChange,
+        handleCancel,
+        elRef,
+        getComponent,
+        getRule,
+        onClickOutside,
+        ruleMessage,
+        getRuleVisible,
+        getComponentProps,
+        handleOptionsChange,
+        getWrapperStyle,
+        getRowEditable,
+      };
+    },
+  });
+</script>
+<style lang="less">
+  @prefix-cls: ~'@{namespace}-editable-cell';
+
+  .edit-cell-rule-popover {
+    // .ant-popover-arrow {
+    //   // border-color: transparent @error-color @error-color transparent !important;
+    // }
+
+    .ant-popover-inner-content {
+      padding: 4px 8px;
+      color: @error-color;
+      // border: 1px solid @error-color;
+      border-radius: 2px;
+    }
+  }
+  .@{prefix-cls} {
+    position: relative;
+
+    &__wrapper {
+      display: flex;
+      align-items: center;
+      justify-content: center;
+    }
+
+    &__icon {
+      &:hover {
+        transform: scale(1.2);
+
+        svg {
+          color: @primary-color;
+        }
+      }
+    }
+
+    &__normal {
+      padding-right: 48px;
+
+      &-icon {
+        position: absolute;
+        top: 4px;
+        right: 0;
+        display: none;
+        width: 20px;
+        cursor: pointer;
+      }
+    }
+
+    &:hover {
+      .@{prefix-cls}__normal-icon {
+        display: inline-block;
+      }
+    }
+  }
+</style>

+ 26 - 0
src/components/Table/src/components/editable/helper.ts

@@ -0,0 +1,26 @@
+import { ComponentType } from '../../types/componentType';
+import { useI18n } from '/@/hooks/web/useI18n';
+
+const { t } = useI18n();
+
+/**
+ * @description: 生成placeholder
+ */
+export function createPlaceholderMessage(component: ComponentType) {
+  if (component.includes('Input')) {
+    return t('component.form.input');
+  }
+  if (component.includes('Picker')) {
+    return t('component.form.choose');
+  }
+
+  if (
+    component.includes('Select') ||
+    component.includes('Checkbox') ||
+    component.includes('Radio') ||
+    component.includes('Switch')
+  ) {
+    return t('component.form.choose');
+  }
+  return '';
+}

+ 52 - 0
src/components/Table/src/components/editable/index.ts

@@ -0,0 +1,52 @@
+import type { BasicColumn } from '/@/components/Table/src/types/table';
+
+import { h } from 'vue';
+
+import EditableCell from './EditableCell.vue';
+
+interface Params {
+  text: string;
+  record: Recordable;
+  index: number;
+}
+
+export function renderEditCell(column: BasicColumn) {
+  return ({ text: value, record, index }: Params) => {
+    record.onEdit = async (edit: boolean, submit = false) => {
+      if (!submit) {
+        record.editable = edit;
+      }
+
+      if (!edit && submit) {
+        const res = await record.onSubmitEdit?.();
+        if (res) {
+          record.editable = false;
+          return true;
+        }
+        return false;
+      }
+      // cancel
+      if (!edit && !submit) {
+        record.onCancelEdit?.();
+      }
+      return true;
+    };
+
+    return h(EditableCell, {
+      value,
+      record,
+      column,
+      index,
+    });
+  };
+}
+
+export type EditRecordRow<T = Hash<any>> = {
+  onEdit: (editable: boolean, submit?: boolean) => Promise<boolean>;
+  editable: boolean;
+  onCancel: Fn;
+  onSubmit: Fn;
+  submitCbs: Fn[];
+  cancelCbs: Fn[];
+  validCbs: Fn[];
+} & T;

+ 0 - 241
src/components/Table/src/components/renderEditable.tsx

@@ -1,241 +0,0 @@
-import '../style/editable-cell.less';
-
-import { defineComponent, PropType, ref, unref, nextTick, watchEffect } from 'vue';
-import { ClickOutSide } from '/@/components/ClickOutSide';
-
-import { RenderEditableCellParams } from '../types/table';
-import { ComponentType } from '../types/componentType';
-
-import { componentMap } from '../componentMap';
-import { isString, isBoolean, isArray } from '/@/utils/is';
-import { FormOutlined, CloseOutlined, CheckOutlined } from '@ant-design/icons-vue';
-
-const prefixCls = 'editable-cell';
-const EditableCell = defineComponent({
-  name: 'EditableCell',
-  props: {
-    value: {
-      type: String as PropType<string>,
-      default: '',
-    },
-    componentProps: {
-      type: Object as PropType<any>,
-      default: null,
-    },
-
-    dataKey: {
-      type: String as PropType<string>,
-      default: '',
-    },
-
-    dataIndex: {
-      type: String as PropType<string>,
-      default: '',
-    },
-
-    component: {
-      type: String as PropType<ComponentType>,
-      default: 'Input',
-    },
-    editable: {
-      type: Boolean as PropType<boolean>,
-      default: false,
-    },
-    editRow: {
-      type: Boolean as PropType<boolean>,
-      default: false,
-    },
-    record: {
-      type: Object as PropType<EditRecordRow>,
-    },
-    placeholder: {
-      type: String as PropType<string>,
-      default: '',
-    },
-  },
-  emits: ['submit', 'cancel'],
-  setup(props, { attrs, emit }) {
-    const elRef = ref<any>(null);
-
-    const isEditRef = ref(false);
-    const currentValueRef = ref<string | boolean>(props.value);
-    const defaultValueRef = ref<string | boolean>(props.value);
-
-    watchEffect(() => {
-      defaultValueRef.value = props.value;
-      if (isBoolean(props.editable)) {
-        isEditRef.value = props.editable;
-      }
-    });
-
-    function handleChange(e: any) {
-      if (e && e.target && Reflect.has(e.target, 'value')) {
-        currentValueRef.value = (e as ChangeEvent).target.value;
-      }
-      if (isString(e) || isBoolean(e)) {
-        currentValueRef.value = e;
-      }
-    }
-
-    function handleEdit() {
-      isEditRef.value = true;
-      nextTick(() => {
-        const el = unref(elRef);
-        el && el.focus();
-      });
-    }
-
-    function handleCancel() {
-      isEditRef.value = false;
-      currentValueRef.value = defaultValueRef.value;
-      emit('cancel');
-    }
-
-    if (props.record) {
-      /* eslint-disable  */
-      isArray(props.record.submitCbs)
-        ? props.record.submitCbs.push(handleSubmit)
-        : (props.record.submitCbs = [handleSubmit]);
-      /* eslint-disable  */
-      isArray(props.record.cancelCbs)
-        ? props.record.cancelCbs.push(handleCancel)
-        : (props.record.cancelCbs = [handleCancel]);
-
-      /* eslint-disable  */
-      props.record.onCancel = () => {
-        isArray(props.record?.cancelCbs) && props.record?.cancelCbs.forEach((fn) => fn());
-      };
-      /* eslint-disable */
-      props.record.onSubmit = () => {
-        isArray(props.record?.submitCbs) && props.record?.submitCbs.forEach((fn) => fn());
-      };
-    }
-
-    function handleSubmit() {
-      const { dataKey, dataIndex } = props;
-      if (!dataKey || !dataIndex) return;
-
-      if (props.record) {
-        /* eslint-disable */
-        props.record[dataIndex] = unref(currentValueRef) as string;
-      }
-      isEditRef.value = false;
-    }
-
-    function onClickOutside() {
-      if (props.editRow) return;
-      const { component } = props;
-
-      if (component && component.includes('Input')) {
-        handleCancel();
-      }
-    }
-
-    function renderValue() {
-      const { value } = props;
-      if (props.editRow) {
-        return !unref(isEditRef) ? value : null;
-      }
-      return (
-        !unref(isEditRef) && (
-          <div class={`${prefixCls}__normal`} onClick={handleEdit}>
-            {value}
-            <FormOutlined class={`${prefixCls}__normal-icon`} />
-          </div>
-        )
-      );
-    }
-    return () => {
-      const { component, componentProps = {} } = props;
-
-      const Comp = componentMap.get(component!) as any;
-      return (
-        <div class={prefixCls}>
-          {unref(isEditRef) && (
-            <ClickOutSide onClickOutside={onClickOutside}>
-              {() => (
-                <div class={`${prefixCls}__wrapper`}>
-                  <Comp
-                    placeholder={props.placeholder}
-                    {...{
-                      ...attrs,
-                      ...componentProps,
-                    }}
-                    style={{ width: 'calc(100% - 48px)' }}
-                    ref={elRef}
-                    value={unref(currentValueRef)}
-                    size="small"
-                    onChange={handleChange}
-                    onPressEnter={handleSubmit}
-                  />
-                  {!props.editRow && (
-                    <div class={`${prefixCls}__action`}>
-                      <CheckOutlined
-                        class={[`${prefixCls}__icon`, 'mx-2']}
-                        onClick={handleSubmit}
-                      />
-                      <CloseOutlined class={[`${prefixCls}__icon `]} onClick={handleCancel} />
-                    </div>
-                  )}
-                </div>
-              )}
-            </ClickOutSide>
-          )}
-          {renderValue()}
-        </div>
-      );
-    };
-  },
-});
-
-export function renderEditableCell({
-  dataIndex,
-  component,
-  componentProps = {},
-  placeholder,
-}: RenderEditableCellParams) {
-  return ({ text, record }: { text: string; record: EditRecordRow }) => {
-    return (
-      <EditableCell
-        {...componentProps}
-        placeholder={placeholder}
-        value={text}
-        record={record}
-        dataKey={record.key}
-        dataIndex={dataIndex}
-        component={component}
-      />
-    );
-  };
-}
-
-export function renderEditableRow({
-  dataIndex,
-  component,
-  componentProps = {},
-  placeholder,
-}: RenderEditableCellParams) {
-  return ({ text, record }: { text: string; record: EditRecordRow }) => {
-    return (
-      <EditableCell
-        {...componentProps}
-        value={text}
-        placeholder={placeholder}
-        editRow={true}
-        editable={record.editable}
-        dataKey={record.key}
-        record={record}
-        dataIndex={dataIndex}
-        component={component}
-      />
-    );
-  };
-}
-
-export type EditRecordRow<T = Hash<any>> = {
-  editable: boolean;
-  onCancel: Fn;
-  onSubmit: Fn;
-  submitCbs: Fn[];
-  cancelCbs: Fn[];
-} & T;

+ 0 - 14
src/components/Table/src/components/renderExpandIcon.tsx

@@ -1,14 +0,0 @@
-import { BasicArrow } from '/@/components/Basic';
-
-export default () => {
-  return (props: Recordable) => {
-    return (
-      <BasicArrow
-        onClick={(e: Event) => {
-          props.onExpand(props.record, e);
-        }}
-        expand={props.expanded}
-      />
-    );
-  };
-};

+ 1 - 1
src/components/Table/src/components/settings/ColumnSetting.vue

@@ -184,7 +184,7 @@
         const ret: Options[] = [];
         table.getColumns({ ignoreIndex: true, ignoreAction: true }).forEach((item) => {
           ret.push({
-            label: item.title as string,
+            label: (item.title as string) || (item.customTitle as string),
             value: (item.dataIndex || item.title) as string,
             ...item,
           });

+ 4 - 0
src/components/Table/src/const.ts

@@ -32,6 +32,10 @@ export function DEFAULT_SORT_FN(sortInfo: SorterResult) {
   };
 }
 
+export function DEFAULT_FILTER_FN(data: Partial<Recordable<string[]>>) {
+  return data;
+}
+
 //  表格单元格默认布局
 export const DEFAULT_ALIGN = 'center';
 

+ 65 - 8
src/components/Table/src/hooks/useColumns.ts

@@ -1,10 +1,13 @@
-import { BasicColumn, BasicTableProps, GetColumnsParams } from '../types/table';
-import { PaginationProps } from '../types/pagination';
+import type { BasicColumn, BasicTableProps, CellFormat, GetColumnsParams } from '../types/table';
+import type { PaginationProps } from '../types/pagination';
 import { unref, ComputedRef, Ref, computed, watchEffect, ref, toRaw } from 'vue';
-import { isBoolean, isArray, isString } from '/@/utils/is';
+import { isBoolean, isArray, isString, isObject } from '/@/utils/is';
 import { DEFAULT_ALIGN, PAGE_SIZE, INDEX_COLUMN_FLAG, ACTION_COLUMN_FLAG } from '../const';
 import { useI18n } from '/@/hooks/web/useI18n';
 import { isEqual, cloneDeep } from 'lodash-es';
+import { isFunction } from '/@/utils/is';
+import { formatToDate } from '/@/utils/dateUtil';
+import { renderEditCell } from '../components/editable';
 
 const { t } = useI18n();
 
@@ -127,8 +130,30 @@ export function useColumns(
     return columns;
   });
 
-  const getSortFixedColumns = computed(() => {
-    return useFixedColumn(unref(getColumnsRef));
+  const getViewColumns = computed(() => {
+    const viewColumns = sortFixedColumn(unref(getColumnsRef));
+
+    viewColumns.forEach((column) => {
+      const { slots, dataIndex, customRender, format, edit, editRow, flag } = column;
+
+      if (!slots || !slots?.title) {
+        column.slots = { title: `header-${dataIndex}`, ...(slots || {}) };
+        column.customTitle = column.title;
+        Reflect.deleteProperty(column, 'title');
+      }
+      const isDefaultAction = [INDEX_COLUMN_FLAG, ACTION_COLUMN_FLAG].includes(flag!);
+      if (!customRender && format && !edit && !isDefaultAction) {
+        column.customRender = ({ text, record, index }) => {
+          return formatCell(text, format, record, index);
+        };
+      }
+
+      // edit table
+      if ((edit || editRow) && !isDefaultAction) {
+        column.customRender = renderEditCell(column);
+      }
+    });
+    return viewColumns;
   });
 
   watchEffect(() => {
@@ -191,7 +216,7 @@ export function useColumns(
     }
 
     if (sort) {
-      columns = useFixedColumn(columns);
+      columns = sortFixedColumn(columns);
     }
 
     return columns;
@@ -200,10 +225,10 @@ export function useColumns(
     return cacheColumns;
   }
 
-  return { getColumnsRef, getCacheColumns, getColumns, setColumns, getSortFixedColumns };
+  return { getColumnsRef, getCacheColumns, getColumns, setColumns, getViewColumns };
 }
 
-export function useFixedColumn(columns: BasicColumn[]) {
+function sortFixedColumn(columns: BasicColumn[]) {
   const fixedLeftColumns: BasicColumn[] = [];
   const fixedRightColumns: BasicColumn[] = [];
   const defColumns: BasicColumn[] = [];
@@ -224,3 +249,35 @@ export function useFixedColumn(columns: BasicColumn[]) {
 
   return resultColumns;
 }
+
+// format cell
+export function formatCell(text: string, format: CellFormat, record: Recordable, index: number) {
+  if (!format) {
+    return text;
+  }
+
+  // custom function
+  if (isFunction(format)) {
+    return format(text, record, index);
+  }
+
+  try {
+    // date type
+    const DATE_FORMAT_PREFIX = 'date|';
+    if (isString(format) && format.startsWith(DATE_FORMAT_PREFIX)) {
+      const dateFormat = format.replace(DATE_FORMAT_PREFIX, '');
+
+      if (!dateFormat) {
+        return text;
+      }
+      return formatToDate(text, dateFormat);
+    }
+
+    // enum
+    if (isObject(format) && Reflect.has(format, 'size')) {
+      return format.get(text);
+    }
+  } catch (error) {
+    return text;
+  }
+}

+ 59 - 3
src/components/Table/src/hooks/useDataSource.ts

@@ -1,7 +1,7 @@
-import type { BasicTableProps, FetchParams } from '../types/table';
+import type { BasicTableProps, FetchParams, SorterResult } from '../types/table';
 import type { PaginationProps } from '../types/pagination';
 
-import { ref, unref, ComputedRef, computed, onMounted, watchEffect } from 'vue';
+import { ref, unref, ComputedRef, computed, onMounted, watchEffect, reactive } from 'vue';
 
 import { useTimeoutFn } from '/@/hooks/core/useTimeout';
 
@@ -16,12 +16,28 @@ interface ActionType {
   setPagination: (info: Partial<PaginationProps>) => void;
   setLoading: (loading: boolean) => void;
   getFieldsValue: () => Recordable;
+  clearSelectedRowKeys: () => void;
+}
+
+interface SearchState {
+  sortInfo: Recordable;
+  filterInfo: Record<string, string[]>;
 }
 export function useDataSource(
   propsRef: ComputedRef<BasicTableProps>,
-  { getPaginationInfo, setPagination, setLoading, getFieldsValue }: ActionType,
+  {
+    getPaginationInfo,
+    setPagination,
+    setLoading,
+    getFieldsValue,
+    clearSelectedRowKeys,
+  }: ActionType,
   emit: EmitType
 ) {
+  const searchState = reactive<SearchState>({
+    sortInfo: {},
+    filterInfo: {},
+  });
   const dataSourceRef = ref<Recordable[]>([]);
 
   watchEffect(() => {
@@ -29,6 +45,32 @@ export function useDataSource(
     !api && dataSource && (dataSourceRef.value = dataSource);
   });
 
+  function handleTableChange(
+    pagination: PaginationProps,
+    filters: Partial<Recordable<string[]>>,
+    sorter: SorterResult
+  ) {
+    const { clearSelectOnPageChange, sortFn, filterFn } = unref(propsRef);
+    if (clearSelectOnPageChange) {
+      clearSelectedRowKeys();
+    }
+    setPagination(pagination);
+
+    const params: Recordable = {};
+    if (sorter && isFunction(sortFn)) {
+      const sortInfo = sortFn(sorter);
+      searchState.sortInfo = sortInfo;
+      params.sortInfo = sortInfo;
+    }
+
+    if (filters && isFunction(filterFn)) {
+      const filterInfo = filterFn(filters);
+      searchState.filterInfo = filterInfo;
+      params.filterInfo = filterInfo;
+    }
+    fetch(params);
+  }
+
   function setTableKey(items: any[]) {
     if (!items || !Array.isArray(items)) return;
     items.forEach((item) => {
@@ -75,6 +117,14 @@ export function useDataSource(
     return unref(dataSourceRef);
   });
 
+  async function updateTableData(index: number, key: string, value: any) {
+    const record = dataSourceRef.value[index];
+    if (record) {
+      dataSourceRef.value[index][key] = value;
+    }
+    return dataSourceRef.value[index];
+  }
+
   async function fetch(opt?: FetchParams) {
     const { api, searchInfo, fetchSetting, beforeFetch, afterFetch, useSearchForm } = unref(
       propsRef
@@ -94,6 +144,8 @@ export function useDataSource(
         pageParams[sizeField] = pageSize;
       }
 
+      const { sortInfo = {}, filterInfo } = searchState;
+
       let params: Recordable = {
         ...pageParams,
         ...(useSearchForm ? getFieldsValue() : {}),
@@ -101,6 +153,8 @@ export function useDataSource(
         ...(opt ? opt.searchInfo : {}),
         ...(opt ? opt.sortInfo : {}),
         ...(opt ? opt.filterInfo : {}),
+        ...sortInfo,
+        ...filterInfo,
       };
       if (beforeFetch && isFunction(beforeFetch)) {
         params = beforeFetch(params) || params;
@@ -175,5 +229,7 @@ export function useDataSource(
     getAutoCreateKey,
     fetch,
     reload,
+    updateTableData,
+    handleTableChange,
   };
 }

+ 38 - 10
src/components/Table/src/hooks/useTable.ts

@@ -1,19 +1,27 @@
 import type { BasicTableProps, TableActionType, FetchParams, BasicColumn } from '../types/table';
 import type { PaginationProps } from '../types/pagination';
+import type { DynamicProps } from '/@/types/utils';
+import { getDynamicProps } from '/@/utils';
 
 import { ref, onUnmounted, unref } from 'vue';
 import { isProdMode } from '/@/utils/env';
 import { isInSetup } from '/@/utils/helper/vueHelper';
+import { error } from '/@/utils/log';
+import { watchEffect } from 'vue';
+import type { FormActionType } from '/@/components/Form';
+
+type Props = Partial<DynamicProps<BasicTableProps>>;
 
 export function useTable(
-  tableProps?: Partial<BasicTableProps>
-): [(instance: TableActionType) => void, TableActionType] {
+  tableProps?: Props
+): [(instance: TableActionType, formInstance: FormActionType) => void, TableActionType] {
   isInSetup();
 
   const tableRef = ref<Nullable<TableActionType>>(null);
   const loadedRef = ref<Nullable<boolean>>(false);
+  const formRef = ref<Nullable<FormActionType>>(null);
 
-  function register(instance: TableActionType) {
+  function register(instance: TableActionType, formInstance: FormActionType) {
     isProdMode() &&
       onUnmounted(() => {
         tableRef.value = null;
@@ -24,20 +32,29 @@ export function useTable(
       return;
     }
     tableRef.value = instance;
-    tableProps && instance.setProps(tableProps);
+    formRef.value = formInstance;
+    // tableProps && instance.setProps(tableProps);
     loadedRef.value = true;
+
+    watchEffect(() => {
+      tableProps && instance.setProps(getDynamicProps(tableProps));
+    });
   }
 
   function getTableInstance(): TableActionType {
     const table = unref(tableRef);
     if (!table) {
-      throw new Error('table is undefined!');
+      error(
+        'The table instance has not been obtained yet, please make sure the table is presented when performing the table operation!'
+      );
     }
-    return table;
+    return table as TableActionType;
   }
 
-  const methods: TableActionType = {
-    reload: (opt?: FetchParams) => {
+  const methods: TableActionType & {
+    getForm: () => FormActionType;
+  } = {
+    reload: async (opt?: FetchParams) => {
       getTableInstance().reload(opt);
     },
     setProps: (props: Partial<BasicTableProps>) => {
@@ -54,7 +71,6 @@ export function useTable(
     },
     getColumns: ({ ignoreIndex = false }: { ignoreIndex?: boolean } = {}) => {
       const columns = getTableInstance().getColumns({ ignoreIndex }) || [];
-
       return columns;
     },
     setColumns: (columns: BasicColumn[]) => {
@@ -87,7 +103,19 @@ export function useTable(
     getSize: () => {
       return getTableInstance().getSize();
     },
-  } as TableActionType;
+    updateTableData: (index: number, key: string, value: any) => {
+      return getTableInstance().updateTableData(index, key, value);
+    },
+    getRowSelection: () => {
+      return getTableInstance().getRowSelection();
+    },
+    getCacheColumns: () => {
+      return getTableInstance().getCacheColumns();
+    },
+    getForm: () => {
+      return unref(formRef) as FormActionType;
+    },
+  };
 
   return [register, methods];
 }

+ 5 - 2
src/components/Table/src/hooks/useTableScroll.ts

@@ -121,7 +121,7 @@ export function useTableScroll(
       width += 60;
     }
 
-    // TODO props
+    // TODO propsdth ?? 0;
     const NORMAL_WIDTH = 150;
 
     const columns = unref(columnsRef);
@@ -135,7 +135,10 @@ export function useTableScroll(
     if (len !== 0) {
       width += len * NORMAL_WIDTH;
     }
-    return width;
+
+    const table = unref(tableElRef);
+    const tableWidth = table?.$el?.offsetWidth ?? 0;
+    return tableWidth > width ? tableWidth - 24 : width;
   });
 
   const getScrollRef = computed(() => {

+ 9 - 1
src/components/Table/src/props.ts

@@ -9,21 +9,29 @@ import type {
   TableRowSelection,
 } from './types/table';
 import type { FormProps } from '/@/components/Form';
-import { DEFAULT_SORT_FN, FETCH_SETTING } from './const';
+import { DEFAULT_FILTER_FN, DEFAULT_SORT_FN, FETCH_SETTING } from './const';
 import { propTypes } from '/@/utils/propTypes';
 
 // 注释看 types/table
 export const basicProps = {
   clickToRowSelect: propTypes.bool.def(true),
+
   tableSetting: {
     type: Object as PropType<TableSetting>,
   },
+
   inset: propTypes.bool,
+
   sortFn: {
     type: Function as PropType<(sortInfo: SorterResult) => any>,
     default: DEFAULT_SORT_FN,
   },
 
+  filterFn: {
+    type: Function as PropType<(data: Partial<Recordable<string[]>>) => any>,
+    default: DEFAULT_FILTER_FN,
+  },
+
   showTableSetting: propTypes.bool,
   autoCreateKey: propTypes.bool.def(true),
   striped: propTypes.bool.def(true),

+ 0 - 39
src/components/Table/src/style/editable-cell.less

@@ -1,39 +0,0 @@
-@prefix-cls: ~'editable-cell';
-
-.@{prefix-cls} {
-  position: relative;
-
-  &__wrapper {
-    display: flex;
-    align-items: center;
-  }
-
-  &__icon {
-    &:hover {
-      transform: scale(1.2);
-
-      svg {
-        color: @primary-color;
-      }
-    }
-  }
-
-  &__normal {
-    padding-right: 48px;
-
-    &-icon {
-      position: absolute;
-      top: 4px;
-      right: 0;
-      display: none;
-      width: 20px;
-      cursor: pointer;
-    }
-  }
-
-  &:hover {
-    .@{prefix-cls}__normal-icon {
-      display: inline-block;
-    }
-  }
-}

+ 10 - 6
src/components/Table/src/style/index.less

@@ -133,14 +133,18 @@
     overflow-y: scroll !important;
   }
 
-  .ant-table-fixed-right .ant-table-header {
-    border-left: 1px solid @border-color !important;
+  .ant-table-fixed-right {
+    right: -1px;
 
-    .ant-table-fixed {
-      border-bottom: none;
+    .ant-table-header {
+      border-left: 1px solid @border-color !important;
+
+      .ant-table-fixed {
+        border-bottom: none;
 
-      .ant-table-thead th {
-        background: rgb(241, 243, 244);
+        .ant-table-thead th {
+          background: rgb(241, 243, 244);
+        }
       }
     }
   }

+ 1 - 2
src/components/Table/src/types/componentType.ts

@@ -1,8 +1,7 @@
 export type ComponentType =
   | 'Input'
-  | 'InputPassword'
   | 'InputNumber'
   | 'Select'
+  | 'ApiSelect'
   | 'Checkbox'
-  | 'CheckboxGroup'
   | 'Switch';

+ 42 - 14
src/components/Table/src/types/table.ts

@@ -6,9 +6,10 @@ import type {
   TableRowSelection as ITableRowSelection,
 } from 'ant-design-vue/lib/table/interface';
 import { ComponentType } from './componentType';
+import { VueNode } from '/@/utils/propTypes';
 // import { ColumnProps } from './column';
 export declare type SortOrder = 'ascend' | 'descend';
-export interface TableCurrentDataSource<T = any> {
+export interface TableCurrentDataSource<T = Recordable> {
   currentDataSource: T[];
 }
 
@@ -53,7 +54,7 @@ export interface ColumnFilterItem {
   children?: any;
 }
 
-export interface TableCustomRecord<T = any> {
+export interface TableCustomRecord<T = Recordable> {
   record?: T;
   index?: number;
 }
@@ -65,18 +66,11 @@ export interface SorterResult {
   columnKey: string;
 }
 
-export interface RenderEditableCellParams {
-  dataIndex: string;
-  component?: ComponentType;
-  componentProps?: any;
-  placeholder?: string;
-}
-
 export interface FetchParams {
-  searchInfo?: any;
+  searchInfo?: Recordable;
   page?: number;
-  sortInfo?: any;
-  filterInfo?: any;
+  sortInfo?: Recordable;
+  filterInfo?: Recordable;
 }
 
 export interface GetColumnsParams {
@@ -89,7 +83,7 @@ export type SizeType = 'default' | 'middle' | 'small' | 'large';
 
 export interface TableActionType {
   reload: (opt?: FetchParams) => Promise<void>;
-  getSelectRows: <T = any>() => T[];
+  getSelectRows: <T = Recordable>() => T[];
   clearSelectedRowKeys: () => void;
   getSelectRowKeys: () => string[];
   deleteSelectRowByKey: (key: string) => void;
@@ -106,6 +100,8 @@ export interface TableActionType {
   getSize: () => SizeType;
   getRowSelection: () => TableRowSelection<Recordable>;
   getCacheColumns: () => BasicColumn[];
+  emit?: EmitType;
+  updateTableData: (index: number, key: string, value: any) => Recordable;
 }
 
 export interface FetchSetting {
@@ -131,6 +127,8 @@ export interface BasicTableProps<T = any> {
   clickToRowSelect?: boolean;
   // 自定义排序方法
   sortFn?: (sortInfo: SorterResult) => any;
+  // 排序方法
+  filterFn?: (data: Partial<Recordable<string[]>>) => any;
   // 取消表格的默认padding
   inset?: boolean;
   // 显示表格设置
@@ -141,7 +139,7 @@ export interface BasicTableProps<T = any> {
   // 是否自动生成key
   autoCreateKey?: boolean;
   // 计算合计行的方法
-  summaryFunc?: (...arg: any) => any[];
+  summaryFunc?: (...arg: any) => Recordable[];
   // 是否显示合计行
   showSummary?: boolean;
   // 是否可拖拽列
@@ -374,13 +372,43 @@ export interface BasicTableProps<T = any> {
   onExpandedRowsChange?: (expandedRows: string[] | number[]) => void;
 }
 
+export type CellFormat =
+  | string
+  | ((text: string, record: Recordable, index: number) => string | number)
+  | Map<string | number, any>;
+
+// @ts-ignore
 export interface BasicColumn extends ColumnProps {
   children?: BasicColumn[];
+  filters?: {
+    text: string;
+    value: string;
+    children?:
+      | unknown[]
+      | (((props: Record<string, unknown>) => unknown[]) & (() => unknown[]) & (() => unknown[]));
+  }[];
 
   //
   flag?: 'INDEX' | 'DEFAULT' | 'CHECKBOX' | 'RADIO' | 'ACTION';
+  customTitle?: VueNode;
 
   slots?: Indexable;
 
+  // Whether to hide the column by default, it can be displayed in the column configuration
   defaultHidden?: boolean;
+
+  // Help text for table column header
+  helpMessage?: string | string[];
+
+  format?: CellFormat;
+
+  // Editable
+  edit?: boolean;
+  editRow?: boolean;
+  editable?: boolean;
+  editComponent?: ComponentType;
+  editComponentProps?: Recordable;
+  editRule?: boolean | ((text: string, record: Recordable) => Promise<string>);
+  editValueMap?: (value: any) => string;
+  onEditRow?: () => void;
 }

+ 5 - 0
src/layouts/default/sider/MixSider.vue

@@ -351,6 +351,11 @@
         position: absolute;
         top: 10px;
         right: 30px;
+
+        &--dot {
+          top: 50%;
+          margin-top: -3px;
+        }
       }
 
       &__title {

+ 9 - 0
src/router/menus/modules/demo/comp.ts

@@ -52,6 +52,9 @@ const menu: MenuModule = {
       {
         path: 'table',
         name: t('routes.demo.table.table'),
+        tag: {
+          dot: true,
+        },
         children: [
           {
             path: 'basic',
@@ -108,10 +111,16 @@ const menu: MenuModule = {
           {
             path: 'editCellTable',
             name: t('routes.demo.table.editCellTable'),
+            tag: {
+              dot: true,
+            },
           },
           {
             path: 'editRowTable',
             name: t('routes.demo.table.editRowTable'),
+            tag: {
+              dot: true,
+            },
           },
         ],
       },

+ 7 - 4
src/utils/dateUtil.ts

@@ -3,12 +3,15 @@ import moment from 'moment';
 const DATE_TIME_FORMAT = 'YYYY-MM-DD HH:mm';
 const DATE_FORMAT = 'YYYY-MM-DD ';
 
-export function formatToDateTime(date: moment.MomentInput = null): string {
-  return moment(date).format(DATE_TIME_FORMAT);
+export function formatToDateTime(
+  date: moment.MomentInput = null,
+  format = DATE_TIME_FORMAT
+): string {
+  return moment(date).format(format);
 }
 
-export function formatToDate(date: moment.MomentInput = null): string {
-  return moment(date).format(DATE_FORMAT);
+export function formatToDate(date: moment.MomentInput = null, format = DATE_FORMAT): string {
+  return moment(date).format(format);
 }
 
 export const formatAgo = (str: string | number) => {

+ 100 - 26
src/views/demo/table/EditCellTable.vue

@@ -1,45 +1,108 @@
 <template>
   <div class="p-4">
-    <BasicTable @register="registerTable">
-      <template #customId>
-        <EditTableHeaderIcon title="Id" />
-      </template>
-      <template #customName>
-        <EditTableHeaderIcon title="姓名" />
-      </template>
+    <BasicTable @register="registerTable" @edit-end="handleEditEnd" @edit-cancel="handleEditCancel">
     </BasicTable>
   </div>
 </template>
 <script lang="ts">
   import { defineComponent } from 'vue';
-  import {
-    BasicTable,
-    useTable,
-    BasicColumn,
-    renderEditableCell,
-    EditTableHeaderIcon,
-  } from '/@/components/Table';
+  import { BasicTable, useTable, BasicColumn, EditTableHeaderIcon } from '/@/components/Table';
+  import { optionsListApi } from '/@/api/demo/select';
 
   import { demoListApi } from '/@/api/demo/table';
   const columns: BasicColumn[] = [
     {
-      // title: 'ID',
+      title: '输入框',
+      dataIndex: 'name',
+      edit: true,
+      editComponentProps: {
+        prefix: '$',
+      },
+      width: 200,
+    },
+    {
+      title: '默认输入状态',
+      dataIndex: 'name7',
+      edit: true,
+      editable: true,
+      width: 200,
+    },
+    {
+      title: '输入框校验',
+      dataIndex: 'name1',
+      edit: true,
+      // 默认必填校验
+      editRule: true,
+      width: 200,
+    },
+    {
+      title: '输入框函数校验',
+      dataIndex: 'name2',
+      edit: true,
+      editRule: async (text) => {
+        if (text === '2') {
+          return '不能输入该值';
+        }
+        return '';
+      },
+      width: 200,
+    },
+    {
+      title: '数字输入框',
       dataIndex: 'id',
-      slots: { title: 'customId' },
-      customRender: renderEditableCell({ dataIndex: 'id' }),
+      edit: true,
+      editRule: true,
+      editComponent: 'InputNumber',
+      width: 200,
     },
     {
-      // title: '姓名',
-      dataIndex: 'name',
-      slots: { title: 'customName' },
-      customRender: renderEditableCell({
-        dataIndex: 'name',
-      }),
+      title: '下拉框',
+      dataIndex: 'name3',
+      edit: true,
+      editComponent: 'Select',
+      editComponentProps: {
+        options: [
+          {
+            label: 'Option1',
+            value: '1',
+          },
+          {
+            label: 'Option2',
+            value: '2',
+          },
+        ],
+      },
+      width: 200,
+    },
+    {
+      title: '远程下拉',
+      dataIndex: 'name4',
+      edit: true,
+      editComponent: 'ApiSelect',
+      editComponentProps: {
+        api: optionsListApi,
+      },
+      width: 200,
     },
     {
-      title: '地址',
-      dataIndex: 'address',
-      sorter: true,
+      title: '勾选框',
+      dataIndex: 'name5',
+      edit: true,
+      editComponent: 'Checkbox',
+      editValueMap: (value) => {
+        return value ? '是' : '否';
+      },
+      width: 200,
+    },
+    {
+      title: '开关',
+      dataIndex: 'name6',
+      edit: true,
+      editComponent: 'Switch',
+      editValueMap: (value) => {
+        return value ? '开' : '关';
+      },
+      width: 200,
     },
   ];
   export default defineComponent({
@@ -50,10 +113,21 @@
         api: demoListApi,
         columns: columns,
         showIndexColumn: false,
+        bordered: true,
       });
 
+      function handleEditEnd({ record, index, key, value }: Recordable) {
+        console.log(record, index, key, value);
+      }
+
+      function handleEditCancel() {
+        console.log('cancel');
+      }
+
       return {
         registerTable,
+        handleEditEnd,
+        handleEditCancel,
       };
     },
   });

+ 96 - 15
src/views/demo/table/EditRowTable.vue

@@ -15,24 +15,105 @@
     TableAction,
     BasicColumn,
     ActionItem,
-    renderEditableRow,
     EditTableHeaderIcon,
     EditRecordRow,
   } from '/@/components/Table';
+  import { optionsListApi } from '/@/api/demo/select';
 
   import { demoListApi } from '/@/api/demo/table';
   const columns: BasicColumn[] = [
     {
-      title: 'ID',
+      title: '输入框',
+      dataIndex: 'name',
+      editRow: true,
+      editComponentProps: {
+        prefix: '$',
+      },
+      width: 200,
+    },
+    {
+      title: '默认输入状态',
+      dataIndex: 'name7',
+      editRow: true,
+      width: 200,
+    },
+    {
+      title: '输入框校验',
+      dataIndex: 'name1',
+      editRow: true,
+      // 默认必填校验
+      editRule: true,
+      width: 200,
+    },
+    {
+      title: '输入框函数校验',
+      dataIndex: 'name2',
+      editRow: true,
+      editRule: async (text) => {
+        if (text === '2') {
+          return '不能输入该值';
+        }
+        return '';
+      },
+      width: 200,
+    },
+    {
+      title: '数字输入框',
       dataIndex: 'id',
-      customRender: renderEditableRow({ dataIndex: 'id' }),
+      editRow: true,
+      editRule: true,
+      editComponent: 'InputNumber',
+      width: 200,
     },
     {
-      title: '姓名',
-      dataIndex: 'name',
-      customRender: renderEditableRow({
-        dataIndex: 'name',
-      }),
+      title: '下拉框',
+      dataIndex: 'name3',
+      editRow: true,
+      editComponent: 'Select',
+      editComponentProps: {
+        options: [
+          {
+            label: 'Option1',
+            value: '1',
+          },
+          {
+            label: 'Option2',
+            value: '2',
+          },
+        ],
+      },
+      width: 200,
+    },
+    {
+      title: '远程下拉',
+      dataIndex: 'name4',
+      editRow: true,
+      editComponent: 'ApiSelect',
+      editComponentProps: {
+        api: optionsListApi,
+      },
+      width: 200,
+    },
+    {
+      title: '勾选框',
+      dataIndex: 'name5',
+      editRow: true,
+
+      editComponent: 'Checkbox',
+      editValueMap: (value) => {
+        return value ? '是' : '否';
+      },
+      width: 200,
+    },
+    {
+      title: '开关',
+      dataIndex: 'name6',
+      editRow: true,
+      editComponent: 'Switch',
+      editValueMap: (value) => {
+        return value ? '开' : '关';
+      },
+      width: 200,
     },
   ];
   export default defineComponent({
@@ -55,19 +136,19 @@
 
       function handleEdit(record: EditRecordRow) {
         currentEditKeyRef.value = record.key;
-        record.editable = true;
+        record.onEdit?.(true);
       }
 
       function handleCancel(record: EditRecordRow) {
         currentEditKeyRef.value = '';
-        record.editable = false;
-        record.onCancel && record.onCancel();
+        record.onEdit?.(false, true);
       }
 
-      function handleSave(record: EditRecordRow) {
-        currentEditKeyRef.value = '';
-        record.editable = false;
-        record.onSubmit && record.onSubmit();
+      async function handleSave(record: EditRecordRow) {
+        const pass = await record.onEdit?.(false, true);
+        if (pass) {
+          currentEditKeyRef.value = '';
+        }
       }
 
       function createActions(record: EditRecordRow, column: BasicColumn): ActionItem[] {

+ 1 - 1
src/views/demo/table/FixedColumn.vue

@@ -41,7 +41,6 @@
     {
       title: '地址',
       dataIndex: 'address',
-      width: 260,
     },
     {
       title: '编号',
@@ -67,6 +66,7 @@
         api: demoListApi,
         columns: columns,
         rowSelection: { type: 'radio' },
+        bordered: true,
         actionColumn: {
           width: 160,
           title: 'Action',

+ 7 - 1
src/views/demo/table/tableData.tsx

@@ -7,12 +7,16 @@ export function getBasicColumns(): BasicColumn[] {
       title: 'ID',
       dataIndex: 'id',
       fixed: 'left',
-      width: 400,
+      width: 200,
     },
     {
       title: '姓名',
       dataIndex: 'name',
       width: 150,
+      filters: [
+        { text: 'Male', value: 'male' },
+        { text: 'Female', value: 'female' },
+      ],
     },
     {
       title: '地址',
@@ -22,11 +26,13 @@ export function getBasicColumns(): BasicColumn[] {
       title: '编号',
       dataIndex: 'no',
       width: 150,
+      sorter: true,
       defaultHidden: true,
     },
     {
       title: '开始时间',
       width: 120,
+      sorter: true,
       dataIndex: 'beginTime',
     },
     {