vben
2020-12-31 9c2f3f30bbd8abcccc4f256183ed7794da7fcda2
refactor(table): refactor table #150 #148 #146 #130 #76
3个文件已删除
5个文件已添加
27个文件已修改
1581 ■■■■ 已修改文件
CHANGELOG.zh_CN.md 59 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mock/demo/table-demo.ts 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Form/index.ts 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Form/src/components/ApiSelect.vue 5 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Menu/src/useOpenKeys.ts 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Table/index.ts 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Table/src/BasicTable.vue 99 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Table/src/componentMap.ts 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Table/src/components/EditTableHeaderIcon.vue 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Table/src/components/HeaderCell.vue 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Table/src/components/TableAction.vue 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Table/src/components/editable/CellComponent.ts 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Table/src/components/editable/EditableCell.vue 359 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Table/src/components/editable/helper.ts 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Table/src/components/editable/index.ts 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Table/src/components/renderEditable.tsx 241 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Table/src/components/renderExpandIcon.tsx 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Table/src/components/settings/ColumnSetting.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Table/src/const.ts 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Table/src/hooks/useColumns.ts 73 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Table/src/hooks/useDataSource.ts 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Table/src/hooks/useTable.ts 48 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Table/src/hooks/useTableScroll.ts 7 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Table/src/props.ts 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Table/src/style/editable-cell.less 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Table/src/style/index.less 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Table/src/types/componentType.ts 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Table/src/types/table.ts 56 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layouts/default/sider/MixSider.vue 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/menus/modules/demo/comp.ts 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/dateUtil.ts 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/demo/table/EditCellTable.vue 130 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/demo/table/EditRowTable.vue 113 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/demo/table/FixedColumn.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/demo/table/tableData.tsx 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
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
mock/demo/table-demo.ts
@@ -10,6 +10,14 @@
      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'],
    });
src/components/Form/index.ts
@@ -9,4 +9,7 @@
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 };
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 {
src/components/Menu/src/useOpenKeys.ts
@@ -15,7 +15,7 @@
  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 @@
  }
  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 @@
  }
  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);
src/components/Table/index.ts
@@ -3,7 +3,6 @@
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 type { FormSchema, FormProps } from '/@/components/Form/src/types/form';
export type { EditRecordRow } from './src/components/renderEditable';
export type { EditRecordRow } from './src/components/editable';
src/components/Table/src/BasicTable.vue
@@ -34,18 +34,18 @@
      <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';
@@ -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,32 +103,6 @@
      const { getLoading, setLoading } = useLoading(getProps);
      const { getPaginationInfo, getPagination, setPagination } = usePagination(getProps);
      const {
        getSortFixedColumns,
        getColumns,
        setColumns,
        getColumnsRef,
        getCacheColumns,
      } = useColumns(getProps, getPaginationInfo);
      const {
        getDataSourceRef,
        getDataSource,
        setTableData,
        fetch,
        getRowKey,
        reload,
        getAutoCreateKey,
      } = useDataSource(
        getProps,
        {
          getPaginationInfo,
          setLoading,
          setPagination,
          getFieldsValue: formActions.getFieldsValue,
        },
        emit
      );
      const {
        getRowSelection,
@@ -132,6 +113,33 @@
        deleteSelectRowByKey,
        setSelectedRowKeys,
      } = useRowSelection(getProps, emit);
      const {
        handleTableChange,
        getDataSourceRef,
        getDataSource,
        setTableData,
        fetch,
        getRowKey,
        reload,
        getAutoCreateKey,
        updateTableData,
      } = useDataSource(
        getProps,
        {
          getPaginationInfo,
          setLoading,
          setPagination,
          getFieldsValue: formActions.getFieldsValue,
          clearSelectedRowKeys,
        },
        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),
@@ -196,26 +204,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,
      };
    },
  });
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);
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">
src/components/Table/src/components/HeaderCell.vue
New file
@@ -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>
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,
src/components/Table/src/components/editable/CellComponent.ts
New file
@@ -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,
    }
  );
};
src/components/Table/src/components/editable/EditableCell.vue
New file
@@ -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>
src/components/Table/src/components/editable/helper.ts
New file
@@ -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 '';
}
src/components/Table/src/components/editable/index.ts
New file
@@ -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;
src/components/Table/src/components/renderEditable.tsx
File was deleted
src/components/Table/src/components/renderExpandIcon.tsx
File was deleted
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,
          });
src/components/Table/src/const.ts
@@ -32,6 +32,10 @@
  };
}
export function DEFAULT_FILTER_FN(data: Partial<Recordable<string[]>>) {
  return data;
}
//  表格单元格默认布局
export const DEFAULT_ALIGN = 'center';
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 @@
    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 @@
    }
    if (sort) {
      columns = useFixedColumn(columns);
      columns = sortFixedColumn(columns);
    }
    return columns;
@@ -200,10 +225,10 @@
    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 @@
  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;
  }
}
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,18 +16,60 @@
  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(() => {
    const { dataSource, api } = unref(propsRef);
    !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;
@@ -75,6 +117,14 @@
    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 @@
        pageParams[sizeField] = pageSize;
      }
      const { sortInfo = {}, filterInfo } = searchState;
      let params: Recordable = {
        ...pageParams,
        ...(useSearchForm ? getFieldsValue() : {}),
@@ -101,6 +153,8 @@
        ...(opt ? opt.searchInfo : {}),
        ...(opt ? opt.sortInfo : {}),
        ...(opt ? opt.filterInfo : {}),
        ...sortInfo,
        ...filterInfo,
      };
      if (beforeFetch && isFunction(beforeFetch)) {
        params = beforeFetch(params) || params;
@@ -175,5 +229,7 @@
    getAutoCreateKey,
    fetch,
    reload,
    updateTableData,
    handleTableChange,
  };
}
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 @@
      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 @@
    },
    getColumns: ({ ignoreIndex = false }: { ignoreIndex?: boolean } = {}) => {
      const columns = getTableInstance().getColumns({ ignoreIndex }) || [];
      return columns;
    },
    setColumns: (columns: BasicColumn[]) => {
@@ -87,7 +103,19 @@
    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];
}
src/components/Table/src/hooks/useTableScroll.ts
@@ -121,7 +121,7 @@
      width += 60;
    }
    // TODO props
    // TODO propsdth ?? 0;
    const NORMAL_WIDTH = 150;
    const columns = unref(columnsRef);
@@ -135,7 +135,10 @@
    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(() => {
src/components/Table/src/props.ts
@@ -9,21 +9,29 @@
  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),
src/components/Table/src/style/editable-cell.less
File was deleted
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-thead th {
        background: rgb(241, 243, 244);
      .ant-table-fixed {
        border-bottom: none;
        .ant-table-thead th {
          background: rgb(241, 243, 244);
        }
      }
    }
  }
src/components/Table/src/types/componentType.ts
@@ -1,8 +1,7 @@
export type ComponentType =
  | 'Input'
  | 'InputPassword'
  | 'InputNumber'
  | 'Select'
  | 'ApiSelect'
  | 'Checkbox'
  | 'CheckboxGroup'
  | 'Switch';
src/components/Table/src/types/table.ts
@@ -6,9 +6,10 @@
  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 @@
  children?: any;
}
export interface TableCustomRecord<T = any> {
export interface TableCustomRecord<T = Recordable> {
  record?: T;
  index?: number;
}
@@ -65,18 +66,11 @@
  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 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 @@
  getSize: () => SizeType;
  getRowSelection: () => TableRowSelection<Recordable>;
  getCacheColumns: () => BasicColumn[];
  emit?: EmitType;
  updateTableData: (index: number, key: string, value: any) => Recordable;
}
export interface FetchSetting {
@@ -131,6 +127,8 @@
  clickToRowSelect?: boolean;
  // 自定义排序方法
  sortFn?: (sortInfo: SorterResult) => any;
  // 排序方法
  filterFn?: (data: Partial<Recordable<string[]>>) => any;
  // 取消表格的默认padding
  inset?: boolean;
  // 显示表格设置
@@ -141,7 +139,7 @@
  // 是否自动生成key
  autoCreateKey?: boolean;
  // 计算合计行的方法
  summaryFunc?: (...arg: any) => any[];
  summaryFunc?: (...arg: any) => Recordable[];
  // 是否显示合计行
  showSummary?: boolean;
  // 是否可拖拽列
@@ -374,13 +372,43 @@
  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;
}
src/layouts/default/sider/MixSider.vue
@@ -351,6 +351,11 @@
        position: absolute;
        top: 10px;
        right: 30px;
        &--dot {
          top: 50%;
          margin-top: -3px;
        }
      }
      &__title {
src/router/menus/modules/demo/comp.ts
@@ -52,6 +52,9 @@
      {
        path: 'table',
        name: t('routes.demo.table.table'),
        tag: {
          dot: true,
        },
        children: [
          {
            path: 'basic',
@@ -108,10 +111,16 @@
          {
            path: 'editCellTable',
            name: t('routes.demo.table.editCellTable'),
            tag: {
              dot: true,
            },
          },
          {
            path: 'editRowTable',
            name: t('routes.demo.table.editRowTable'),
            tag: {
              dot: true,
            },
          },
        ],
      },
src/utils/dateUtil.ts
@@ -3,12 +3,15 @@
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) => {
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',
      dataIndex: 'id',
      slots: { title: 'customId' },
      customRender: renderEditableCell({ dataIndex: 'id' }),
    },
    {
      // title: '姓名',
      title: '输入框',
      dataIndex: 'name',
      slots: { title: 'customName' },
      customRender: renderEditableCell({
        dataIndex: 'name',
      }),
      edit: true,
      editComponentProps: {
        prefix: '$',
      },
      width: 200,
    },
    {
      title: '地址',
      dataIndex: 'address',
      sorter: true,
      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',
      edit: true,
      editRule: true,
      editComponent: 'InputNumber',
      width: 200,
    },
    {
      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: '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,
      };
    },
  });
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',
      dataIndex: 'id',
      customRender: renderEditableRow({ dataIndex: 'id' }),
      title: '输入框',
      dataIndex: 'name',
      editRow: true,
      editComponentProps: {
        prefix: '$',
      },
      width: 200,
    },
    {
      title: '姓名',
      dataIndex: 'name',
      customRender: renderEditableRow({
        dataIndex: 'name',
      }),
      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',
      editRow: true,
      editRule: true,
      editComponent: 'InputNumber',
      width: 200,
    },
    {
      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[] {
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',
src/views/demo/table/tableData.tsx
@@ -7,12 +7,16 @@
      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 @@
      title: '编号',
      dataIndex: 'no',
      width: 150,
      sorter: true,
      defaultHidden: true,
    },
    {
      title: '开始时间',
      width: 120,
      sorter: true,
      dataIndex: 'beginTime',
    },
    {