瀏覽代碼

Merge branch 'master' of http://192.168.100.32:3000/fanfan/xt-front

lxz 2 年之前
父節點
當前提交
6365730362
共有 91 個文件被更改,包括 6999 次插入131 次删除
  1. 2 2
      package.json
  2. 9 0
      src/api/monitor/LogApi.ts
  3. 96 0
      src/api/sys/sysConstantConfig.ts
  4. 4 4
      src/assets/iconfont/iconfont.css
  5. 1 0
      src/components/Form/src/components/ApiSelect.vue
  6. 11 0
      src/components/TableCard/index.ts
  7. 567 0
      src/components/TableCard/src/BasicTable.vue
  8. 40 0
      src/components/TableCard/src/componentMap.ts
  9. 16 0
      src/components/TableCard/src/components/EditTableHeaderIcon.vue
  10. 48 0
      src/components/TableCard/src/components/HeaderCell.vue
  11. 202 0
      src/components/TableCard/src/components/TableAction.vue
  12. 92 0
      src/components/TableCard/src/components/TableFooter.vue
  13. 58 0
      src/components/TableCard/src/components/TableImg.vue
  14. 53 0
      src/components/TableCard/src/components/TableTitle.vue
  15. 44 0
      src/components/TableCard/src/components/editable/CellComponent.ts
  16. 532 0
      src/components/TableCard/src/components/editable/EditableCell.vue
  17. 25 0
      src/components/TableCard/src/components/editable/helper.ts
  18. 68 0
      src/components/TableCard/src/components/editable/index.ts
  19. 503 0
      src/components/TableCard/src/components/settings/ColumnSetting.vue
  20. 36 0
      src/components/TableCard/src/components/settings/FullScreenSetting.vue
  21. 32 0
      src/components/TableCard/src/components/settings/RedoSetting.vue
  22. 62 0
      src/components/TableCard/src/components/settings/SizeSetting.vue
  23. 74 0
      src/components/TableCard/src/components/settings/index.vue
  24. 42 0
      src/components/TableCard/src/const.ts
  25. 349 0
      src/components/TableCard/src/hooks/useColumns.ts
  26. 100 0
      src/components/TableCard/src/hooks/useCustomRow.ts
  27. 374 0
      src/components/TableCard/src/hooks/useDataSource.ts
  28. 21 0
      src/components/TableCard/src/hooks/useLoading.ts
  29. 82 0
      src/components/TableCard/src/hooks/usePagination.tsx
  30. 128 0
      src/components/TableCard/src/hooks/useRowSelection.ts
  31. 55 0
      src/components/TableCard/src/hooks/useScrollTo.ts
  32. 170 0
      src/components/TableCard/src/hooks/useTable.ts
  33. 22 0
      src/components/TableCard/src/hooks/useTableContext.ts
  34. 65 0
      src/components/TableCard/src/hooks/useTableExpand.ts
  35. 56 0
      src/components/TableCard/src/hooks/useTableFooter.ts
  36. 220 0
      src/components/TableCard/src/hooks/useTableScroll.ts
  37. 20 0
      src/components/TableCard/src/hooks/useTableStyle.ts
  38. 179 0
      src/components/TableCard/src/props.ts
  39. 198 0
      src/components/TableCard/src/types/column.ts
  40. 14 0
      src/components/TableCard/src/types/componentType.ts
  41. 115 0
      src/components/TableCard/src/types/pagination.ts
  42. 492 0
      src/components/TableCard/src/types/table.ts
  43. 39 0
      src/components/TableCard/src/types/tableAction.ts
  44. 21 0
      src/components/XTForm/src/XTForm.vue
  45. 2 0
      src/components/XTForm/src/componentEnum.ts
  46. 4 0
      src/components/XTList/index.ts
  47. 26 1
      src/design/index.less
  48. 2 1
      src/layouts/default/feature/index.vue
  49. 0 1
      src/layouts/default/header/index.less
  50. 21 0
      src/utils/validate.ts
  51. 15 0
      src/views/biz/README.md
  52. 7 0
      src/views/biz/archives/index.vue
  53. 7 0
      src/views/biz/bed/long/index.vue
  54. 7 0
      src/views/biz/bed/memo/index.vue
  55. 91 0
      src/views/biz/bed/near/data.ts
  56. 801 0
      src/views/biz/bed/near/index.vue
  57. 7 0
      src/views/biz/bed/person/index.vue
  58. 7 0
      src/views/biz/visit/check/index.vue
  59. 7 0
      src/views/biz/visit/room/index.vue
  60. 7 0
      src/views/biz/visit/rounds/index.vue
  61. 7 0
      src/views/biz/visit/transfer/index.vue
  62. 18 0
      src/views/infra/numStrategy/data.ts
  63. 1 1
      src/views/infra/numStrategy/formDrawer.vue
  64. 7 24
      src/views/monitor/operLog/index.vue
  65. 111 0
      src/views/sys/sysConstant/ConstantConfig/data.ts
  66. 71 0
      src/views/sys/sysConstant/ConstantConfig/formDrawer.vue
  67. 186 0
      src/views/sys/sysConstant/ConstantConfig/index.vue
  68. 40 0
      src/views/sys/sysConstant/ConstantConfig/viewDrawer.vue
  69. 17 0
      src/views/sys/sysDict/sysDictItemTable/data.ts
  70. 4 4
      src/views/sys/sysDict/sysDictItemTable/index.vue
  71. 18 1
      src/views/sys/sysDict/sysDictTable/data.ts
  72. 4 4
      src/views/sys/sysDict/sysDictTable/index.vue
  73. 14 3
      src/views/sys/sysMenu/sysMenuTable/FormModal.vue
  74. 14 3
      src/views/sys/sysOrg/sysOrgTable/FormModal.vue
  75. 10 1
      src/views/sys/sysOrg/sysOrgTable/index.vue
  76. 1 1
      src/views/sys/sysPortal/FormModalPortalMenu.vue
  77. 17 32
      src/views/sys/sysPortal/data.ts
  78. 0 4
      src/views/sys/sysPortal/index.vue
  79. 28 15
      src/views/sys/sysPos/data.ts
  80. 1 0
      src/views/sys/sysPos/formDrawer.vue
  81. 5 5
      src/views/sys/sysPos/index.vue
  82. 1 1
      src/views/sys/sysRole/FormModal.vue
  83. 23 5
      src/views/sys/sysRole/data.ts
  84. 5 3
      src/views/sys/sysRole/index.vue
  85. 9 0
      src/views/sys/sysSms/channel/data.ts
  86. 1 1
      src/views/sys/sysSms/channel/formDrawer.vue
  87. 20 3
      src/views/sys/sysSms/temp/data.ts
  88. 1 1
      src/views/sys/sysSms/temp/index.vue
  89. 2 4
      src/views/sys/sysTenant/page/data.ts
  90. 9 0
      src/views/sys/sysUser/sysUserTable/FormModal.vue
  91. 6 6
      src/views/sys/sysUser/sysUserTable/data.ts

+ 2 - 2
package.json

@@ -39,8 +39,8 @@
     "@codemirror/view": "^6.9.2",
     "@highlightjs/vue-plugin": "^2.1.0",
     "@pureadmin/utils": "^1.8.5",
-    "@vueuse/core": "^9.6.0",
-    "@vueuse/motion": "2.0.0-beta.12",
+    "@vueuse/core": "^10.1.2",
+    "@vueuse/motion": "2.0.0",
     "@wangeditor/editor": "^5.1.23",
     "@wangeditor/editor-for-vue": "^5.1.12",
     "animate.css": "^4.1.1",

+ 9 - 0
src/api/monitor/LogApi.ts

@@ -7,6 +7,7 @@ enum Api {
   LogAdd = '/sys/log/add',
   LogEdit = '/sys/log/edit',
   LogRemove = '/sys/log/removeByIds',
+  LogExport = '/sys/log/removeByIds',
 }
 
 /**
@@ -128,3 +129,11 @@ export const LogEdit = (params?: object) => {
 export const LogRemove = (params: Array<string | number>) => {
   return defHttp.post({ url: Api.LogRemove, params: params });
 };
+
+/**
+ * @description: 导出,权限 - sys:log:export
+ * @method: POST
+ */
+export const LogExport = (params: Array<string | number>) => {
+  return defHttp.post({ url: Api.LogExport, params: params });
+};

+ 96 - 0
src/api/sys/sysConstantConfig.ts

@@ -0,0 +1,96 @@
+import { defHttp } from '/@/utils/http/axios';
+import { setParams } from '/@/utils/index';
+
+enum Api {
+  sysConfigQueryPage = '/sys/constant/config/query/page',
+  sysConfigDetail = '/sys/constant/config/detail',
+  sysConfigAdd = '/sys/constant/config/add',
+  sysConfigEdit = '/sys/constant/config/edit',
+  sysConfigRemove = '/sys/constant/config/removeByIds',
+}
+
+/**
+ *
+ * @author zsl
+ * @date  2023/05/17 15:41
+ * @description: 根据条件查询常量配置列表,权限 - constant:config:query
+ * @method: POST
+ * @param:
+ *       {String}  name  like   常量名称
+ *       {String}  code  eq   常量编码
+ * @return:
+ *       {String}  name  常量名称
+ *       {Integer}  type  常量类型
+ *       {String}  remark  备注
+ *       {String}  code  常量编码
+ *       {String}  cateid  目录id
+ */
+
+export const sysConfigQueryPage = (params?: object) => {
+  return defHttp.post({ url: Api.sysConfigQueryPage, params: setParams(params) });
+};
+/**
+ *
+ * @author zsl
+ * @date  2023/05/17 15:41
+ * @description: 根据id查询常量配置详细信息,权限 - constant:config:query
+ * @method: GET
+ * @param:  id 常量配置主键id
+ * @return:
+ *       {String}  name  常量名称
+ *       {Integer}  type  常量类型
+ *       {String}  remark  备注
+ *       {String}  code  常量编码
+ *       {String}  cateid  目录id
+ */
+export const sysConfigDetail = (id: string) => {
+  return defHttp.get({ url: Api.sysConfigDetail + '/' + id });
+};
+
+/**
+ *
+ * @author zsl
+ * @date  2023/05/17 15:41
+ * @description: 添加常量配置,权限 - constant:config:add
+ * @method: POST
+ * @param:
+ *       {String}  name  常量名称
+ *       {Integer}  type  常量类型
+ *       {String}  remark  备注
+ *       {String}  code  常量编码
+ *       {String}  cateid  目录id
+ * @return:
+ *       0 添加失败
+ *       1 添加成功
+ */
+export const sysConfigAdd = (params?: object) => {
+  return defHttp.post({ url: Api.sysConfigAdd, params: params });
+};
+
+/**
+ *
+ * @author zsl
+ * @date  2023/05/17 15:41
+ * @description: 通过主键id编辑常量配置,权限 - constant:config:edit
+ * @method: POST
+ * @param:
+ *       {String}  name  常量名称
+ *       {Integer}  type  常量类型
+ *       {String}  remark  备注
+ *       {String}  code  常量编码
+ *       {String}  cateid  目录id
+ * @return:
+ *       0 编辑失败
+ *       1 编辑成功
+ */
+export const sysConfigEdit = (params?: object) => {
+  return defHttp.post({ url: Api.sysConfigEdit, params: params });
+};
+
+/**
+ * @description: 删除,权限 - constant:config:remove
+ * @method: POST
+ */
+export const sysConfigRemove = (params: Array<string | number>) => {
+  return defHttp.post({ url: Api.sysConfigRemove, params: params });
+};

+ 4 - 4
src/assets/iconfont/iconfont.css

@@ -1,8 +1,8 @@
 @font-face {
   font-family: 'iconfont'; /* Project id 3806176 */
-  src: url('//at.alicdn.com/t/c/font_3806176_bwn3g1i4dft.woff2?t=1685318825278') format('woff2'),
-    url('//at.alicdn.com/t/c/font_3806176_bwn3g1i4dft.woff?t=1685318825278') format('woff'),
-    url('//at.alicdn.com/t/c/font_3806176_bwn3g1i4dft.ttf?t=1685318825278') format('truetype');
+  src: url('//at.alicdn.com/t/c/font_3806176_em9e5cie965.woff2?t=1685580968021') format('woff2'),
+    url('//at.alicdn.com/t/c/font_3806176_em9e5cie965.woff?t=1685580968021') format('woff'),
+    url('//at.alicdn.com/t/c/font_3806176_em9e5cie965.ttf?t=1685580968021') format('truetype');
 }
 
 .iconfont {
@@ -14,7 +14,7 @@
 }
 
 .icon-xt-dual-pump_default:before {
-  content: '\e73d';
+  content: '\e73f';
 }
 
 .icon-xt-bed_disable:before {

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

@@ -36,6 +36,7 @@
   export default defineComponent({
     name: 'ApiSelect',
     components: {
+      // eslint-disable-next-line vue/no-reserved-component-names
       Select,
       LoadingOutlined,
     },

+ 11 - 0
src/components/TableCard/index.ts

@@ -0,0 +1,11 @@
+export { default as BasicTable } from './src/BasicTable.vue';
+export { default as TableAction } from './src/components/TableAction.vue';
+export { default as EditTableHeaderIcon } from './src/components/EditTableHeaderIcon.vue';
+export { default as TableImg } from './src/components/TableImg.vue';
+
+export * from './src/types/table';
+export * from './src/types/pagination';
+export * from './src/types/tableAction';
+export { useTable } from './src/hooks/useTable';
+export type { FormSchema, FormProps } from '/@/components/Form/src/types/form';
+export type { EditRecordRow } from './src/components/editable';

+ 567 - 0
src/components/TableCard/src/BasicTable.vue

@@ -0,0 +1,567 @@
+<template>
+  <div>
+    <div ref="wrapRef" :class="getWrapperClass">
+      <Table
+        ref="tableElRef"
+        v-bind="getBindValues"
+        :rowClassName="getRowClassName"
+        v-show="getEmptyDataIsShowTable"
+        :position="['topRight']"
+        @change="handleTableChange"
+      >
+        <template #[item]="data" v-for="item in Object.keys($slots)" :key="item">
+          <slot :name="item" v-bind="data || {}" />
+        </template>
+        <template #headerCell="{ column }">
+          <HeaderCell :column="column" />
+        </template>
+        <!-- 增加对antdv3.x兼容 -->
+        <template #bodyCell="data">
+          <slot name="bodyCell" v-bind="data || {}" />
+        </template>
+      </Table>
+    </div>
+    <div class="p-2 batch_style" :style="{ left: batchLeft }" v-if="getSelectRowKeys()?.length">
+      <Checkbox
+        :indeterminate="indeterminate"
+        :checked="checkedAll"
+        @change="onCheckAllChange"
+        style="margin-right: 10px"
+      />
+      <Button
+        type="primary"
+        ghost
+        @click="batchExport"
+        v-if="getProps.batchExportApi"
+        v-auth="getProps.exportAuthList"
+        >导出选中项</Button
+      >
+      <Button danger @click="batchDel" v-if="getProps.batchDelApi" v-auth="getProps.delAuthList"
+        >删除选中项</Button
+      >
+      <span>当前选中"{{ getSelectRowKeys()?.length }}"行列表</span>
+    </div>
+  </div>
+</template>
+<script lang="ts">
+  import type { BasicTableProps, TableActionType, SizeType } from './types/table';
+
+  import { defineComponent, ref, computed, unref, toRaw, inject, watchEffect, watch } from 'vue';
+  import { Checkbox, Table, Button } from 'ant-design-vue';
+  import { useForm } from '/@/components/Form/index';
+  import { PageWrapperFixedHeightKey } from '/@/components/Page';
+  import HeaderCell from './components/HeaderCell.vue';
+  import { usePagination } from './hooks/usePagination';
+  import { useColumns } from './hooks/useColumns';
+  import { useDataSource } from './hooks/useDataSource';
+  import { useLoading } from './hooks/useLoading';
+  import { useRowSelection } from './hooks/useRowSelection';
+  import { useTableScroll } from './hooks/useTableScroll';
+  import { useTableScrollTo } from './hooks/useScrollTo';
+  import { useCustomRow } from './hooks/useCustomRow';
+  import { useTableStyle } from './hooks/useTableStyle';
+  import { useTableExpand } from './hooks/useTableExpand';
+  import { createTableContext } from './hooks/useTableContext';
+  import { useTableFooter } from './hooks/useTableFooter';
+  import { useDesign } from '/@/hooks/web/useDesign';
+  import { useMessage } from '/@/hooks/web/useMessage';
+
+  import { omit } from 'lodash-es';
+  import { basicProps } from './props';
+  import { isFunction } from '/@/utils/is';
+  import { warn } from '/@/utils/log';
+  import { useAppStore } from '/@/store/modules/app';
+
+  export default defineComponent({
+    components: {
+      Checkbox,
+      Table,
+      Button,
+      HeaderCell,
+    },
+    props: basicProps,
+    emits: [
+      'fetch-success',
+      'fetch-error',
+      'selection-change',
+      'register',
+      'row-click',
+      'row-dbClick',
+      'row-contextmenu',
+      'row-mouseenter',
+      'row-mouseleave',
+      'edit-end',
+      'edit-cancel',
+      'edit-row-end',
+      'edit-change',
+      'expanded-rows-change',
+      'change',
+      'columns-change',
+    ],
+    setup(props, { attrs, emit, expose }) {
+      const useApp = useAppStore();
+      const batchLeft = ref(useApp.getMenuSetting?.collapsed ? '80px' : '190px');
+      const indeterminate = ref(true);
+      const checkedAll = ref(false);
+
+      const tableElRef = ref(null);
+      const tableData = ref<Recordable[]>([]);
+
+      const wrapRef = ref(null);
+      const formRef = ref(null);
+      const innerPropsRef = ref<Partial<BasicTableProps>>();
+
+      const { prefixCls } = useDesign('basic-table');
+      const [registerForm, formActions] = useForm();
+
+      const getProps = computed(() => {
+        return { ...props, ...unref(innerPropsRef) } as BasicTableProps;
+      });
+
+      function setProps(props: Partial<BasicTableProps>) {
+        innerPropsRef.value = { ...unref(innerPropsRef), ...props };
+      }
+      const isFixedHeightPage = inject(PageWrapperFixedHeightKey, false);
+      watch(
+        () => useApp.getMenuSetting,
+        (value, _oldValue) => {
+          // console.log('value', value);
+          // console.log('oldValue', oldValue);
+          batchLeft.value = value.collapsed ? '80px' : '190px';
+        },
+      );
+
+      const { getLoading, setLoading } = useLoading(getProps);
+      const {
+        getPaginationInfo,
+        getPagination,
+        setPagination,
+        setShowPagination,
+        getShowPagination,
+      } = usePagination(getProps);
+
+      const {
+        getRowSelection,
+        getRowSelectionRef,
+        getSelectRows,
+        setSelectedRows,
+        clearSelectedRowKeys,
+        getSelectRowKeys,
+        deleteSelectRowByKey,
+        setSelectedRowKeys,
+      } = useRowSelection(getProps, tableData, emit);
+
+      const {
+        handleTableChange: onTableChange,
+        getDataSourceRef,
+        getDataSource,
+        getRawDataSource,
+        setTableData,
+        updateTableDataRecord,
+        deleteTableDataRecord,
+        insertTableDataRecord,
+        findTableDataRecord,
+        getRowKey,
+        reload,
+        getAutoCreateKey,
+        updateTableData,
+      } = useDataSource(
+        getProps,
+        {
+          tableData,
+          getPaginationInfo,
+          setLoading,
+          setPagination,
+          getFieldsValue: formActions.getFieldsValue,
+          clearSelectedRowKeys,
+        },
+        emit,
+      );
+      // 动态变化底部margin
+      const mbottom = ref('margin0');
+      const getWrapperClass = computed(() => {
+        const values = unref(getBindValues);
+        return [
+          prefixCls,
+          attrs.class,
+          {
+            [`${prefixCls}-form-container`]: values.useSearchForm,
+            [`${prefixCls}--inset`]: values.inset,
+          },
+          mbottom.value,
+        ];
+      });
+
+      watchEffect(() => {
+        indeterminate.value =
+          getSelectRowKeys()?.length && getSelectRowKeys()?.length < getDataSource()?.length;
+        mbottom.value = getSelectRowKeys()?.length > 0 ? 'margin70' : 'margin0';
+        checkedAll.value = getSelectRowKeys()?.length === getDataSource()?.length;
+        unref(isFixedHeightPage) &&
+          props.canResize &&
+          warn(
+            "'canResize' of BasicTable may not work in PageWrapper with 'fixedHeight' (especially in hot updates)",
+          );
+      });
+
+      function handleTableChange(...args) {
+        onTableChange.call(undefined, ...args);
+        emit('change', ...args);
+        // 解决通过useTable注册onChange时不起作用的问题
+        const { onChange } = unref(getProps);
+        onChange && isFunction(onChange) && onChange.call(undefined, ...args);
+      }
+      // 批量删除
+      const { createMessage, createConfirm } = useMessage();
+      function batchDel() {
+        if (!innerPropsRef.value.batchDelApi || !isFunction(innerPropsRef.value.batchDelApi)) {
+          createMessage.error('未找到批量操作入口!');
+          return;
+        }
+        createConfirm({
+          content: '确定要删除选中数据?',
+          iconType: 'warning',
+          onOk: async () => {
+            const keys = getSelectRowKeys();
+            await innerPropsRef.value.batchDelApi(keys);
+            createMessage.success('删除成功!');
+            await reload();
+            clearSelectedRowKeys();
+          },
+        });
+      }
+      // 批量导出
+      async function batchExport() {
+        // const selectRow = getSelectRows();
+        if (!innerPropsRef.value.batchDelApi || !isFunction(innerPropsRef.value.batchDelApi)) {
+          createMessage.error('未找到批量操作入口!');
+          return;
+        }
+        // const keys = getSelectRowKeys();
+        // await innerPropsRef.value.batchDelApi(keys);
+        // createMessage.success('删除成功!');
+      }
+      const {
+        getViewColumns,
+        getColumns,
+        setCacheColumnsByField,
+        setColumns,
+        getColumnsRef,
+        getCacheColumns,
+      } = useColumns(getProps, getPaginationInfo);
+
+      const { getScrollRef, redoHeight } = useTableScroll(
+        getProps,
+        tableElRef,
+        getColumnsRef,
+        getRowSelectionRef,
+        getDataSourceRef,
+        wrapRef,
+        formRef,
+      );
+
+      const { scrollTo } = useTableScrollTo(tableElRef, getDataSourceRef);
+
+      const { customRow } = useCustomRow(getProps, {
+        setSelectedRowKeys,
+        getSelectRowKeys,
+        clearSelectedRowKeys,
+        getAutoCreateKey,
+        emit,
+      });
+
+      const { getRowClassName } = useTableStyle(getProps, prefixCls);
+
+      const { getExpandOption, expandAll, expandRows, collapseAll } = useTableExpand(
+        getProps,
+        tableData,
+        emit,
+      );
+
+      const { getFooterProps } = useTableFooter(
+        getProps,
+        getScrollRef,
+        tableElRef,
+        getDataSourceRef,
+      );
+
+      const getBindValues = computed(() => {
+        const dataSource = unref(getDataSourceRef);
+        let propsData: Recordable = {
+          ...attrs,
+          customRow,
+          ...unref(getProps),
+          scroll: unref(getScrollRef),
+          loading: unref(getLoading),
+          tableLayout: 'fixed',
+          rowSelection: unref(getRowSelectionRef),
+          rowKey: unref(getRowKey),
+          columns: toRaw(unref(getViewColumns)),
+          pagination: toRaw(unref(getPaginationInfo)),
+          dataSource,
+          footer: unref(getFooterProps),
+          ...unref(getExpandOption),
+        };
+
+        propsData = omit(propsData, ['class', 'onChange']);
+        return propsData;
+      });
+
+      const getEmptyDataIsShowTable = computed(() => {
+        const { emptyDataIsShowTable, useSearchForm } = unref(getProps);
+        if (emptyDataIsShowTable || !useSearchForm) {
+          return true;
+        }
+        return !!unref(getDataSourceRef).length;
+      });
+
+      function onCheckAllChange(e: any) {
+        if (e.target.checked) {
+          const selectRows = [];
+          getDataSource().forEach(data => {
+            selectRows.push(data.id);
+          });
+          setSelectedRowKeys(selectRows);
+        } else {
+          clearSelectedRowKeys();
+        }
+      }
+
+      const tableAction: TableActionType = {
+        reload,
+        getSelectRows,
+        setSelectedRows,
+        clearSelectedRowKeys,
+        getSelectRowKeys,
+        deleteSelectRowByKey,
+        setPagination,
+        setTableData,
+        updateTableDataRecord,
+        deleteTableDataRecord,
+        insertTableDataRecord,
+        findTableDataRecord,
+        redoHeight,
+        setSelectedRowKeys,
+        setColumns,
+        setLoading,
+        getDataSource,
+        getRawDataSource,
+        setProps,
+        getRowSelection,
+        getPaginationRef: getPagination,
+        getColumns,
+        getCacheColumns,
+        emit,
+        updateTableData,
+        setShowPagination,
+        getShowPagination,
+        setCacheColumnsByField,
+        expandAll,
+        expandRows,
+        collapseAll,
+        scrollTo,
+        getSize: () => {
+          return unref(getBindValues).size as SizeType;
+        },
+      };
+      createTableContext({ ...tableAction, wrapRef, getBindValues });
+
+      expose(tableAction);
+
+      emit('register', tableAction, formActions);
+
+      return {
+        formRef,
+        tableElRef,
+        getBindValues,
+        getLoading,
+        registerForm,
+        getEmptyDataIsShowTable,
+        handleTableChange,
+        getRowClassName,
+        wrapRef,
+        tableAction,
+        redoHeight,
+        getWrapperClass,
+        columns: getViewColumns,
+        getSelectRowKeys,
+        batchDel,
+        batchExport,
+        batchLeft,
+        indeterminate,
+        checkedAll,
+        onCheckAllChange,
+        getProps,
+      };
+    },
+  });
+</script>
+<style lang="less">
+  @border-color: #cecece4d;
+
+  @prefix-cls: ~'@{namespace}-basic-table';
+
+  [data-theme='dark'] {
+    .ant-table-tbody > tr:hover.ant-table-row-selected > td,
+    .ant-table-tbody > tr.ant-table-row-selected td {
+      background-color: #262626;
+    }
+  }
+
+  .margin0 {
+    margin-bottom: 0;
+  }
+
+  .margin70 {
+    margin-bottom: 70px;
+  }
+  .@{prefix-cls} {
+    max-width: 100%;
+    height: 100%;
+
+    &-row__striped {
+      td {
+        background-color: @app-content-background;
+      }
+    }
+
+    &-form-container {
+      margin-bottom: 62px;
+      padding: 16px;
+      background: #f4f6f9 !important;
+
+      .ant-form {
+        width: 100%;
+        padding: 12px 10px 6px;
+        margin-bottom: 16px;
+        background-color: #f4f6f9;
+        border-radius: 2px;
+      }
+    }
+
+    .ant-tag {
+      margin-right: 0;
+    }
+
+    .ant-table-wrapper {
+      padding: 0 !important;
+      background-color: #f4f6f9;
+      border-radius: 2px;
+
+      .ant-table-thead > tr > th {
+        background: #f4f6f9;
+      }
+
+      .ant-table-title {
+        min-height: 40px;
+        padding: 0 0 8px !important;
+      }
+
+      .ant-table.ant-table-bordered .ant-table-title {
+        border: none !important;
+        background: #f4f6f9;
+      }
+
+      .ant-table.ant-table-bordered > .ant-table-container {
+        border: none !important;
+      }
+    }
+
+    .ant-table {
+      width: 100%;
+      overflow-x: hidden;
+
+      &-title {
+        display: flex;
+        padding: 8px 6px;
+        border-bottom: none;
+        justify-content: space-between;
+        align-items: center;
+      }
+
+      .ant-table-body {
+        background: #f4f6f9;
+        border: none !important;
+        height: auto !important;
+        max-height: 100% !important;
+      }
+
+      .ant-checkbox-inner {
+        border-spacing: 0;
+      }
+
+      .ant-table-body > table {
+        border: none !important;
+        border-collapse: separate;
+        border-spacing: 0 12px;
+        margin-top: -20px;
+
+        tr {
+          border: none !important;
+
+          td {
+            border: none !important;
+          }
+        }
+
+        .ant-table-tbody > tr {
+          background: #fff;
+        }
+      }
+
+      .ant-table-thead > tr > th {
+        background: #f4f6f9 !important;
+        color: #818694 !important;
+        border: none !important;
+      }
+    }
+
+    .ant-pagination {
+      margin: 10px 0 0;
+    }
+
+    .ant-table-footer {
+      padding: 0;
+
+      .ant-table-wrapper {
+        padding: 0 !important;
+      }
+
+      table {
+        border: none !important;
+      }
+
+      .ant-table-body {
+        overflow-x: hidden !important;
+        //  overflow-y: scroll !important;
+      }
+
+      td {
+        padding: 12px 8px;
+      }
+    }
+
+    &--inset {
+      .ant-table-wrapper {
+        padding: 0;
+      }
+    }
+  }
+
+  .batch_style {
+    position: fixed; // 使按钮固定于可视窗口的底部
+    bottom: 0;
+    right: 8px;
+    height: 62px; // 设置固定高度
+    line-height: 62px;
+    background: #fff;
+    z-index: 3;
+    width: 100%;
+    // border: #9b9ea8 solid 1px;
+    box-shadow: 0 -2px 18px 0 rgb(0 37 74 / 12%);
+
+    button {
+      margin-right: 10px;
+    }
+  }
+</style>

+ 40 - 0
src/components/TableCard/src/componentMap.ts

@@ -0,0 +1,40 @@
+import type { Component } from 'vue';
+import {
+  Input,
+  Select,
+  Checkbox,
+  InputNumber,
+  Switch,
+  DatePicker,
+  TimePicker,
+  AutoComplete,
+  Radio,
+} from 'ant-design-vue';
+import type { ComponentType } from './types/componentType';
+import { ApiSelect, ApiTreeSelect, RadioButtonGroup, ApiRadioGroup } from '/@/components/Form';
+
+const componentMap = new Map<ComponentType, Component>();
+
+componentMap.set('Input', Input);
+componentMap.set('InputNumber', InputNumber);
+componentMap.set('Select', Select);
+componentMap.set('ApiSelect', ApiSelect);
+componentMap.set('AutoComplete', AutoComplete);
+componentMap.set('ApiTreeSelect', ApiTreeSelect);
+componentMap.set('Switch', Switch);
+componentMap.set('Checkbox', Checkbox);
+componentMap.set('DatePicker', DatePicker);
+componentMap.set('TimePicker', TimePicker);
+componentMap.set('RadioGroup', Radio.Group);
+componentMap.set('RadioButtonGroup', RadioButtonGroup);
+componentMap.set('ApiRadioGroup', ApiRadioGroup);
+
+export function add(compName: ComponentType, component: Component) {
+  componentMap.set(compName, component);
+}
+
+export function del(compName: ComponentType) {
+  componentMap.delete(compName);
+}
+
+export { componentMap };

+ 16 - 0
src/components/TableCard/src/components/EditTableHeaderIcon.vue

@@ -0,0 +1,16 @@
+<template>
+  <span>
+    <slot />
+    {{ title }}
+    <FormOutlined />
+  </span>
+</template>
+<script lang="ts">
+  import { defineComponent } from 'vue';
+  import { FormOutlined } from '@ant-design/icons-vue';
+  export default defineComponent({
+    name: 'EditTableHeaderIcon',
+    components: { FormOutlined },
+    props: { title: { type: String, default: '' } },
+  });
+</script>

+ 48 - 0
src/components/TableCard/src/components/HeaderCell.vue

@@ -0,0 +1,48 @@
+<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 BasicHelp from '/@/components/Basic/src/BasicHelp.vue';
+  import EditTableHeaderCell from './EditTableHeaderIcon.vue';
+  import { useDesign } from '/@/hooks/web/useDesign';
+
+  export default defineComponent({
+    name: 'TableHeaderCell',
+    components: {
+      EditTableHeaderCell,
+      BasicHelp,
+    },
+    props: {
+      column: {
+        type: Object as PropType<BasicColumn>,
+        default: () => ({} as any),
+      },
+    },
+    setup(props) {
+      const { prefixCls } = useDesign('basic-table-header-cell');
+
+      const getIsEdit = computed(() => !!props.column?.edit);
+      const getTitle = computed(() => props.column?.customTitle || props.column?.title);
+      const getHelpMessage = computed(() => 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: rgb(0 0 0 / 65%) !important;
+    }
+  }
+</style>

+ 202 - 0
src/components/TableCard/src/components/TableAction.vue

@@ -0,0 +1,202 @@
+<template>
+  <div :class="[prefixCls, getAlign]" @click="onCellClick">
+    <template v-for="(action, index) in getActions" :key="`${index}-${action.label}`">
+      <Tooltip v-if="action.tooltip" v-bind="getTooltip(action.tooltip)">
+        <PopConfirmButton v-bind="action">
+          <Icon :icon="action.icon" :class="{ 'mr-1': !!action.label }" v-if="action.icon" />
+          <template v-if="action.label">{{ action.label }}</template>
+        </PopConfirmButton>
+      </Tooltip>
+      <PopConfirmButton v-else v-bind="action">
+        <Icon :icon="action.icon" :class="{ 'mr-1': !!action.label }" v-if="action.icon" />
+        <template v-if="action.label">{{ action.label }}</template>
+      </PopConfirmButton>
+      <Divider
+        type="vertical"
+        class="action-divider"
+        v-if="divider && index < getActions.length - 1"
+      />
+    </template>
+    <Dropdown
+      :trigger="['hover']"
+      :dropMenuList="getDropdownList"
+      popconfirm
+      v-if="dropDownActions && getDropdownList.length > 0"
+    >
+      <slot name="more" />
+      <a-button type="link" size="small" v-if="!$slots.more">
+        <MoreOutlined class="icon-more" />
+      </a-button>
+    </Dropdown>
+  </div>
+</template>
+<script lang="ts">
+  import { defineComponent, PropType, computed, toRaw, unref } from 'vue';
+  import { MoreOutlined } from '@ant-design/icons-vue';
+  import { Divider, Tooltip, TooltipProps } from 'ant-design-vue';
+  import Icon from '/@/components/Icon/index';
+  import { ActionItem, TableActionType } from '/@/components/Table';
+  import { PopConfirmButton } from '/@/components/Button';
+  import { Dropdown } from '/@/components/Dropdown';
+  import { useDesign } from '/@/hooks/web/useDesign';
+  import { useTableContext } from '../hooks/useTableContext';
+  import { usePermission } from '/@/hooks/web/usePermission';
+  import { isBoolean, isFunction, isString } from '/@/utils/is';
+  import { propTypes } from '/@/utils/propTypes';
+  import { ACTION_COLUMN_FLAG } from '../const';
+
+  export default defineComponent({
+    name: 'TableAction',
+    components: { Icon, PopConfirmButton, Divider, Dropdown, MoreOutlined, Tooltip },
+    props: {
+      actions: {
+        type: Array as PropType<ActionItem[]>,
+        default: null,
+      },
+      dropDownActions: {
+        type: Array as PropType<ActionItem[]>,
+        default: null,
+      },
+      divider: propTypes.bool.def(true),
+      outside: propTypes.bool,
+      stopButtonPropagation: propTypes.bool.def(false),
+    },
+    setup(props) {
+      const { prefixCls } = useDesign('basic-table-action');
+      let table: Partial<TableActionType> = {};
+      if (!props.outside) {
+        table = useTableContext();
+      }
+
+      const { hasPermission } = usePermission();
+      function isIfShow(action: ActionItem): boolean {
+        const ifShow = action.ifShow;
+
+        let isIfShow = true;
+
+        if (isBoolean(ifShow)) {
+          isIfShow = ifShow;
+        }
+        if (isFunction(ifShow)) {
+          isIfShow = ifShow(action);
+        }
+        return isIfShow;
+      }
+
+      const getActions = computed(() => {
+        return (toRaw(props.actions) || [])
+          .filter(action => {
+            return hasPermission(action.auth) && isIfShow(action);
+          })
+          .map(action => {
+            const { popConfirm } = action;
+            return {
+              getPopupContainer: () => unref((table as any)?.wrapRef.value) ?? document.body,
+              type: 'link',
+              size: 'small',
+              ...action,
+              ...(popConfirm || {}),
+              onConfirm: popConfirm?.confirm,
+              onCancel: popConfirm?.cancel,
+              enable: !!popConfirm,
+            };
+          });
+      });
+
+      const getDropdownList = computed((): any[] => {
+        const list = (toRaw(props.dropDownActions) || []).filter(action => {
+          return hasPermission(action.auth) && isIfShow(action);
+        });
+        return list.map((action, index) => {
+          const { label, popConfirm } = action;
+          return {
+            ...action,
+            ...popConfirm,
+            onConfirm: popConfirm?.confirm,
+            onCancel: popConfirm?.cancel,
+            text: label,
+            divider: index < list.length - 1 ? props.divider : false,
+          };
+        });
+      });
+
+      const getAlign = computed(() => {
+        const columns = (table as TableActionType)?.getColumns?.() || [];
+        const actionColumn = columns.find(item => item.flag === ACTION_COLUMN_FLAG);
+        return actionColumn?.align ?? 'left';
+      });
+
+      function getTooltip(data: string | TooltipProps): TooltipProps {
+        return {
+          getPopupContainer: () => unref((table as any)?.wrapRef.value) ?? document.body,
+          placement: 'bottom',
+          ...(isString(data) ? { title: data } : data),
+        };
+      }
+
+      function onCellClick(e: MouseEvent) {
+        if (!props.stopButtonPropagation) return;
+        const path = e.composedPath() as HTMLElement[];
+        const isInButton = path.find(ele => {
+          return ele.tagName?.toUpperCase() === 'BUTTON';
+        });
+        isInButton && e.stopPropagation();
+      }
+
+      return { prefixCls, getActions, getDropdownList, getAlign, onCellClick, getTooltip };
+    },
+  });
+</script>
+<style lang="less">
+  @prefix-cls: ~'@{namespace}-basic-table-action';
+
+  .@{prefix-cls} {
+    display: flex;
+    align-items: center;
+
+    .action-divider {
+      display: table;
+    }
+
+    &.left {
+      justify-content: flex-start;
+    }
+
+    &.center {
+      justify-content: center;
+    }
+
+    &.right {
+      justify-content: flex-end;
+    }
+
+    button {
+      display: flex;
+      align-items: center;
+
+      span {
+        margin-left: 0 !important;
+      }
+    }
+
+    button.ant-btn-circle {
+      span {
+        margin: auto !important;
+      }
+    }
+
+    .ant-divider,
+    .ant-divider-vertical {
+      margin: 0 2px;
+    }
+
+    .icon-more {
+      transform: rotate(90deg);
+
+      svg {
+        font-size: 1.1em;
+        font-weight: 700;
+      }
+    }
+  }
+</style>

+ 92 - 0
src/components/TableCard/src/components/TableFooter.vue

@@ -0,0 +1,92 @@
+<template>
+  <Table
+    v-if="summaryFunc || summaryData"
+    :showHeader="false"
+    :bordered="false"
+    :pagination="false"
+    :dataSource="getDataSource"
+    :rowKey="r => r[rowKey]"
+    :columns="getColumns"
+    tableLayout="fixed"
+    :scroll="scroll"
+  />
+</template>
+<script lang="ts">
+  import type { PropType } from 'vue';
+  import { defineComponent, unref, computed, toRaw } from 'vue';
+  import { cloneDeep } from 'lodash-es';
+  import { isFunction } from '/@/utils/is';
+  import type { BasicColumn } from '../types/table';
+  import { INDEX_COLUMN_FLAG } from '../const';
+  import { propTypes } from '/@/utils/propTypes';
+  import { useTableContext } from '../hooks/useTableContext';
+
+  const SUMMARY_ROW_KEY = '_row';
+  const SUMMARY_INDEX_KEY = '_index';
+  export default defineComponent({
+    name: 'BasicTableFooter',
+    props: {
+      summaryFunc: {
+        type: Function as PropType<Fn>,
+      },
+      summaryData: {
+        type: Array as PropType<Recordable[]>,
+      },
+      scroll: {
+        type: Object as PropType<Recordable>,
+      },
+      rowKey: propTypes.string.def('key'),
+    },
+    setup(props) {
+      const table = useTableContext();
+
+      const getDataSource = computed((): Recordable[] => {
+        const { summaryFunc, summaryData } = props;
+        if (summaryData?.length) {
+          summaryData.forEach((item, i) => (item[props.rowKey] = `${i}`));
+          return summaryData;
+        }
+        if (!isFunction(summaryFunc)) {
+          return [];
+        }
+        let dataSource = toRaw(unref(table.getDataSource()));
+        dataSource = summaryFunc(dataSource);
+        dataSource.forEach((item, i) => {
+          item[props.rowKey] = `${i}`;
+        });
+        return dataSource;
+      });
+
+      const getColumns = computed(() => {
+        const dataSource = unref(getDataSource);
+        const columns: BasicColumn[] = cloneDeep(table.getColumns());
+        const index = columns.findIndex(item => item.flag === INDEX_COLUMN_FLAG);
+        const hasRowSummary = dataSource.some(item => Reflect.has(item, SUMMARY_ROW_KEY));
+        const hasIndexSummary = dataSource.some(item => Reflect.has(item, SUMMARY_INDEX_KEY));
+
+        if (index !== -1) {
+          if (hasIndexSummary) {
+            columns[index].customRender = ({ record }) => record[SUMMARY_INDEX_KEY];
+            columns[index].ellipsis = false;
+          } else {
+            Reflect.deleteProperty(columns[index], 'customRender');
+          }
+        }
+
+        if (table.getRowSelection() && hasRowSummary) {
+          const isFixed = columns.some(col => col.fixed === 'left');
+          columns.unshift({
+            width: 60,
+            title: 'selection',
+            key: 'selectionKey',
+            align: 'center',
+            ...(isFixed ? { fixed: 'left' } : {}),
+            customRender: ({ record }) => record[SUMMARY_ROW_KEY],
+          });
+        }
+        return columns;
+      });
+      return { getColumns, getDataSource };
+    },
+  });
+</script>

File diff suppressed because it is too large
+ 58 - 0
src/components/TableCard/src/components/TableImg.vue


+ 53 - 0
src/components/TableCard/src/components/TableTitle.vue

@@ -0,0 +1,53 @@
+<template>
+  <BasicTitle :class="prefixCls" v-if="getTitle" :helpMessage="helpMessage">
+    {{ getTitle }}
+  </BasicTitle>
+</template>
+<script lang="ts">
+  import { computed, defineComponent, PropType } from 'vue';
+  import { BasicTitle } from '/@/components/Basic/index';
+  import { useDesign } from '/@/hooks/web/useDesign';
+  import { isFunction } from '/@/utils/is';
+
+  export default defineComponent({
+    name: 'BasicTableTitle',
+    components: { BasicTitle },
+    props: {
+      title: {
+        type: [Function, String] as PropType<string | ((data: Recordable) => string)>,
+      },
+      getSelectRows: {
+        type: Function as PropType<() => Recordable[]>,
+      },
+      helpMessage: {
+        type: [String, Array] as PropType<string | string[]>,
+      },
+    },
+    setup(props) {
+      const { prefixCls } = useDesign('basic-table-title');
+
+      const getTitle = computed(() => {
+        const { title, getSelectRows = () => {} } = props;
+        let tit = title;
+
+        if (isFunction(title)) {
+          tit = title({
+            selectRows: getSelectRows(),
+          });
+        }
+        return tit;
+      });
+
+      return { getTitle, prefixCls };
+    },
+  });
+</script>
+<style lang="less">
+  @prefix-cls: ~'@{namespace}-basic-table-title';
+
+  .@{prefix-cls} {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+  }
+</style>

+ 44 - 0
src/components/TableCard/src/components/editable/CellComponent.ts

@@ -0,0 +1,44 @@
+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;
+  getPopupContainer?: Fn;
+}
+
+export const CellComponent: FunctionalComponent = (
+  {
+    component = 'Input',
+    rule = true,
+    ruleMessage,
+    popoverVisible,
+    getPopupContainer,
+  }: 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,
+      ...(getPopupContainer ? { getPopupContainer } : {}),
+    },
+    {
+      default: () => DefaultComp,
+      content: () => ruleMessage,
+    },
+  );
+};

+ 532 - 0
src/components/TableCard/src/components/editable/EditableCell.vue

@@ -0,0 +1,532 @@
+<script lang="tsx">
+  import type { CSSProperties, PropType } from 'vue';
+  import { computed, defineComponent, nextTick, ref, toRaw, unref, watchEffect } from 'vue';
+  import type { BasicColumn } from '../../types/table';
+  import type { EditRecordRow } from './index';
+  import { CheckOutlined, CloseOutlined, FormOutlined } from '@ant-design/icons-vue';
+  import { CellComponent } from './CellComponent';
+
+  import { useDesign } from '/@/hooks/web/useDesign';
+  import { useTableContext } from '../../hooks/useTableContext';
+
+  import clickOutside from '/@/directives/clickOutside';
+
+  import { propTypes } from '/@/utils/propTypes';
+  import { isArray, isBoolean, isFunction, isNumber, isString } from '/@/utils/is';
+  import { createPlaceholderMessage } from './helper';
+  import { pick, set } from 'lodash-es';
+  import { treeToList } from '/@/utils/helper/treeHelper';
+  import { Spin } from 'ant-design-vue';
+
+  export default defineComponent({
+    name: 'EditableCell',
+    components: { FormOutlined, CloseOutlined, CheckOutlined, CellComponent, Spin },
+    directives: {
+      clickOutside,
+    },
+    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: () => ({} as any),
+      },
+      index: propTypes.number,
+    },
+    setup(props) {
+      const table = useTableContext();
+      const isEdit = ref(false);
+      const elRef = ref();
+      const ruleVisible = ref(false);
+      const ruleMessage = ref('');
+      const optionsRef = ref<LabelValueOptions>([]);
+      const currentValueRef = ref<any>(props.value);
+      const defaultValueRef = ref<any>(props.value);
+      const spinning = ref<boolean>(false);
+
+      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 isCheckValue = unref(getIsCheckComp);
+
+        const valueField = isCheckValue ? 'checked' : 'value';
+        const val = unref(currentValueRef);
+
+        const value = isCheckValue ? (isNumber(val) && isBoolean(val) ? val : !!val) : val;
+
+        let compProps = props.column?.editComponentProps ?? {};
+        const { record, column, index } = props;
+
+        if (isFunction(compProps)) {
+          compProps = compProps({ text: val, record, column, index }) ?? {};
+        }
+        const component = unref(getComponent);
+        const apiSelectProps: Recordable = {};
+        if (component === 'ApiSelect') {
+          apiSelectProps.cache = true;
+        }
+        upEditDynamicDisabled(record, column, value);
+        return {
+          size: 'small',
+          getPopupContainer: () => unref(table?.wrapRef.value) ?? document.body,
+          placeholder: createPlaceholderMessage(unref(getComponent)),
+          ...apiSelectProps,
+          ...compProps,
+          [valueField]: value,
+          disabled: unref(getDisable),
+        } as any;
+      });
+      function upEditDynamicDisabled(record, column, value) {
+        if (!record) return false;
+        const { key, dataIndex } = column;
+        if (!key && !dataIndex) return;
+        const dataKey = (dataIndex || key) as string;
+        set(record, dataKey, value);
+      }
+      const getDisable = computed(() => {
+        const { editDynamicDisabled } = props.column;
+        let disabled = false;
+        if (isBoolean(editDynamicDisabled)) {
+          disabled = editDynamicDisabled;
+        }
+        if (isFunction(editDynamicDisabled)) {
+          const { record } = props;
+          disabled = editDynamicDisabled({ record });
+        }
+        return disabled;
+      });
+      const getValues = computed(() => {
+        const { editValueMap } = props.column;
+
+        const value = unref(currentValueRef);
+
+        if (editValueMap && isFunction(editValueMap)) {
+          return editValueMap(value);
+        }
+
+        const component = unref(getComponent);
+        if (!component.includes('Select') && !component.includes('Radio')) {
+          return value;
+        }
+
+        const options: LabelValueOptions =
+          unref(getComponentProps)?.options ?? (unref(optionsRef) || []);
+        const option = options.find(item => `${item.value}` === `${value}`);
+
+        return option?.label ?? value;
+      });
+
+      const getWrapperStyle = computed((): CSSProperties => {
+        if (unref(getIsCheckComp) || unref(getRowEditable)) {
+          return {};
+        }
+        return {
+          width: 'calc(100% - 48px)',
+        };
+      });
+
+      const getWrapperClass = computed(() => {
+        const { align = 'center' } = props.column;
+        return `edit-cell-align-${align}`;
+      });
+
+      const getRowEditable = computed(() => {
+        const { editable } = props.record || {};
+        return !!editable;
+      });
+
+      watchEffect(() => {
+        // defaultValueRef.value = props.value;
+        currentValueRef.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) {
+          currentValueRef.value = e;
+        } else if (component === 'Checkbox') {
+          currentValueRef.value = (e as ChangeEvent).target.checked;
+        } else if (component === 'Switch') {
+          currentValueRef.value = e;
+        } else if (e?.target && Reflect.has(e.target, 'value')) {
+          currentValueRef.value = (e as ChangeEvent).target.value;
+        } else if (isString(e) || isBoolean(e) || isNumber(e) || isArray(e)) {
+          currentValueRef.value = e;
+        }
+        const onChange = unref(getComponentProps)?.onChange;
+        // eslint-disable-next-line prefer-rest-params
+        if (onChange && isFunction(onChange)) onChange(...arguments);
+
+        table.emit?.('edit-change', {
+          column: props.column,
+          value: unref(currentValueRef),
+          record: toRaw(props.record),
+        });
+        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);
+            ruleMessage.value = createPlaceholderMessage(component);
+            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(needEmit = true, valid = true) {
+        if (valid) {
+          const isPass = await handleSubmiRule();
+          if (!isPass) return false;
+        }
+
+        const { column, index, record } = props;
+        if (!record) return false;
+        const { key, dataIndex } = column;
+        const value = unref(currentValueRef);
+        if (!key && !dataIndex) return;
+
+        const dataKey = (dataIndex || key) as string;
+
+        if (!record.editable) {
+          const { getBindValues } = table;
+
+          const { beforeEditSubmit, columns } = unref(getBindValues);
+
+          if (beforeEditSubmit && isFunction(beforeEditSubmit)) {
+            spinning.value = true;
+            const keys: string[] = columns
+              .map(_column => _column.dataIndex)
+              .filter(field => !!field) as string[];
+            let result: any = true;
+            try {
+              result = await beforeEditSubmit({
+                record: pick(record, keys),
+                index,
+                key: dataKey as string,
+                value,
+              });
+            } catch (e) {
+              result = false;
+            } finally {
+              spinning.value = false;
+            }
+            if (result === false) {
+              return;
+            }
+          }
+        }
+
+        set(record, dataKey, value);
+        //const record = await table.updateTableData(index, dataKey, value);
+        needEmit && table.emit?.('edit-end', { record, index, key: dataKey, value });
+        isEdit.value = false;
+      }
+
+      async function handleEnter() {
+        if (props.column?.editRow) {
+          return;
+        }
+        handleSubmit();
+      }
+
+      function handleSubmitClick() {
+        handleSubmit();
+      }
+
+      function handleCancel() {
+        isEdit.value = false;
+        currentValueRef.value = defaultValueRef.value;
+        const { column, index, record } = props;
+        const { key, dataIndex } = column;
+        table.emit?.('edit-cancel', {
+          record,
+          index,
+          key: dataIndex || key,
+          value: unref(currentValueRef),
+        });
+      }
+
+      function onClickOutside() {
+        if (props.column?.editable || unref(getRowEditable)) {
+          return;
+        }
+        const component = unref(getComponent);
+
+        if (component.includes('Input')) {
+          handleCancel();
+        }
+      }
+
+      // only ApiSelect or TreeSelect
+      function handleOptionsChange(options: LabelValueOptions) {
+        const { replaceFields } = unref(getComponentProps);
+        const component = unref(getComponent);
+        if (component === 'ApiTreeSelect') {
+          const { title = 'title', value = 'value', children = 'children' } = replaceFields || {};
+          let listOptions: Recordable[] = treeToList(options, { children });
+          listOptions = listOptions.map(item => {
+            return {
+              label: item[title],
+              value: item[value],
+            };
+          });
+          optionsRef.value = listOptions as LabelValueOptions;
+        } else {
+          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);
+
+        if (props.column.dataIndex) {
+          if (!props.record.editValueRefs) props.record.editValueRefs = {};
+          props.record.editValueRefs[props.column.dataIndex as any] = currentValueRef;
+        }
+        /* 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)) {
+            if (!props.record?.onValid?.()) return;
+            const submitFns = props.record?.submitCbs || [];
+            submitFns.forEach(fn => fn(false, false));
+            table.emit?.('edit-row-end');
+            return true;
+          }
+        };
+      }
+
+      return {
+        isEdit,
+        prefixCls,
+        handleEdit,
+        currentValueRef,
+        handleSubmit,
+        handleChange,
+        handleCancel,
+        elRef,
+        getComponent,
+        getRule,
+        onClickOutside,
+        ruleMessage,
+        getRuleVisible,
+        getComponentProps,
+        handleOptionsChange,
+        getWrapperStyle,
+        getWrapperClass,
+        getRowEditable,
+        getValues,
+        handleEnter,
+        handleSubmitClick,
+        spinning,
+      };
+    },
+    render() {
+      return (
+        <div class={this.prefixCls}>
+          <div
+            v-show={!this.isEdit}
+            class={{ [`${this.prefixCls}__normal`]: true, 'ellipsis-cell': this.column.ellipsis }}
+            onClick={this.handleEdit}
+          >
+            <div class="cell-content" title={this.column.ellipsis ? this.getValues ?? '' : ''}>
+              {this.column.editRender
+                ? this.column.editRender({
+                    text: this.value,
+                    record: this.record as Recordable,
+                    column: this.column,
+                    index: this.index,
+                  })
+                : this.getValues
+                ? this.getValues
+                : '\u00A0'}
+            </div>
+            {!this.column.editRow && <FormOutlined class={`${this.prefixCls}__normal-icon`} />}
+          </div>
+          {this.isEdit && (
+            <Spin spinning={this.spinning}>
+              <div class={`${this.prefixCls}__wrapper`} v-click-outside={this.onClickOutside}>
+                <CellComponent
+                  {...this.getComponentProps}
+                  component={this.getComponent}
+                  style={this.getWrapperStyle}
+                  popoverVisible={this.getRuleVisible}
+                  rule={this.getRule}
+                  ruleMessage={this.ruleMessage}
+                  class={this.getWrapperClass}
+                  ref="elRef"
+                  onChange={this.handleChange}
+                  onOptionsChange={this.handleOptionsChange}
+                  onPressEnter={this.handleEnter}
+                />
+                {!this.getRowEditable && !this.column?.editComponentProps?.editIconHidden && (
+                  <div class={`${this.prefixCls}__action`}>
+                    <CheckOutlined
+                      class={[`${this.prefixCls}__icon`, 'mx-2']}
+                      onClick={this.handleSubmitClick}
+                    />
+                    <CloseOutlined class={`${this.prefixCls}__icon `} onClick={this.handleCancel} />
+                  </div>
+                )}
+              </div>
+            </Spin>
+          )}
+        </div>
+      );
+    },
+  });
+</script>
+<style lang="less">
+  @prefix-cls: ~'@{namespace}-editable-cell';
+
+  .edit-cell-align-left {
+    text-align: left;
+
+    input:not(.ant-calendar-picker-input, .ant-time-picker-input) {
+      text-align: left;
+    }
+  }
+
+  .edit-cell-align-center {
+    text-align: center;
+
+    input:not(.ant-calendar-picker-input, .ant-time-picker-input) {
+      text-align: center;
+    }
+  }
+
+  .edit-cell-align-right {
+    text-align: right;
+
+    input:not(.ant-calendar-picker-input, .ant-time-picker-input) {
+      text-align: right;
+    }
+  }
+
+  .edit-cell-rule-popover {
+    .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;
+
+      > .ant-select {
+        min-width: calc(100% - 50px);
+      }
+    }
+
+    &__icon {
+      &:hover {
+        transform: scale(1.2);
+
+        svg {
+          color: @primary-color;
+        }
+      }
+    }
+
+    .ellipsis-cell {
+      .cell-content {
+        overflow-wrap: break-word;
+        word-break: break-word;
+        overflow: hidden;
+        white-space: nowrap;
+        text-overflow: ellipsis;
+      }
+    }
+
+    &__normal {
+      &-icon {
+        position: absolute;
+        top: 4px;
+        right: 0;
+        display: none;
+        width: 20px;
+        cursor: pointer;
+      }
+    }
+
+    &:hover {
+      .@{prefix-cls}__normal-icon {
+        display: inline-block;
+      }
+    }
+  }
+</style>

+ 25 - 0
src/components/TableCard/src/components/editable/helper.ts

@@ -0,0 +1,25 @@
+import { ComponentType } from '../../types/componentType';
+import locales from '/@/utils/locales';
+/**
+ * @description: 生成placeholder
+ */
+export function createPlaceholderMessage(component: ComponentType) {
+  if (component.includes('Input') || component.includes('AutoComplete')) {
+    return locales.common.inputText;
+  }
+  if (component.includes('Picker')) {
+    return locales.common.chooseText;
+  }
+
+  if (
+    component.includes('Select') ||
+    component.includes('Checkbox') ||
+    component.includes('Radio') ||
+    component.includes('Switch') ||
+    component.includes('DatePicker') ||
+    component.includes('TimePicker')
+  ) {
+    return locales.common.chooseText;
+  }
+  return '';
+}

+ 68 - 0
src/components/TableCard/src/components/editable/index.ts

@@ -0,0 +1,68 @@
+import type { BasicColumn } from '/@/components/Table/src/types/table';
+
+import { h, Ref } from 'vue';
+
+import EditableCell from './EditableCell.vue';
+import { isArray } from '/@/utils/is';
+
+interface Params {
+  text: string;
+  record: Recordable;
+  index: number;
+}
+
+export function renderEditCell(column: BasicColumn) {
+  return ({ text: value, record, index }: Params) => {
+    record.onValid = async () => {
+      if (isArray(record?.validCbs)) {
+        const validFns = (record?.validCbs || []).map(fn => fn());
+        const res = await Promise.all(validFns);
+        return res.every(item => !!item);
+      } else {
+        return false;
+      }
+    };
+
+    record.onEdit = async (edit: boolean, submit = false) => {
+      if (!submit) {
+        record.editable = edit;
+      }
+
+      if (!edit && submit) {
+        if (!(await record.onValid())) return false;
+        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 = Recordable> = Partial<
+  {
+    onEdit: (editable: boolean, submit?: boolean) => Promise<boolean>;
+    onValid: () => Promise<boolean>;
+    editable: boolean;
+    onCancel: Fn;
+    onSubmit: Fn;
+    submitCbs: Fn[];
+    cancelCbs: Fn[];
+    validCbs: Fn[];
+    editValueRefs: Recordable<Ref>;
+  } & T
+>;

+ 503 - 0
src/components/TableCard/src/components/settings/ColumnSetting.vue

@@ -0,0 +1,503 @@
+<template>
+  <Tooltip placement="top">
+    <template #title>
+      <span>{{ locales.table.settingColumn }}</span>
+    </template>
+    <Popover
+      placement="bottomLeft"
+      trigger="click"
+      @visible-change="handleVisibleChange"
+      :overlayClassName="`${prefixCls}__cloumn-list`"
+      :getPopupContainer="getPopupContainer"
+    >
+      <template #title>
+        <div :class="`${prefixCls}__popover-title`">
+          <Checkbox
+            :indeterminate="indeterminate"
+            v-model:checked="checkAll"
+            @change="onCheckAllChange"
+          >
+            {{ locales.table.settingColumnShow }}
+          </Checkbox>
+
+          <Checkbox v-model:checked="checkIndex" @change="handleIndexCheckChange">
+            {{ locales.table.settingIndexColumnShow }}
+          </Checkbox>
+
+          <Checkbox
+            v-model:checked="checkSelect"
+            @change="handleSelectCheckChange"
+            :disabled="!defaultRowSelection"
+          >
+            {{ locales.table.settingSelectColumnShow }}
+          </Checkbox>
+
+          <a-button size="small" type="link" @click="reset">
+            {{ locales.common.resetText }}
+          </a-button>
+        </div>
+      </template>
+
+      <template #content>
+        <ScrollContainer>
+          <CheckboxGroup v-model:value="checkedList" @change="onChange" ref="columnListRef">
+            <template v-for="item in plainOptions" :key="item.value">
+              <div :class="`${prefixCls}__check-item`" v-if="!('ifShow' in item && !item.ifShow)">
+                <DragOutlined class="table-column-drag-icon" />
+                <Checkbox :value="item.value">
+                  {{ item.label }}
+                </Checkbox>
+
+                <Tooltip
+                  placement="bottomLeft"
+                  :mouseLeaveDelay="0.4"
+                  :getPopupContainer="getPopupContainer"
+                >
+                  <template #title>
+                    {{ locales.table.settingFixedLeft }}
+                  </template>
+                  <Icon
+                    icon="icon-left|iconfont"
+                    :class="[
+                      `${prefixCls}__fixed-left`,
+                      {
+                        active: item.fixed === 'left',
+                        disabled: !checkedList.includes(item.value),
+                      },
+                    ]"
+                    @click="handleColumnFixed(item, 'left')"
+                  />
+                </Tooltip>
+                <Divider type="vertical" />
+                <Tooltip
+                  placement="bottomLeft"
+                  :mouseLeaveDelay="0.4"
+                  :getPopupContainer="getPopupContainer"
+                >
+                  <template #title>
+                    {{ locales.table.settingFixedRight }}
+                  </template>
+                  <Icon
+                    icon="icon-left|iconfont"
+                    :class="[
+                      `${prefixCls}__fixed-right`,
+                      {
+                        active: item.fixed === 'right',
+                        disabled: !checkedList.includes(item.value),
+                      },
+                    ]"
+                    @click="handleColumnFixed(item, 'right')"
+                  />
+                </Tooltip>
+              </div>
+            </template>
+          </CheckboxGroup>
+        </ScrollContainer>
+      </template>
+      <SettingOutlined />
+    </Popover>
+  </Tooltip>
+</template>
+<script lang="ts">
+  import type { BasicColumn, ColumnChangeParam } from '../../types/table';
+  import {
+    defineComponent,
+    ref,
+    reactive,
+    toRefs,
+    watchEffect,
+    nextTick,
+    unref,
+    computed,
+  } from 'vue';
+  import { Tooltip, Popover, Checkbox, Divider } from 'ant-design-vue';
+  import type { CheckboxChangeEvent } from 'ant-design-vue/lib/checkbox/interface';
+  import { SettingOutlined, DragOutlined } from '@ant-design/icons-vue';
+  import { Icon } from '/@/components/Icon';
+  import { ScrollContainer } from '/@/components/Container';
+  import { useTableContext } from '../../hooks/useTableContext';
+  import { useDesign } from '/@/hooks/web/useDesign';
+  // import { useSortable } from '/@/hooks/web/useSortable';
+  import { isFunction, isNullAndUnDef } from '/@/utils/is';
+  import { getPopupContainer as getParentContainer } from '/@/utils';
+  import { cloneDeep, omit } from 'lodash-es';
+  import Sortablejs from 'sortablejs';
+  import type Sortable from 'sortablejs';
+  import locales from '/@/utils/locales';
+  import { STORAGE_KEY } from '../../const';
+
+  interface State {
+    checkAll: boolean;
+    isInit?: boolean;
+    checkedList: string[];
+    defaultCheckList: string[];
+  }
+
+  interface Options {
+    label: string;
+    value: string;
+    fixed?: boolean | 'left' | 'right';
+  }
+
+  export default defineComponent({
+    name: 'ColumnSetting',
+    components: {
+      SettingOutlined,
+      Popover,
+      Tooltip,
+      Checkbox,
+      CheckboxGroup: Checkbox.Group,
+      DragOutlined,
+      ScrollContainer,
+      Divider,
+      Icon,
+    },
+    emits: ['columns-change'],
+
+    setup(_, { emit, attrs }) {
+      const table = useTableContext();
+
+      const defaultRowSelection = omit(table.getRowSelection(), 'selectedRowKeys');
+      let inited = false;
+
+      const tableId = table.getBindValues.value.id || '';
+
+      const storage = table.getBindValues.value.storage || false;
+
+      const cachePlainOptions = ref<Options[]>([]);
+      const plainOptions = ref<Options[] | any>([]);
+
+      const plainSortOptions = ref<Options[]>([]);
+
+      const columnListRef = ref<ComponentRef>(null);
+
+      const state = reactive<State>({
+        checkAll: true,
+        checkedList: [],
+        defaultCheckList: [],
+      });
+
+      const checkIndex = ref(false);
+      const checkSelect = ref(false);
+
+      const { prefixCls } = useDesign('basic-column-setting');
+
+      const getValues = computed(() => {
+        return unref(table?.getBindValues) || {};
+      });
+
+      watchEffect(() => {
+        setTimeout(() => {
+          const columns = table.getColumns();
+          if (columns.length && !state.isInit) {
+            init();
+          }
+        }, 0);
+      });
+
+      watchEffect(() => {
+        const values = unref(getValues);
+        checkIndex.value = !!values.showIndexColumn;
+        checkSelect.value = !!values.rowSelection;
+      });
+
+      function getColumns() {
+        const ret: Options[] = [];
+        // 如果有 id 的话进行存储
+        if (tableId && storage) {
+          const storageData = window.localStorage.getItem(STORAGE_KEY) || '';
+          if (storageData != '' && storageData.length) {
+            const formmatData = JSON.parse(storageData)?.[tableId] ?? [];
+            formmatData.forEach(item => {
+              ret.push({
+                label: (item.title as string) || (item.customTitle as string),
+                value: (item.dataIndex || item.title) as string,
+                ...item,
+              });
+            });
+            return ret;
+          }
+        }
+        table.getColumns({ ignoreIndex: true, ignoreAction: true }).forEach(item => {
+          ret.push({
+            label: (item.title as string) || (item.customTitle as string),
+            value: (item.dataIndex || item.title) as string,
+            ...item,
+          });
+        });
+
+        return ret;
+      }
+
+      function init() {
+        const columns = getColumns();
+
+        const checkList = table
+          .getColumns({ ignoreAction: true, ignoreIndex: true })
+          .map(item => {
+            if (item.defaultHidden) {
+              return '';
+            }
+            return item.dataIndex || item.title;
+          })
+          .filter(Boolean) as string[];
+
+        if (!plainOptions.value.length) {
+          plainOptions.value = columns;
+          plainSortOptions.value = columns;
+          cachePlainOptions.value = columns;
+          state.defaultCheckList = checkList;
+        } else {
+          // const fixedColumns = columns.filter((item) =>
+          //   Reflect.has(item, 'fixed')
+          // ) as BasicColumn[];
+
+          unref(plainOptions).forEach((item: BasicColumn) => {
+            const findItem = columns.find((col: BasicColumn) => col.dataIndex === item.dataIndex);
+            if (findItem) {
+              item.fixed = findItem.fixed;
+            }
+          });
+        }
+        state.isInit = true;
+        state.checkedList = checkList;
+      }
+
+      // checkAll change
+      function onCheckAllChange(e: CheckboxChangeEvent) {
+        const checkList = plainOptions.value.map(item => item.value);
+        if (e.target.checked) {
+          state.checkedList = checkList;
+          setColumns(checkList);
+        } else {
+          state.checkedList = [];
+          setColumns([]);
+        }
+      }
+
+      const indeterminate = computed(() => {
+        const len = plainOptions.value.length;
+        const checkedLen = state.checkedList.length;
+        // unref(checkIndex) && checkedLen--;
+        return checkedLen > 0 && checkedLen < len;
+      });
+
+      // Trigger when check/uncheck a column
+      function onChange(checkedList: string[]) {
+        const len = plainSortOptions.value.length;
+        state.checkAll = checkedList.length === len;
+        const sortList = unref(plainSortOptions).map(item => item.value);
+        checkedList.sort((prev, next) => {
+          return sortList.indexOf(prev) - sortList.indexOf(next);
+        });
+        setColumns(checkedList);
+      }
+
+      let sortable: Sortable;
+      let sortableOrder: string[] = [];
+      // reset columns
+      function reset() {
+        state.checkedList = [...state.defaultCheckList];
+        state.checkAll = true;
+        plainOptions.value = unref(cachePlainOptions);
+        plainSortOptions.value = unref(cachePlainOptions);
+        setColumns(table.getCacheColumns());
+        sortable.sort(sortableOrder);
+      }
+
+      // Open the pop-up window for drag and drop initialization
+      function handleVisibleChange() {
+        if (inited) return;
+        nextTick(() => {
+          const columnListEl = unref(columnListRef);
+          if (!columnListEl) return;
+          const el = columnListEl.$el as any;
+          if (!el) return;
+          // Drag and drop sort
+          sortable = Sortablejs.create(unref(el), {
+            animation: 500,
+            delay: 400,
+            delayOnTouchOnly: true,
+            handle: '.table-column-drag-icon ',
+            onEnd: evt => {
+              const { oldIndex, newIndex } = evt;
+              if (isNullAndUnDef(oldIndex) || isNullAndUnDef(newIndex) || oldIndex === newIndex) {
+                return;
+              }
+              // Sort column
+              const columns = cloneDeep(plainSortOptions.value);
+
+              if (oldIndex > newIndex) {
+                columns.splice(newIndex, 0, columns[oldIndex]);
+                columns.splice(oldIndex + 1, 1);
+              } else {
+                columns.splice(newIndex + 1, 0, columns[oldIndex]);
+                columns.splice(oldIndex, 1);
+              }
+
+              plainSortOptions.value = columns;
+              console.log(
+                '🚀 ~ file: ColumnSetting.vue:339 ~ sortable=Sortablejs.create ~ columns:',
+                columns,
+              );
+
+              setColumns(columns.filter((item: any) => state.checkedList.includes(item.value)));
+            },
+          });
+          // 记录原始order 序列
+          sortableOrder = sortable.toArray();
+          inited = true;
+        });
+      }
+
+      // Control whether the serial number column is displayed
+      function handleIndexCheckChange(e: CheckboxChangeEvent) {
+        table.setProps({
+          showIndexColumn: e.target.checked,
+        });
+      }
+
+      // Control whether the check box is displayed
+      function handleSelectCheckChange(e: CheckboxChangeEvent) {
+        table.setProps({
+          rowSelection: e.target.checked ? defaultRowSelection : undefined,
+        });
+      }
+
+      function handleColumnFixed(item: BasicColumn, fixed?: 'left' | 'right') {
+        if (!state.checkedList.includes(item.dataIndex as string)) return;
+
+        const columns = getColumns() as BasicColumn[];
+        const isFixed = item.fixed === fixed ? false : fixed;
+        const index = columns.findIndex(col => col.dataIndex === item.dataIndex);
+        if (index !== -1) {
+          columns[index].fixed = isFixed;
+        }
+        item.fixed = isFixed;
+
+        if (isFixed && !item.width) {
+          item.width = 100;
+        }
+        table.setCacheColumnsByField?.(item.dataIndex as string, { fixed: isFixed });
+        setColumns(columns);
+      }
+
+      function setColumns(columns: BasicColumn[] | string[]) {
+        console.log('🚀 ~ file: ColumnSetting.vue:386 ~ setColumns ~ columns:', columns);
+        table.setColumns(columns);
+        const data: ColumnChangeParam[] = unref(plainSortOptions).map(col => {
+          const visible =
+            columns.findIndex(
+              (c: BasicColumn | string) =>
+                c === col.value || (typeof c !== 'string' && c.dataIndex === col.value),
+            ) !== -1;
+          return { dataIndex: col.value, fixed: col.fixed, visible };
+        });
+
+        emit('columns-change', data);
+      }
+
+      function getPopupContainer() {
+        return isFunction(attrs.getPopupContainer)
+          ? attrs.getPopupContainer()
+          : getParentContainer();
+      }
+
+      return {
+        locales,
+        ...toRefs(state),
+        indeterminate,
+        onCheckAllChange,
+        onChange,
+        plainOptions,
+        reset,
+        prefixCls,
+        columnListRef,
+        handleVisibleChange,
+        checkIndex,
+        checkSelect,
+        handleIndexCheckChange,
+        handleSelectCheckChange,
+        defaultRowSelection,
+        handleColumnFixed,
+        getPopupContainer,
+      };
+    },
+  });
+</script>
+<style lang="less">
+  @prefix-cls: ~'@{namespace}-basic-column-setting';
+
+  .table-column-drag-icon {
+    margin: 0 5px;
+    cursor: move;
+  }
+
+  .@{prefix-cls} {
+    &__popover-title {
+      position: relative;
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+    }
+
+    &__check-item {
+      display: flex;
+      align-items: center;
+      min-width: 100%;
+      padding: 4px 16px 8px 0;
+
+      .ant-checkbox-wrapper {
+        width: 100%;
+
+        &:hover {
+          color: @primary-color;
+        }
+      }
+    }
+
+    &__fixed-left,
+    &__fixed-right {
+      color: rgb(0 0 0 / 45%);
+      cursor: pointer;
+
+      &.active,
+      &:hover {
+        color: @primary-color;
+      }
+
+      &.disabled {
+        color: @disabled-color;
+        cursor: not-allowed;
+      }
+    }
+
+    &__fixed-right {
+      transform: rotate(180deg);
+    }
+
+    &__cloumn-list {
+      svg {
+        width: 1em !important;
+        height: 1em !important;
+      }
+
+      .ant-popover-inner-content {
+        // max-height: 360px;
+        padding-right: 0;
+        padding-left: 0;
+        // overflow: auto;
+      }
+
+      .ant-checkbox-group {
+        width: 100%;
+        min-width: 260px;
+        // flex-wrap: wrap;
+      }
+
+      .scrollbar {
+        height: 220px;
+      }
+    }
+  }
+</style>

+ 36 - 0
src/components/TableCard/src/components/settings/FullScreenSetting.vue

@@ -0,0 +1,36 @@
+<template>
+  <Tooltip placement="top">
+    <template #title>
+      <span>{{ locales.table.settingFullScreen }}</span>
+    </template>
+    <FullscreenOutlined @click="toggle" v-if="!isFullscreen" />
+    <FullscreenExitOutlined @click="toggle" v-else />
+  </Tooltip>
+</template>
+<script lang="ts">
+  import { defineComponent } from 'vue';
+  import { Tooltip } from 'ant-design-vue';
+  import { FullscreenOutlined, FullscreenExitOutlined } from '@ant-design/icons-vue';
+  import { useFullscreen } from '@vueuse/core';
+  import { useTableContext } from '../../hooks/useTableContext';
+  import locales from '/@/utils/locales';
+  export default defineComponent({
+    name: 'FullScreenSetting',
+    components: {
+      FullscreenExitOutlined,
+      FullscreenOutlined,
+      Tooltip,
+    },
+
+    setup() {
+      const table = useTableContext();
+      const { toggle, isFullscreen } = useFullscreen(table.wrapRef);
+
+      return {
+        toggle,
+        isFullscreen,
+        locales,
+      };
+    },
+  });
+</script>

+ 32 - 0
src/components/TableCard/src/components/settings/RedoSetting.vue

@@ -0,0 +1,32 @@
+<template>
+  <Tooltip placement="top">
+    <template #title>
+      <span>{{ locales.common.redo }}</span>
+    </template>
+    <RedoOutlined @click="redo" />
+  </Tooltip>
+</template>
+<script lang="ts">
+  import { defineComponent } from 'vue';
+  import { Tooltip } from 'ant-design-vue';
+  import { RedoOutlined } from '@ant-design/icons-vue';
+  import { useTableContext } from '../../hooks/useTableContext';
+  import locales from '/@/utils/locales';
+
+  export default defineComponent({
+    name: 'RedoSetting',
+    components: {
+      RedoOutlined,
+      Tooltip,
+    },
+    setup() {
+      const table = useTableContext();
+
+      function redo() {
+        table.reload();
+      }
+
+      return { redo, locales };
+    },
+  });
+</script>

+ 62 - 0
src/components/TableCard/src/components/settings/SizeSetting.vue

@@ -0,0 +1,62 @@
+<template>
+  <Tooltip placement="top">
+    <template #title>
+      <span>{{ locales.table.settingDens }}</span>
+    </template>
+
+    <Dropdown placement="bottom" :trigger="['click']" :getPopupContainer="getPopupContainer">
+      <ColumnHeightOutlined />
+      <template #overlay>
+        <Menu @click="handleTitleClick" selectable v-model:selectedKeys="selectedKeysRef">
+          <MenuItem key="default">
+            <span>{{ locales.table.settingDensDefault }}</span>
+          </MenuItem>
+          <MenuItem key="middle">
+            <span>{{ locales.table.settingDensMiddle }}</span>
+          </MenuItem>
+          <MenuItem key="small">
+            <span>{{ locales.table.settingDensSmall }}</span>
+          </MenuItem>
+        </Menu>
+      </template>
+    </Dropdown>
+  </Tooltip>
+</template>
+<script lang="ts">
+  import type { SizeType } from '../../types/table';
+  import { defineComponent, ref } from 'vue';
+  import { Tooltip, Dropdown } from 'ant-design-vue';
+  import { ColumnHeightOutlined } from '@ant-design/icons-vue';
+  import { useTableContext } from '../../hooks/useTableContext';
+  import { getPopupContainer } from '/@/utils';
+  import locales from '/@/utils/locales';
+
+  export default defineComponent({
+    name: 'SizeSetting',
+    components: {
+      ColumnHeightOutlined,
+      Tooltip,
+      Dropdown,
+      MenuItem: Menu.Item,
+    },
+    setup() {
+      const table = useTableContext();
+
+      const selectedKeysRef = ref<SizeType[]>([table.getSize()]);
+
+      function handleTitleClick({ key }: { key: SizeType }) {
+        selectedKeysRef.value = [key];
+        table.setProps({
+          size: key,
+        });
+      }
+
+      return {
+        handleTitleClick,
+        selectedKeysRef,
+        getPopupContainer,
+        locales,
+      };
+    },
+  });
+</script>

+ 74 - 0
src/components/TableCard/src/components/settings/index.vue

@@ -0,0 +1,74 @@
+<template>
+  <div class="table-settings">
+    <RedoSetting v-if="getSetting.redo" :getPopupContainer="getTableContainer" />
+    <SizeSetting v-if="getSetting.size" :getPopupContainer="getTableContainer" />
+    <ColumnSetting
+      v-if="getSetting.setting"
+      @columns-change="handleColumnChange"
+      :getPopupContainer="getTableContainer"
+    />
+    <FullScreenSetting v-if="getSetting.fullScreen" :getPopupContainer="getTableContainer" />
+  </div>
+</template>
+<script lang="ts">
+  import type { PropType } from 'vue';
+  import type { TableSetting, ColumnChangeParam } from '../../types/table';
+  import { defineComponent, computed, unref } from 'vue';
+  import ColumnSetting from './ColumnSetting.vue';
+  import SizeSetting from './SizeSetting.vue';
+  import RedoSetting from './RedoSetting.vue';
+  import FullScreenSetting from './FullScreenSetting.vue';
+  import { useTableContext } from '../../hooks/useTableContext';
+
+  export default defineComponent({
+    name: 'TableSetting',
+    components: {
+      ColumnSetting,
+      SizeSetting,
+      RedoSetting,
+      FullScreenSetting,
+    },
+    props: {
+      setting: {
+        type: Object as PropType<TableSetting>,
+        default: () => ({}),
+      },
+    },
+    emits: ['columns-change'],
+    setup(props, { emit }) {
+      const table = useTableContext();
+
+      const getSetting = computed((): TableSetting => {
+        return {
+          redo: true,
+          size: false,
+          setting: true,
+          fullScreen: false,
+          ...props.setting,
+        };
+      });
+
+      function handleColumnChange(data: ColumnChangeParam[]) {
+        emit('columns-change', data);
+      }
+
+      function getTableContainer() {
+        return table ? unref(table.wrapRef) : document.body;
+      }
+
+      return { getSetting, handleColumnChange, getTableContainer };
+    },
+  });
+</script>
+<style lang="less">
+  .table-settings {
+    & > * {
+      margin-right: 12px;
+    }
+
+    svg {
+      width: 1.3em;
+      height: 1.3em;
+    }
+  }
+</style>

+ 42 - 0
src/components/TableCard/src/const.ts

@@ -0,0 +1,42 @@
+import componentSetting from '/@/settings/componentSetting';
+
+const { table } = componentSetting;
+
+const {
+  pageSizeOptions,
+  defaultPageSize,
+  fetchSetting,
+  defaultSize,
+  defaultSortFn,
+  defaultFilterFn,
+} = table;
+
+export const ROW_KEY = 'key';
+
+// Optional display number per page;
+export const PAGE_SIZE_OPTIONS = pageSizeOptions;
+
+// Number of items displayed per page
+export const PAGE_SIZE = defaultPageSize;
+
+// Common interface field settings
+export const FETCH_SETTING = fetchSetting;
+
+// Default Size
+export const DEFAULT_SIZE = defaultSize;
+
+// Configure general sort function
+export const DEFAULT_SORT_FN = defaultSortFn;
+
+export const DEFAULT_FILTER_FN = defaultFilterFn;
+
+//  Default layout of table cells
+export const DEFAULT_ALIGN = 'center';
+
+export const INDEX_COLUMN_FLAG = 'INDEX';
+
+export const ACTION_COLUMN_FLAG = 'ACTION';
+
+export const STORAGE_KEY = 'TABLE_CUSTOM_STORAGE';
+
+export const SEARCH_KEY = 'TABLE_ADVANCE_SEARCH';

+ 349 - 0
src/components/TableCard/src/hooks/useColumns.ts

@@ -0,0 +1,349 @@
+import type { BasicColumn, BasicTableProps, CellFormat, GetColumnsParams } from '../types/table';
+import type { PaginationProps } from '../types/pagination';
+import type { ComputedRef } from 'vue';
+import { computed, Ref, ref, reactive, toRaw, unref, watch } from 'vue';
+import { renderEditCell } from '../components/editable';
+import { usePermission } from '/@/hooks/web/usePermission';
+import { isArray, isBoolean, isFunction, isMap, isString } from '/@/utils/is';
+import { cloneDeep, isEqual } from 'lodash-es';
+import { formatToDate } from '/@/utils/dateUtil';
+import {
+  ACTION_COLUMN_FLAG,
+  DEFAULT_ALIGN,
+  INDEX_COLUMN_FLAG,
+  PAGE_SIZE,
+  STORAGE_KEY,
+} from '../const';
+import locales from '/@/utils/locales';
+
+function handleItem(item: BasicColumn, ellipsis: boolean) {
+  const { key, dataIndex, children } = item;
+  item.align = item.align || DEFAULT_ALIGN;
+  if (ellipsis) {
+    if (!key) {
+      item.key = dataIndex;
+    }
+    if (!isBoolean(item.ellipsis)) {
+      Object.assign(item, {
+        ellipsis,
+      });
+    }
+  }
+  if (children && children.length) {
+    handleChildren(children, !!ellipsis);
+  }
+}
+
+function handleChildren(children: BasicColumn[] | undefined, ellipsis: boolean) {
+  if (!children) return;
+  children.forEach(item => {
+    const { children } = item;
+    handleItem(item, ellipsis);
+    handleChildren(children, ellipsis);
+  });
+}
+
+function handleIndexColumn(
+  propsRef: ComputedRef<BasicTableProps>,
+  getPaginationRef: ComputedRef<boolean | PaginationProps>,
+  columns: BasicColumn[],
+) {
+  const { showIndexColumn, indexColumnProps, isTreeTable } = unref(propsRef);
+
+  let pushIndexColumns = false;
+  if (unref(isTreeTable)) {
+    return;
+  }
+  columns.forEach(() => {
+    const indIndex = columns.findIndex(column => column.flag === INDEX_COLUMN_FLAG);
+    if (showIndexColumn) {
+      pushIndexColumns = indIndex === -1;
+    } else if (!showIndexColumn && indIndex !== -1) {
+      columns.splice(indIndex, 1);
+    }
+  });
+
+  if (!pushIndexColumns) return;
+
+  const isFixedLeft = columns.some(item => item.fixed === 'left');
+
+  columns.unshift({
+    flag: INDEX_COLUMN_FLAG,
+    width: 50,
+    title: locales.table.index,
+    align: 'center',
+    customRender: ({ index }) => {
+      const getPagination = unref(getPaginationRef);
+      if (isBoolean(getPagination)) {
+        return `${index + 1}`;
+      }
+      const { current = 1, pageSize = PAGE_SIZE } = getPagination;
+      return ((current < 1 ? 1 : current) - 1) * pageSize + index + 1;
+    },
+    ...(isFixedLeft
+      ? {
+          fixed: 'left',
+        }
+      : {}),
+    ...indexColumnProps,
+  });
+}
+
+function handleActionColumn(propsRef: ComputedRef<BasicTableProps>, columns: BasicColumn[]) {
+  const { actionColumn } = unref(propsRef);
+  if (!actionColumn) return;
+
+  const hasIndex = columns.findIndex(column => column.flag === ACTION_COLUMN_FLAG);
+  if (hasIndex === -1) {
+    columns.push({
+      ...columns[hasIndex],
+      fixed: 'right',
+      ...actionColumn,
+      flag: ACTION_COLUMN_FLAG,
+    });
+  }
+}
+
+export function useColumns(
+  propsRef: ComputedRef<BasicTableProps>,
+  getPaginationRef: ComputedRef<boolean | PaginationProps>,
+) {
+  const columnsRef = ref(unref(propsRef).columns) as unknown as Ref<BasicColumn[]>;
+  let cacheColumns = unref(propsRef).columns;
+
+  const getColumnsRef = computed(() => {
+    const columns = cloneDeep(unref(columnsRef));
+
+    handleIndexColumn(propsRef, getPaginationRef, columns);
+    handleActionColumn(propsRef, columns);
+
+    if (!columns) {
+      return [];
+    }
+    const { ellipsis } = unref(propsRef);
+
+    columns.forEach(item => {
+      const { customRender, slots } = item;
+
+      handleItem(
+        item,
+        Reflect.has(item, 'ellipsis') ? !!item.ellipsis : !!ellipsis && !customRender && !slots,
+      );
+    });
+    return columns;
+  });
+
+  function isIfShow(column: BasicColumn): boolean {
+    const ifShow = column.ifShow;
+
+    let isIfShow = true;
+
+    if (isBoolean(ifShow)) {
+      isIfShow = ifShow;
+    }
+    if (isFunction(ifShow)) {
+      isIfShow = ifShow(column);
+    }
+    return isIfShow;
+  }
+  const { hasPermission } = usePermission();
+
+  const getViewColumns = computed(() => {
+    const viewColumns = sortFixedColumn(unref(getColumnsRef));
+
+    const columns = cloneDeep(viewColumns);
+    return columns
+      .filter(column => {
+        return hasPermission(column.auth) && isIfShow(column);
+      })
+      .map(column => {
+        const { slots, 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 reactive(column);
+      });
+  });
+
+  watch(
+    () => unref(propsRef).columns,
+    columns => {
+      // 在这里 获取 sorage 里面存储的 表头
+      // storage 存储 并且有 table_id, 重新排序对比输出
+      if (unref(propsRef)?.id && unref(propsRef)?.storage) {
+        const storageData = window.localStorage.getItem(STORAGE_KEY) || '';
+        if (storageData != '') {
+          const formmatData = JSON.parse(storageData);
+          columns = formmatData[unref(propsRef)?.id];
+        }
+      }
+      columnsRef.value = columns;
+      cacheColumns = columns?.filter(item => !item.flag) ?? [];
+    },
+  );
+
+  function setCacheColumnsByField(dataIndex: string | undefined, value: Partial<BasicColumn>) {
+    if (!dataIndex || !value) {
+      return;
+    }
+    cacheColumns.forEach(item => {
+      if (item.dataIndex === dataIndex) {
+        Object.assign(item, value);
+        return;
+      }
+    });
+  }
+
+  function setStorageData(columns: any) {
+    // 设置存储表头数据
+    if (unref(propsRef)?.id && unref(propsRef)?.storage) {
+      const storageData = window.localStorage.getItem(STORAGE_KEY) || '';
+      let origin = {};
+      if (storageData != '') {
+        origin = JSON.parse(storageData);
+      }
+      window.localStorage.setItem(
+        STORAGE_KEY,
+        JSON.stringify({ ...origin, [unref(propsRef)?.id]: columns }),
+      );
+    }
+  }
+  /**
+   * set columns 设置表头数据
+   * @param columnList key|column
+   */
+  function setColumns(columnList: Partial<BasicColumn>[] | (string | string[])[]) {
+    const columns = cloneDeep(columnList);
+    if (!isArray(columns)) return;
+
+    if (columns.length <= 0) {
+      columnsRef.value = [];
+      return;
+    }
+
+    const firstColumn = columns[0];
+
+    const cacheKeys = cacheColumns.map(item => item.dataIndex);
+
+    if (!isString(firstColumn) && !isArray(firstColumn)) {
+      columnsRef.value = columns as BasicColumn[];
+      setStorageData(columns);
+    } else {
+      const columnKeys = (columns as (string | string[])[]).map(m => m.toString());
+      const newColumns: BasicColumn[] = [];
+      cacheColumns.forEach(item => {
+        newColumns.push({
+          ...item,
+          defaultHidden: !columnKeys.includes(item.dataIndex?.toString() || (item.key as string)),
+        });
+      });
+      // Sort according to another array
+      if (!isEqual(cacheKeys, columns)) {
+        newColumns.sort((prev, next) => {
+          return (
+            columnKeys.indexOf(prev.dataIndex?.toString() as string) -
+            columnKeys.indexOf(next.dataIndex?.toString() as string)
+          );
+        });
+      }
+      columnsRef.value = newColumns;
+      // 设置存储表头数据
+      setStorageData(newColumns);
+    }
+  }
+
+  function getColumns(opt?: GetColumnsParams) {
+    const { ignoreIndex, ignoreAction, sort } = opt || {};
+    let columns = toRaw(unref(getColumnsRef));
+    if (ignoreIndex) {
+      columns = columns.filter(item => item.flag !== INDEX_COLUMN_FLAG);
+    }
+    if (ignoreAction) {
+      columns = columns.filter(item => item.flag !== ACTION_COLUMN_FLAG);
+    }
+
+    if (sort) {
+      columns = sortFixedColumn(columns);
+    }
+
+    return columns;
+  }
+  function getCacheColumns() {
+    return cacheColumns;
+  }
+
+  return {
+    getColumnsRef,
+    getCacheColumns,
+    getColumns,
+    setColumns,
+    getViewColumns,
+    setCacheColumnsByField,
+  };
+}
+
+function sortFixedColumn(columns: BasicColumn[]) {
+  const fixedLeftColumns: BasicColumn[] = [];
+  const fixedRightColumns: BasicColumn[] = [];
+  const defColumns: BasicColumn[] = [];
+  for (const column of columns) {
+    if (column.fixed === 'left') {
+      fixedLeftColumns.push(column);
+      continue;
+    }
+    if (column.fixed === 'right') {
+      fixedRightColumns.push(column);
+      continue;
+    }
+    defColumns.push(column);
+  }
+  return [...fixedLeftColumns, ...defColumns, ...fixedRightColumns].filter(
+    item => !item.defaultHidden,
+  );
+}
+
+// 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) && text) {
+      const dateFormat = format.replace(DATE_FORMAT_PREFIX, '');
+
+      if (!dateFormat) {
+        return text;
+      }
+      return formatToDate(text, dateFormat);
+    }
+
+    // Map
+    if (isMap(format)) {
+      return format.get(text);
+    }
+  } catch (error) {
+    return text;
+  }
+}

+ 100 - 0
src/components/TableCard/src/hooks/useCustomRow.ts

@@ -0,0 +1,100 @@
+import type { ComputedRef } from 'vue';
+import type { BasicTableProps } from '../types/table';
+import { unref } from 'vue';
+import { ROW_KEY } from '../const';
+import { isString, isFunction } from '/@/utils/is';
+
+interface Options {
+  setSelectedRowKeys: (keys: string[]) => void;
+  getSelectRowKeys: () => string[];
+  clearSelectedRowKeys: () => void;
+  emit: EmitType;
+  getAutoCreateKey: ComputedRef<boolean | undefined>;
+}
+
+function getKey(
+  record: Recordable,
+  rowKey: string | ((record: Record<string, any>) => string) | undefined,
+  autoCreateKey?: boolean,
+) {
+  if (!rowKey || autoCreateKey) {
+    return record[ROW_KEY];
+  }
+  if (isString(rowKey)) {
+    return record[rowKey];
+  }
+  if (isFunction(rowKey)) {
+    return record[rowKey(record)];
+  }
+  return null;
+}
+
+export function useCustomRow(
+  propsRef: ComputedRef<BasicTableProps>,
+  { setSelectedRowKeys, getSelectRowKeys, getAutoCreateKey, clearSelectedRowKeys, emit }: Options,
+) {
+  const customRow = (record: Recordable, index: number) => {
+    return {
+      onClick: (e: Event) => {
+        e?.stopPropagation();
+        function handleClick() {
+          const { rowSelection, rowKey, clickToRowSelect } = unref(propsRef);
+          if (!rowSelection || !clickToRowSelect) return;
+          const keys = getSelectRowKeys() || [];
+          const key = getKey(record, rowKey, unref(getAutoCreateKey));
+          if (!key) return;
+
+          const isCheckbox = rowSelection.type === 'checkbox';
+          if (isCheckbox) {
+            // 找到tr
+            const tr: HTMLElement = (e as MouseEvent)
+              .composedPath?.()
+              .find((dom: HTMLElement) => dom.tagName === 'TR') as HTMLElement;
+            if (!tr) return;
+            // 找到Checkbox,检查是否为disabled
+            const checkBox = tr.querySelector('input[type=checkbox]');
+            if (!checkBox || checkBox.hasAttribute('disabled')) return;
+            if (!keys.includes(key)) {
+              setSelectedRowKeys([...keys, key]);
+              return;
+            }
+            const keyIndex = keys.findIndex(item => item === key);
+            keys.splice(keyIndex, 1);
+            setSelectedRowKeys(keys);
+            return;
+          }
+
+          const isRadio = rowSelection.type === 'radio';
+          if (isRadio) {
+            if (!keys.includes(key)) {
+              if (keys.length) {
+                clearSelectedRowKeys();
+              }
+              setSelectedRowKeys([key]);
+              return;
+            }
+            clearSelectedRowKeys();
+          }
+        }
+        handleClick();
+        emit('row-click', record, index, e);
+      },
+      onDblclick: (event: Event) => {
+        emit('row-dbClick', record, index, event);
+      },
+      onContextmenu: (event: Event) => {
+        emit('row-contextmenu', record, index, event);
+      },
+      onMouseenter: (event: Event) => {
+        emit('row-mouseenter', record, index, event);
+      },
+      onMouseleave: (event: Event) => {
+        emit('row-mouseleave', record, index, event);
+      },
+    };
+  };
+
+  return {
+    customRow,
+  };
+}

+ 374 - 0
src/components/TableCard/src/hooks/useDataSource.ts

@@ -0,0 +1,374 @@
+import type { BasicTableProps, FetchParams, SorterResult } from '../types/table';
+import type { PaginationProps } from '../types/pagination';
+import {
+  ref,
+  unref,
+  ComputedRef,
+  computed,
+  onMounted,
+  watch,
+  reactive,
+  Ref,
+  watchEffect,
+} from 'vue';
+import { useTimeoutFn } from '/@/hooks/core/useTimeout';
+import { buildUUID } from '/@/utils/uuid';
+import { isFunction, isBoolean } from '/@/utils/is';
+import { get, cloneDeep, merge } from 'lodash-es';
+import { FETCH_SETTING, ROW_KEY, PAGE_SIZE } from '../const';
+
+interface ActionType {
+  getPaginationInfo: ComputedRef<boolean | PaginationProps>;
+  setPagination: (info: Partial<PaginationProps>) => void;
+  setLoading: (loading: boolean) => void;
+  getFieldsValue: () => Recordable;
+  clearSelectedRowKeys: () => void;
+  tableData: Ref<Recordable[]>;
+}
+
+interface SearchState {
+  sortInfo: Recordable;
+  filterInfo: Record<string, string[]>;
+}
+export function useDataSource(
+  propsRef: ComputedRef<BasicTableProps>,
+  {
+    getPaginationInfo,
+    setPagination,
+    setLoading,
+    getFieldsValue,
+    clearSelectedRowKeys,
+    tableData,
+  }: ActionType,
+  emit: EmitType,
+) {
+  const searchState = reactive<SearchState>({
+    sortInfo: {},
+    filterInfo: {},
+  });
+  const dataSourceRef = ref<Recordable[]>([]);
+  const rawDataSourceRef = ref<Recordable>({});
+
+  watchEffect(() => {
+    tableData.value = unref(dataSourceRef);
+  });
+
+  watch(
+    () => unref(propsRef).dataSource,
+    () => {
+      const { dataSource, api } = unref(propsRef);
+      !api && dataSource && (dataSourceRef.value = dataSource);
+    },
+    {
+      immediate: true,
+    },
+  );
+
+  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 => {
+      if (!item[ROW_KEY]) {
+        item[ROW_KEY] = buildUUID();
+      }
+      if (item.children && item.children.length) {
+        setTableKey(item.children);
+      }
+    });
+  }
+
+  const getAutoCreateKey = computed(() => {
+    return unref(propsRef).autoCreateKey && !unref(propsRef).rowKey;
+  });
+
+  const getRowKey = computed(() => {
+    const { rowKey } = unref(propsRef);
+    return unref(getAutoCreateKey) ? ROW_KEY : rowKey;
+  });
+
+  const getDataSourceRef = computed(() => {
+    const dataSource = unref(dataSourceRef);
+    if (!dataSource || dataSource.length === 0) {
+      return unref(dataSourceRef);
+    }
+    if (unref(getAutoCreateKey)) {
+      const firstItem = dataSource[0];
+      const lastItem = dataSource[dataSource.length - 1];
+
+      if (firstItem && lastItem) {
+        if (!firstItem[ROW_KEY] || !lastItem[ROW_KEY]) {
+          const data = cloneDeep(unref(dataSourceRef));
+          data.forEach(item => {
+            if (!item[ROW_KEY]) {
+              item[ROW_KEY] = buildUUID();
+            }
+            if (item.children && item.children.length) {
+              setTableKey(item.children);
+            }
+          });
+          dataSourceRef.value = data;
+        }
+      }
+    }
+    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];
+  }
+
+  function updateTableDataRecord(
+    rowKey: string | number,
+    record: Recordable,
+  ): Recordable | undefined {
+    const row = findTableDataRecord(rowKey);
+
+    if (row) {
+      for (const field in row) {
+        if (Reflect.has(record, field)) row[field] = record[field];
+      }
+      return row;
+    }
+  }
+
+  function deleteTableDataRecord(rowKey: string | number | string[] | number[]) {
+    if (!dataSourceRef.value || dataSourceRef.value.length == 0) return;
+    const rowKeyName = unref(getRowKey);
+    if (!rowKeyName) return;
+    const rowKeys = !Array.isArray(rowKey) ? [rowKey] : rowKey;
+    for (const key of rowKeys) {
+      let index: number | undefined = dataSourceRef.value.findIndex(row => {
+        let targetKeyName: string;
+        if (typeof rowKeyName === 'function') {
+          targetKeyName = rowKeyName(row);
+        } else {
+          targetKeyName = rowKeyName as string;
+        }
+        return row[targetKeyName] === key;
+      });
+      if (index >= 0) {
+        dataSourceRef.value.splice(index, 1);
+      }
+      index = unref(propsRef).dataSource?.findIndex(row => {
+        let targetKeyName: string;
+        if (typeof rowKeyName === 'function') {
+          targetKeyName = rowKeyName(row);
+        } else {
+          targetKeyName = rowKeyName as string;
+        }
+        return row[targetKeyName] === key;
+      });
+      if (typeof index !== 'undefined' && index !== -1)
+        unref(propsRef).dataSource?.splice(index, 1);
+    }
+    setPagination({
+      total: unref(propsRef).dataSource?.length,
+    });
+  }
+
+  function insertTableDataRecord(record: Recordable, index: number): Recordable | undefined {
+    // if (!dataSourceRef.value || dataSourceRef.value.length == 0) return;
+    index = index ?? dataSourceRef.value?.length;
+    unref(dataSourceRef).splice(index, 0, record);
+    return unref(dataSourceRef);
+  }
+
+  function findTableDataRecord(rowKey: string | number) {
+    if (!dataSourceRef.value || dataSourceRef.value.length == 0) return;
+
+    const rowKeyName = unref(getRowKey);
+    if (!rowKeyName) return;
+
+    const { childrenColumnName = 'children' } = unref(propsRef);
+
+    const findRow = (array: any[]) => {
+      let ret;
+      array.some(function iter(r) {
+        if (typeof rowKeyName === 'function') {
+          if ((rowKeyName(r) as string) === rowKey) {
+            ret = r;
+            return true;
+          }
+        } else {
+          if (Reflect.has(r, rowKeyName) && r[rowKeyName] === rowKey) {
+            ret = r;
+            return true;
+          }
+        }
+        return r[childrenColumnName] && r[childrenColumnName].some(iter);
+      });
+      return ret;
+    };
+
+    // const row = dataSourceRef.value.find(r => {
+    //   if (typeof rowKeyName === 'function') {
+    //     return (rowKeyName(r) as string) === rowKey
+    //   } else {
+    //     return Reflect.has(r, rowKeyName) && r[rowKeyName] === rowKey
+    //   }
+    // })
+    return findRow(dataSourceRef.value);
+  }
+
+  async function fetch(opt?: FetchParams) {
+    const {
+      api,
+      searchInfo,
+      defSort,
+      fetchSetting,
+      beforeFetch,
+      afterFetch,
+      useSearchForm,
+      pagination,
+    } = unref(propsRef);
+    if (!api || !isFunction(api)) return;
+    try {
+      setLoading(true);
+      const { pageField, sizeField, listField, totalField } = Object.assign(
+        {},
+        FETCH_SETTING,
+        fetchSetting,
+      );
+      let pageParams: Recordable = {};
+
+      const { current = 1, pageSize = PAGE_SIZE } = unref(getPaginationInfo) as PaginationProps;
+
+      if ((isBoolean(pagination) && !pagination) || isBoolean(getPaginationInfo)) {
+        pageParams = {};
+      } else {
+        pageParams[pageField] = (opt && opt.page) || current;
+        pageParams[sizeField] = pageSize;
+      }
+
+      const { sortInfo = {}, filterInfo } = searchState;
+
+      let params: Recordable = merge(
+        pageParams,
+        useSearchForm ? getFieldsValue() : {},
+        searchInfo,
+        opt?.searchInfo ?? {},
+        defSort,
+        sortInfo,
+        filterInfo,
+        opt?.sortInfo ?? {},
+        opt?.filterInfo ?? {},
+      );
+      if (beforeFetch && isFunction(beforeFetch)) {
+        params = (await beforeFetch(params)) || params;
+      }
+
+      const res = await api(params);
+      rawDataSourceRef.value = res;
+
+      const isArrayResult = Array.isArray(res);
+
+      let resultItems: Recordable[] = isArrayResult ? res : get(res, listField);
+      const resultTotal: number = isArrayResult ? res.length : get(res, totalField);
+
+      // 假如数据变少,导致总页数变少并小于当前选中页码,通过getPaginationRef获取到的页码是不正确的,需获取正确的页码再次执行
+      if (resultTotal) {
+        const currentTotalPage = Math.ceil(resultTotal / pageSize);
+        if (current > currentTotalPage) {
+          setPagination({
+            current: currentTotalPage,
+          });
+          return await fetch(opt);
+        }
+      }
+
+      if (afterFetch && isFunction(afterFetch)) {
+        resultItems = (await afterFetch(resultItems)) || resultItems;
+      }
+      dataSourceRef.value = resultItems;
+      setPagination({
+        total: resultTotal || 0,
+      });
+      if (opt && opt.page) {
+        setPagination({
+          current: opt.page || 1,
+        });
+      }
+      emit('fetch-success', {
+        items: unref(resultItems),
+        total: resultTotal,
+      });
+      return resultItems;
+    } catch (error) {
+      emit('fetch-error', error);
+      dataSourceRef.value = [];
+      setPagination({
+        total: 0,
+      });
+    } finally {
+      setLoading(false);
+    }
+  }
+
+  function setTableData<T = Recordable>(values: T[]) {
+    dataSourceRef.value = values;
+  }
+
+  function getDataSource<T = Recordable>() {
+    return getDataSourceRef.value as T[];
+  }
+
+  function getRawDataSource<T = Recordable>() {
+    return rawDataSourceRef.value as T;
+  }
+
+  async function reload(opt?: FetchParams) {
+    return await fetch(opt);
+  }
+
+  onMounted(() => {
+    useTimeoutFn(() => {
+      unref(propsRef).immediate && fetch();
+    }, 16);
+  });
+
+  return {
+    getDataSourceRef,
+    getDataSource,
+    getRawDataSource,
+    getRowKey,
+    setTableData,
+    getAutoCreateKey,
+    fetch,
+    reload,
+    updateTableData,
+    updateTableDataRecord,
+    deleteTableDataRecord,
+    insertTableDataRecord,
+    findTableDataRecord,
+    handleTableChange,
+  };
+}

+ 21 - 0
src/components/TableCard/src/hooks/useLoading.ts

@@ -0,0 +1,21 @@
+import { ref, ComputedRef, unref, computed, watch } from 'vue';
+import type { BasicTableProps } from '../types/table';
+
+export function useLoading(props: ComputedRef<BasicTableProps>) {
+  const loadingRef = ref(unref(props).loading);
+
+  watch(
+    () => unref(props).loading,
+    loading => {
+      loadingRef.value = loading;
+    },
+  );
+
+  const getLoading = computed(() => unref(loadingRef));
+
+  function setLoading(loading: boolean) {
+    loadingRef.value = loading;
+  }
+
+  return { getLoading, setLoading };
+}

+ 82 - 0
src/components/TableCard/src/hooks/usePagination.tsx

@@ -0,0 +1,82 @@
+import type { PaginationProps } from '../types/pagination';
+import type { BasicTableProps } from '../types/table';
+import { computed, unref, ref, ComputedRef, watch } from 'vue';
+import { LeftOutlined, RightOutlined } from '@ant-design/icons-vue';
+import { isBoolean } from '/@/utils/is';
+import { PAGE_SIZE, PAGE_SIZE_OPTIONS } from '../const';
+interface ItemRender {
+  page: number;
+  type: 'page' | 'prev' | 'next';
+  originalElement: any;
+}
+
+function itemRender({ page, type, originalElement }: ItemRender) {
+  if (type === 'prev') {
+    return page === 0 ? null : <LeftOutlined />;
+  } else if (type === 'next') {
+    return page === 1 ? null : <RightOutlined />;
+  }
+  return originalElement;
+}
+
+export function usePagination(refProps: ComputedRef<BasicTableProps>) {
+  const configRef = ref<PaginationProps>({});
+  const show = ref(true);
+
+  watch(
+    () => unref(refProps).pagination,
+    pagination => {
+      if (!isBoolean(pagination) && pagination) {
+        configRef.value = {
+          ...unref(configRef),
+          ...(pagination ?? {}),
+        };
+      }
+    },
+  );
+
+  const getPaginationInfo = computed((): PaginationProps | boolean => {
+    const { pagination } = unref(refProps);
+
+    if (!unref(show) || (isBoolean(pagination) && !pagination)) {
+      return false;
+    }
+
+    return {
+      current: 1,
+      pageSize: PAGE_SIZE,
+      size: 'small',
+      defaultPageSize: PAGE_SIZE,
+      // showTotal: total => `共 ${total} 条数据 `,
+      showSizeChanger: true,
+      pageSizeOptions: PAGE_SIZE_OPTIONS,
+      position: ['bottomRight'],
+      itemRender: itemRender,
+      showQuickJumper: false,
+      ...(isBoolean(pagination) ? {} : pagination),
+      ...unref(configRef),
+    };
+  });
+
+  function setPagination(info: Partial<PaginationProps>) {
+    const paginationInfo = unref(getPaginationInfo);
+    configRef.value = {
+      ...(!isBoolean(paginationInfo) ? paginationInfo : {}),
+      ...info,
+    };
+  }
+
+  function getPagination() {
+    return unref(getPaginationInfo);
+  }
+
+  function getShowPagination() {
+    return unref(show);
+  }
+
+  async function setShowPagination(flag: boolean) {
+    show.value = flag;
+  }
+
+  return { getPagination, getPaginationInfo, setShowPagination, getShowPagination, setPagination };
+}

+ 128 - 0
src/components/TableCard/src/hooks/useRowSelection.ts

@@ -0,0 +1,128 @@
+import { isFunction } from '/@/utils/is';
+import type { BasicTableProps, TableRowSelection } from '../types/table';
+import { computed, ComputedRef, nextTick, Ref, ref, toRaw, unref, watch } from 'vue';
+import { ROW_KEY } from '../const';
+import { omit } from 'lodash-es';
+import { findNodeAll } from '/@/utils/helper/treeHelper';
+
+export function useRowSelection(
+  propsRef: ComputedRef<BasicTableProps>,
+  tableData: Ref<Recordable[]>,
+  emit: EmitType,
+) {
+  const selectedRowKeysRef = ref<string[]>([]);
+  const selectedRowRef = ref<Recordable[]>([]);
+
+  const getRowSelectionRef = computed((): TableRowSelection | null => {
+    const { rowSelection } = unref(propsRef);
+    if (!rowSelection) {
+      return null;
+    }
+
+    return {
+      selectedRowKeys: unref(selectedRowKeysRef),
+      onChange: (selectedRowKeys: string[]) => {
+        setSelectedRowKeys(selectedRowKeys);
+      },
+      ...omit(rowSelection, ['onChange']),
+    };
+  });
+
+  watch(
+    () => unref(propsRef).rowSelection?.selectedRowKeys,
+    (v: string[]) => {
+      setSelectedRowKeys(v);
+    },
+  );
+
+  watch(
+    () => unref(selectedRowKeysRef),
+    () => {
+      nextTick(() => {
+        const { rowSelection } = unref(propsRef);
+        if (rowSelection) {
+          const { onChange } = rowSelection;
+          if (onChange && isFunction(onChange)) onChange(getSelectRowKeys(), getSelectRows());
+        }
+        emit('selection-change', {
+          keys: getSelectRowKeys(),
+          rows: getSelectRows(),
+        });
+      });
+    },
+    { deep: true },
+  );
+
+  const getAutoCreateKey = computed(() => {
+    return unref(propsRef).autoCreateKey && !unref(propsRef).rowKey;
+  });
+
+  const getRowKey = computed(() => {
+    const { rowKey } = unref(propsRef);
+    return unref(getAutoCreateKey) ? ROW_KEY : rowKey;
+  });
+
+  function setSelectedRowKeys(rowKeys: string[]) {
+    selectedRowKeysRef.value = rowKeys;
+    const allSelectedRows = findNodeAll(
+      toRaw(unref(tableData)).concat(toRaw(unref(selectedRowRef))),
+      item => rowKeys?.includes(item[unref(getRowKey) as string]),
+      {
+        children: propsRef.value.childrenColumnName ?? 'children',
+      },
+    );
+    const trueSelectedRows: any[] = [];
+    rowKeys?.forEach((key: string) => {
+      const found = allSelectedRows.find(item => item[unref(getRowKey) as string] === key);
+      found && trueSelectedRows.push(found);
+    });
+    selectedRowRef.value = trueSelectedRows;
+  }
+
+  function setSelectedRows(rows: Recordable[]) {
+    selectedRowRef.value = rows;
+  }
+
+  function clearSelectedRowKeys() {
+    selectedRowRef.value = [];
+    selectedRowKeysRef.value = [];
+  }
+
+  function deleteSelectRowByKey(key: string) {
+    const selectedRowKeys = unref(selectedRowKeysRef);
+    const index = selectedRowKeys.findIndex(item => item === key);
+    if (index !== -1) {
+      unref(selectedRowKeysRef).splice(index, 1);
+    }
+  }
+
+  function getSelectRowKeys() {
+    return unref(selectedRowKeysRef);
+  }
+
+  function getSelectRows<T = Recordable>() {
+    // const ret = toRaw(unref(selectedRowRef)).map((item) => toRaw(item));
+    return unref(selectedRowRef) as T[];
+  }
+
+  function getRowSelection() {
+    return unref(getRowSelectionRef)!;
+  }
+
+  function batchDel() {}
+  // function batchPrint() {}
+  function batchExport() {}
+
+  return {
+    getRowSelection,
+    getRowSelectionRef,
+    getSelectRows,
+    getSelectRowKeys,
+    setSelectedRowKeys,
+    clearSelectedRowKeys,
+    deleteSelectRowByKey,
+    setSelectedRows,
+    batchDel,
+    batchExport,
+  };
+}

+ 55 - 0
src/components/TableCard/src/hooks/useScrollTo.ts

@@ -0,0 +1,55 @@
+import type { ComputedRef, Ref } from 'vue';
+import { nextTick, unref } from 'vue';
+import { warn } from '/@/utils/log';
+
+export function useTableScrollTo(
+  tableElRef: Ref<ComponentRef>,
+  getDataSourceRef: ComputedRef<Recordable[]>,
+) {
+  let bodyEl: HTMLElement | null;
+
+  async function findTargetRowToScroll(targetRowData: Recordable) {
+    const { id } = targetRowData;
+    const targetRowEl: HTMLElement | null | undefined = bodyEl?.querySelector(
+      `[data-row-key="${id}"]`,
+    );
+    //Add a delay to get new dataSource
+    await nextTick();
+    bodyEl?.scrollTo({
+      top: targetRowEl?.offsetTop ?? 0,
+      behavior: 'smooth',
+    });
+  }
+
+  function scrollTo(pos: string): void {
+    const table = unref(tableElRef);
+    if (!table) return;
+
+    const tableEl: Element = table.$el;
+    if (!tableEl) return;
+
+    if (!bodyEl) {
+      bodyEl = tableEl.querySelector('.ant-table-body');
+      if (!bodyEl) return;
+    }
+
+    const dataSource = unref(getDataSourceRef);
+    if (!dataSource) return;
+
+    // judge pos type
+    if (pos === 'top') {
+      findTargetRowToScroll(dataSource[0]);
+    } else if (pos === 'bottom') {
+      findTargetRowToScroll(dataSource[dataSource.length - 1]);
+    } else {
+      const targetRowData = dataSource.find(data => data.id === pos);
+      if (targetRowData) {
+        findTargetRowToScroll(targetRowData);
+      } else {
+        warn(`id: ${pos} doesn't exist`);
+      }
+    }
+  }
+
+  return { scrollTo };
+}

+ 170 - 0
src/components/TableCard/src/hooks/useTable.ts

@@ -0,0 +1,170 @@
+import type { BasicTableProps, TableActionType, FetchParams, BasicColumn } from '../types/table';
+import type { PaginationProps } from '../types/pagination';
+import type { DynamicProps } from '/#/utils';
+import type { FormActionType } from '/@/components/Form';
+import type { WatchStopHandle } from 'vue';
+import { getDynamicProps } from '/@/utils';
+import { ref, onUnmounted, unref, watch, toRaw } from 'vue';
+import { isProdMode } from '/@/utils/env';
+import { error } from '/@/utils/log';
+
+type Props = Partial<DynamicProps<BasicTableProps>>;
+
+type UseTableMethod = TableActionType & {
+  getForm: () => FormActionType;
+};
+
+export function useTable(tableProps?: Props): [
+  (instance: TableActionType, formInstance: UseTableMethod) => void,
+  TableActionType & {
+    getForm: () => FormActionType;
+  },
+] {
+  const tableRef = ref<Nullable<TableActionType>>(null);
+  const loadedRef = ref<Nullable<boolean>>(false);
+  const formRef = ref<Nullable<UseTableMethod>>(null);
+
+  let stopWatch: WatchStopHandle;
+
+  function register(instance: TableActionType, formInstance: UseTableMethod) {
+    isProdMode() &&
+      onUnmounted(() => {
+        tableRef.value = null;
+        loadedRef.value = null;
+      });
+
+    if (unref(loadedRef) && isProdMode() && instance === unref(tableRef)) return;
+
+    tableRef.value = instance;
+    formRef.value = formInstance;
+    tableProps && instance.setProps(getDynamicProps(tableProps));
+    loadedRef.value = true;
+
+    stopWatch?.();
+
+    stopWatch = watch(
+      () => tableProps,
+      () => {
+        tableProps && instance.setProps(getDynamicProps(tableProps));
+      },
+      {
+        immediate: true,
+        deep: true,
+      },
+    );
+  }
+
+  function getTableInstance(): TableActionType {
+    const table = unref(tableRef);
+    if (!table) {
+      error(
+        'The table instance has not been obtained yet, please make sure the table is presented when performing the table operation!',
+      );
+    }
+    return table as TableActionType;
+  }
+
+  const methods: TableActionType & {
+    getForm: () => FormActionType;
+  } = {
+    reload: async (opt?: FetchParams) => {
+      return await getTableInstance().reload(opt);
+    },
+    setProps: (props: Partial<BasicTableProps>) => {
+      getTableInstance().setProps(props);
+    },
+    redoHeight: () => {
+      getTableInstance().redoHeight();
+    },
+    setSelectedRows: (rows: Recordable[]) => {
+      return toRaw(getTableInstance().setSelectedRows(rows));
+    },
+    setLoading: (loading: boolean) => {
+      getTableInstance().setLoading(loading);
+    },
+    getDataSource: () => {
+      return getTableInstance().getDataSource();
+    },
+    getRawDataSource: () => {
+      return getTableInstance().getRawDataSource();
+    },
+    getColumns: ({ ignoreIndex = false }: { ignoreIndex?: boolean } = {}) => {
+      const columns = getTableInstance().getColumns({ ignoreIndex }) || [];
+      return toRaw(columns);
+    },
+    setColumns: (columns: BasicColumn[]) => {
+      getTableInstance().setColumns(columns);
+    },
+    setTableData: (values: any[]) => {
+      return getTableInstance().setTableData(values);
+    },
+    setPagination: (info: Partial<PaginationProps>) => {
+      return getTableInstance().setPagination(info);
+    },
+    deleteSelectRowByKey: (key: string) => {
+      getTableInstance().deleteSelectRowByKey(key);
+    },
+    getSelectRowKeys: () => {
+      return toRaw(getTableInstance().getSelectRowKeys());
+    },
+    getSelectRows: () => {
+      return toRaw(getTableInstance().getSelectRows());
+    },
+    clearSelectedRowKeys: () => {
+      getTableInstance().clearSelectedRowKeys();
+    },
+    setSelectedRowKeys: (keys: string[] | number[]) => {
+      getTableInstance().setSelectedRowKeys(keys);
+    },
+    getPaginationRef: () => {
+      return getTableInstance().getPaginationRef();
+    },
+    getSize: () => {
+      return toRaw(getTableInstance().getSize());
+    },
+    updateTableData: (index: number, key: string, value: any) => {
+      return getTableInstance().updateTableData(index, key, value);
+    },
+    deleteTableDataRecord: (rowKey: string | number | string[] | number[]) => {
+      return getTableInstance().deleteTableDataRecord(rowKey);
+    },
+    insertTableDataRecord: (record: Recordable | Recordable[], index?: number) => {
+      return getTableInstance().insertTableDataRecord(record, index);
+    },
+    updateTableDataRecord: (rowKey: string | number, record: Recordable) => {
+      return getTableInstance().updateTableDataRecord(rowKey, record);
+    },
+    findTableDataRecord: (rowKey: string | number) => {
+      return getTableInstance().findTableDataRecord(rowKey);
+    },
+    getRowSelection: () => {
+      return toRaw(getTableInstance().getRowSelection());
+    },
+    getCacheColumns: () => {
+      return toRaw(getTableInstance().getCacheColumns());
+    },
+    getForm: () => {
+      return unref(formRef) as unknown as FormActionType;
+    },
+    setShowPagination: async (show: boolean) => {
+      getTableInstance().setShowPagination(show);
+    },
+    getShowPagination: () => {
+      return toRaw(getTableInstance().getShowPagination());
+    },
+    expandAll: () => {
+      getTableInstance().expandAll();
+    },
+    expandRows: (keys: string[]) => {
+      getTableInstance().expandRows(keys);
+    },
+    collapseAll: () => {
+      getTableInstance().collapseAll();
+    },
+    scrollTo: (pos: string) => {
+      getTableInstance().scrollTo(pos);
+    },
+  };
+
+  return [register, methods];
+}

+ 22 - 0
src/components/TableCard/src/hooks/useTableContext.ts

@@ -0,0 +1,22 @@
+import type { Ref } from 'vue';
+import type { BasicTableProps, TableActionType } from '../types/table';
+import { provide, inject, ComputedRef } from 'vue';
+
+const key = Symbol('basic-table');
+
+type Instance = TableActionType & {
+  wrapRef: Ref<Nullable<HTMLElement>>;
+  getBindValues: ComputedRef<Recordable>;
+};
+
+type RetInstance = Omit<Instance, 'getBindValues'> & {
+  getBindValues: ComputedRef<BasicTableProps>;
+};
+
+export function createTableContext(instance: Instance) {
+  provide(key, instance);
+}
+
+export function useTableContext(): RetInstance {
+  return inject(key) as RetInstance;
+}

+ 65 - 0
src/components/TableCard/src/hooks/useTableExpand.ts

@@ -0,0 +1,65 @@
+import type { ComputedRef, Ref } from 'vue';
+import type { BasicTableProps } from '../types/table';
+import { computed, unref, ref, toRaw } from 'vue';
+import { ROW_KEY } from '../const';
+
+export function useTableExpand(
+  propsRef: ComputedRef<BasicTableProps>,
+  tableData: Ref<Recordable[]>,
+  emit: EmitType,
+) {
+  const expandedRowKeys = ref<string[]>([]);
+
+  const getAutoCreateKey = computed(() => {
+    return unref(propsRef).autoCreateKey && !unref(propsRef).rowKey;
+  });
+
+  const getRowKey = computed(() => {
+    const { rowKey } = unref(propsRef);
+    return unref(getAutoCreateKey) ? ROW_KEY : rowKey;
+  });
+
+  const getExpandOption = computed(() => {
+    const { isTreeTable } = unref(propsRef);
+    if (!isTreeTable) return {};
+
+    return {
+      expandedRowKeys: unref(expandedRowKeys),
+      onExpandedRowsChange: (keys: string[]) => {
+        expandedRowKeys.value = keys;
+        emit('expanded-rows-change', keys);
+      },
+    };
+  });
+
+  function expandAll() {
+    const keys = getAllKeys();
+    expandedRowKeys.value = keys;
+  }
+
+  function expandRows(keys: string[]) {
+    // use row ID expands the specified table row
+    const { isTreeTable } = unref(propsRef);
+    if (!isTreeTable) return;
+    expandedRowKeys.value = [...expandedRowKeys.value, ...keys];
+  }
+
+  function getAllKeys(data?: Recordable[]) {
+    const keys: string[] = [];
+    const { childrenColumnName } = unref(propsRef);
+    toRaw(data || unref(tableData)).forEach(item => {
+      keys.push(item[unref(getRowKey) as string]);
+      const children = item[childrenColumnName || 'children'];
+      if (children?.length) {
+        keys.push(...getAllKeys(children));
+      }
+    });
+    return keys;
+  }
+
+  function collapseAll() {
+    expandedRowKeys.value = [];
+  }
+
+  return { getExpandOption, expandAll, expandRows, collapseAll };
+}

+ 56 - 0
src/components/TableCard/src/hooks/useTableFooter.ts

@@ -0,0 +1,56 @@
+import type { ComputedRef, Ref } from 'vue';
+import type { BasicTableProps } from '../types/table';
+import { unref, computed, h, nextTick, watchEffect } from 'vue';
+import TableFooter from '../components/TableFooter.vue';
+import { useEventListener } from '/@/hooks/event/useEventListener';
+
+export function useTableFooter(
+  propsRef: ComputedRef<BasicTableProps>,
+  scrollRef: ComputedRef<{
+    x: string | number | true;
+    y: string | number | null;
+    scrollToFirstRowOnChange: boolean;
+  }>,
+  tableElRef: Ref<ComponentRef>,
+  getDataSourceRef: ComputedRef<Recordable>,
+) {
+  const getIsEmptyData = computed(() => {
+    return (unref(getDataSourceRef) || []).length === 0;
+  });
+
+  const getFooterProps = computed((): Recordable | undefined => {
+    const { summaryFunc, showSummary, summaryData } = unref(propsRef);
+    return showSummary && !unref(getIsEmptyData)
+      ? () => h(TableFooter, { summaryFunc, summaryData, scroll: unref(scrollRef) })
+      : undefined;
+  });
+
+  watchEffect(() => {
+    handleSummary();
+  });
+
+  function handleSummary() {
+    const { showSummary } = unref(propsRef);
+    if (!showSummary || unref(getIsEmptyData)) return;
+
+    nextTick(() => {
+      const tableEl = unref(tableElRef);
+      if (!tableEl) return;
+      const bodyDom = tableEl.$el.querySelector('.ant-table-content');
+      useEventListener({
+        el: bodyDom,
+        name: 'scroll',
+        listener: () => {
+          const footerBodyDom = tableEl.$el.querySelector(
+            '.ant-table-footer .ant-table-content',
+          ) as HTMLDivElement;
+          if (!footerBodyDom || !bodyDom) return;
+          footerBodyDom.scrollLeft = bodyDom.scrollLeft;
+        },
+        wait: 0,
+        options: true,
+      });
+    });
+  }
+  return { getFooterProps };
+}

+ 220 - 0
src/components/TableCard/src/hooks/useTableScroll.ts

@@ -0,0 +1,220 @@
+import type { BasicTableProps, TableRowSelection, BasicColumn } from '../types/table';
+import { Ref, ComputedRef, ref } from 'vue';
+import { computed, unref, nextTick, watch } from 'vue';
+import { getViewportOffset } from '/@/utils/domUtils';
+import { isBoolean } from '/@/utils/is';
+import { useWindowSizeFn } from '/@/hooks/event/useWindowSizeFn';
+import { useModalContext } from '/@/components/Modal';
+import { onMountedOrActivated } from '/@/hooks/core/onMountedOrActivated';
+import { useDebounceFn } from '@vueuse/core';
+
+export function useTableScroll(
+  propsRef: ComputedRef<BasicTableProps>,
+  tableElRef: Ref<ComponentRef>,
+  columnsRef: ComputedRef<BasicColumn[]>,
+  rowSelectionRef: ComputedRef<TableRowSelection | null>,
+  getDataSourceRef: ComputedRef<Recordable[]>,
+  wrapRef: Ref<HTMLElement | null>,
+  formRef: Ref<ComponentRef>,
+) {
+  const tableHeightRef: Ref<Nullable<number | string>> = ref(167);
+  const modalFn = useModalContext();
+
+  // Greater than animation time 280
+  const debounceRedoHeight = useDebounceFn(redoHeight, 100);
+
+  const getCanResize = computed(() => {
+    const { canResize, scroll } = unref(propsRef);
+    return canResize && !(scroll || {}).y;
+  });
+
+  watch(
+    () => [unref(getCanResize), unref(getDataSourceRef)?.length],
+    () => {
+      debounceRedoHeight();
+    },
+    {
+      flush: 'post',
+    },
+  );
+
+  function redoHeight() {
+    nextTick(() => {
+      calcTableHeight();
+    });
+  }
+
+  function setHeight(height: number) {
+    tableHeightRef.value = height;
+    //  Solve the problem of modal adaptive height calculation when the form is placed in the modal
+    modalFn?.redoModalHeight?.();
+  }
+
+  // No need to repeat queries
+  let paginationEl: HTMLElement | null;
+  let footerEl: HTMLElement | null;
+  let bodyEl: HTMLElement | null;
+
+  async function calcTableHeight() {
+    const { resizeHeightOffset, pagination, maxHeight, isCanResizeParent, useSearchForm } =
+      unref(propsRef);
+    const tableData = unref(getDataSourceRef);
+
+    const table = unref(tableElRef);
+    if (!table) return;
+
+    const tableEl: Element = table.$el;
+    if (!tableEl) return;
+
+    if (!bodyEl) {
+      bodyEl = tableEl.querySelector('.ant-table-body');
+      if (!bodyEl) return;
+    }
+
+    const hasScrollBarY = bodyEl.scrollHeight > bodyEl.clientHeight;
+    const hasScrollBarX = bodyEl.scrollWidth > bodyEl.clientWidth;
+
+    if (hasScrollBarY) {
+      tableEl.classList.contains('hide-scrollbar-y') &&
+        tableEl.classList.remove('hide-scrollbar-y');
+    } else {
+      !tableEl.classList.contains('hide-scrollbar-y') && tableEl.classList.add('hide-scrollbar-y');
+    }
+
+    if (hasScrollBarX) {
+      tableEl.classList.contains('hide-scrollbar-x') &&
+        tableEl.classList.remove('hide-scrollbar-x');
+    } else {
+      !tableEl.classList.contains('hide-scrollbar-x') && tableEl.classList.add('hide-scrollbar-x');
+    }
+
+    bodyEl!.style.height = 'unset';
+
+    if (!unref(getCanResize) || !unref(tableData) || tableData.length === 0) return;
+
+    await nextTick();
+    // Add a delay to get the correct bottomIncludeBody paginationHeight footerHeight headerHeight
+
+    const headEl = tableEl.querySelector('.ant-table-thead ');
+
+    if (!headEl) return;
+
+    // Table height from bottom height-custom offset
+    let paddingHeight = 32;
+    // Pager height
+    let paginationHeight = 2;
+    if (!isBoolean(pagination)) {
+      paginationEl = tableEl.querySelector('.ant-pagination') as HTMLElement;
+      if (paginationEl) {
+        const offsetHeight = paginationEl.offsetHeight;
+        paginationHeight += offsetHeight || 0;
+      } else {
+        // TODO First fix 24
+        paginationHeight += 24;
+      }
+    } else {
+      paginationHeight = -8;
+    }
+
+    let footerHeight = 0;
+    if (!isBoolean(pagination)) {
+      if (!footerEl) {
+        footerEl = tableEl.querySelector('.ant-table-footer') as HTMLElement;
+      } else {
+        const offsetHeight = footerEl.offsetHeight;
+        footerHeight += offsetHeight || 0;
+      }
+    }
+
+    let headerHeight = 0;
+    if (headEl) {
+      headerHeight = (headEl as HTMLElement).offsetHeight;
+    }
+
+    let bottomIncludeBody = 0;
+    if (unref(wrapRef) && isCanResizeParent) {
+      const tablePadding = 12;
+      const formMargin = 16;
+      let paginationMargin = 10;
+      const wrapHeight = unref(wrapRef)?.offsetHeight ?? 0;
+
+      let formHeight = unref(formRef)?.$el.offsetHeight ?? 0;
+      if (formHeight) {
+        formHeight += formMargin;
+      }
+      if (isBoolean(pagination) && !pagination) {
+        paginationMargin = 0;
+      }
+      if (isBoolean(useSearchForm) && !useSearchForm) {
+        paddingHeight = 0;
+      }
+
+      const headerCellHeight =
+        (tableEl.querySelector('.ant-table-title') as HTMLElement)?.offsetHeight ?? 0;
+
+      console.log(wrapHeight - formHeight - headerCellHeight - tablePadding - paginationMargin);
+      bottomIncludeBody =
+        wrapHeight - formHeight - headerCellHeight - tablePadding - paginationMargin;
+    } else {
+      // Table height from bottom
+      bottomIncludeBody = getViewportOffset(headEl).bottomIncludeBody;
+    }
+
+    let height =
+      bottomIncludeBody -
+      (resizeHeightOffset || 0) -
+      paddingHeight -
+      paginationHeight -
+      footerHeight -
+      headerHeight;
+    height = (height > maxHeight! ? (maxHeight as number) : height) ?? height;
+    setHeight(height);
+
+    bodyEl!.style.height = `${height}px`;
+  }
+  useWindowSizeFn(calcTableHeight, 280);
+  onMountedOrActivated(() => {
+    calcTableHeight();
+    nextTick(() => {
+      debounceRedoHeight();
+    });
+  });
+
+  const getScrollX = computed(() => {
+    let width = 0;
+    if (unref(rowSelectionRef)) {
+      width += 60;
+    }
+
+    // TODO props ?? 0;
+    const NORMAL_WIDTH = 150;
+
+    const columns = unref(columnsRef).filter(item => !item.defaultHidden);
+    columns.forEach(item => {
+      width += Number.parseFloat(item.width as string) || 0;
+    });
+    const unsetWidthColumns = columns.filter(item => !Reflect.has(item, 'width'));
+
+    const len = unsetWidthColumns.length;
+    if (len !== 0) {
+      width += len * NORMAL_WIDTH;
+    }
+
+    const table = unref(tableElRef);
+    const tableWidth = table?.$el?.offsetWidth ?? 0;
+    return tableWidth > width ? '100%' : width;
+  });
+
+  const getScrollRef = computed(() => {
+    const tableHeight = unref(tableHeightRef);
+    const { canResize, scroll } = unref(propsRef);
+    return {
+      x: unref(getScrollX),
+      y: canResize ? tableHeight : null,
+      scrollToFirstRowOnChange: false,
+      ...scroll,
+    };
+  });
+
+  return { getScrollRef, redoHeight };
+}

+ 20 - 0
src/components/TableCard/src/hooks/useTableStyle.ts

@@ -0,0 +1,20 @@
+import type { ComputedRef } from 'vue';
+import type { BasicTableProps, TableCustomRecord } from '../types/table';
+import { unref } from 'vue';
+import { isFunction } from '/@/utils/is';
+
+export function useTableStyle(propsRef: ComputedRef<BasicTableProps>, prefixCls: string) {
+  function getRowClassName(record: TableCustomRecord, index: number) {
+    const { striped, rowClassName } = unref(propsRef);
+    const classNames: string[] = [];
+    if (striped) {
+      classNames.push((index || 0) % 2 === 1 ? `${prefixCls}-row__striped` : '');
+    }
+    if (rowClassName && isFunction(rowClassName)) {
+      classNames.push(rowClassName(record, index));
+    }
+    return classNames.filter(cls => !!cls).join(' ');
+  }
+
+  return { getRowClassName };
+}

+ 179 - 0
src/components/TableCard/src/props.ts

@@ -0,0 +1,179 @@
+import type { PropType } from 'vue';
+import type { PaginationProps } from './types/pagination';
+import type {
+  BasicColumn,
+  FetchSetting,
+  TableSetting,
+  SorterResult,
+  TableCustomRecord,
+  TableRowSelection,
+  SizeType,
+} from './types/table';
+import type { FormProps } from '/@/components/Form';
+
+import { DEFAULT_FILTER_FN, DEFAULT_SORT_FN, FETCH_SETTING, DEFAULT_SIZE } from './const';
+import { propTypes } from '/@/utils/propTypes';
+
+export const basicProps = {
+  id: { type: String, default: '' },
+  storage: Boolean,
+  clickToRowSelect: { type: Boolean, default: true },
+  isTreeTable: Boolean,
+  tableSetting: propTypes.shape<TableSetting>({}),
+  inset: Boolean,
+  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: Boolean,
+  autoCreateKey: { type: Boolean, default: true },
+  striped: { type: Boolean, default: false },
+  showSummary: Boolean,
+  summaryFunc: {
+    type: [Function, Array] as PropType<(...arg: any[]) => any[]>,
+    default: null,
+  },
+  summaryData: {
+    type: Array as PropType<Recordable[]>,
+    default: null,
+  },
+  indentSize: propTypes.number.def(24),
+  canColDrag: { type: Boolean, default: true },
+  api: {
+    type: Function as PropType<(...arg: any[]) => Promise<any>>,
+    default: null,
+  },
+  batchDelApi: {
+    type: Function as PropType<(...arg: any[]) => Promise<any>>,
+    default: null,
+  },
+  batchExportApi: {
+    type: Function as PropType<(...arg: any[]) => Promise<any>>,
+    default: null,
+  },
+  delAuthList: {
+    type: Array as unknown as any[],
+    default: () => [],
+  },
+
+  exportAuthList: {
+    type: Array as unknown as any[],
+    default: () => [],
+  },
+  beforeFetch: {
+    type: Function as PropType<Fn>,
+    default: null,
+  },
+  afterFetch: {
+    type: Function as PropType<Fn>,
+    default: null,
+  },
+  handleSearchInfoFn: {
+    type: Function as PropType<Fn>,
+    default: null,
+  },
+  fetchSetting: {
+    type: Object as PropType<FetchSetting>,
+    default: () => {
+      return FETCH_SETTING;
+    },
+  },
+  // 立即请求接口
+  immediate: { type: Boolean, default: true },
+  emptyDataIsShowTable: { type: Boolean, default: true },
+  // 额外的请求参数
+  searchInfo: {
+    type: Object as PropType<Recordable>,
+    default: null,
+  },
+  // 默认的排序参数
+  defSort: {
+    type: Object as PropType<Recordable>,
+    default: null,
+  },
+  // 使用搜索表单
+  useSearchForm: propTypes.bool,
+  // 表单配置
+  formConfig: {
+    type: Object as PropType<Partial<FormProps>>,
+    default: null,
+  },
+  // 基本搜索表单设置
+  basicSearch: {
+    type: Object as any,
+    default: null,
+  },
+  columns: {
+    type: [Array] as PropType<BasicColumn[]>,
+    default: () => [],
+  },
+  showIndexColumn: { type: Boolean, default: true },
+  indexColumnProps: {
+    type: Object as PropType<BasicColumn>,
+    default: null,
+  },
+  actionColumn: {
+    type: Object as PropType<BasicColumn>,
+    default: null,
+  },
+  ellipsis: { type: Boolean, default: true },
+  isCanResizeParent: { type: Boolean, default: false },
+  canResize: { type: Boolean, default: true },
+  clearSelectOnPageChange: propTypes.bool,
+  resizeHeightOffset: propTypes.number.def(0),
+  rowSelection: {
+    type: Object as PropType<TableRowSelection | null>,
+    default: null,
+  },
+  title: {
+    type: [String, Function] as PropType<string | ((data: Recordable) => string)>,
+    default: null,
+  },
+  titleHelpMessage: {
+    type: [String, Array] as PropType<string | string[]>,
+  },
+  maxHeight: propTypes.number,
+  dataSource: {
+    type: Array as PropType<Recordable[]>,
+    default: null,
+  },
+  rowKey: {
+    type: [String, Function] as PropType<string | ((record: Recordable) => string)>,
+    default: '',
+  },
+  bordered: propTypes.bool,
+  pagination: {
+    type: [Object, Boolean] as PropType<PaginationProps | boolean>,
+    default: null,
+  },
+  loading: propTypes.bool,
+  rowClassName: {
+    type: Function as PropType<(record: TableCustomRecord<any>, index: number) => string>,
+  },
+  scroll: {
+    type: Object as PropType<{ x: number | true; y: number }>,
+    default: null,
+  },
+  beforeEditSubmit: {
+    type: Function as PropType<
+      (data: {
+        record: Recordable;
+        index: number;
+        key: string | number;
+        value: any;
+      }) => Promise<any>
+    >,
+  },
+  size: {
+    type: String as PropType<SizeType>,
+    default: DEFAULT_SIZE,
+  },
+  batchPrintBtn: {
+    type: Boolean,
+    default: false,
+  },
+};

+ 198 - 0
src/components/TableCard/src/types/column.ts

@@ -0,0 +1,198 @@
+import { VNodeChild } from 'vue';
+
+export interface ColumnFilterItem {
+  text?: string;
+  value?: string;
+  children?: any;
+}
+
+export declare type SortOrder = 'ascend' | 'descend';
+
+export interface RecordProps<T> {
+  text: any;
+  record: T;
+  index: number;
+}
+
+export interface FilterDropdownProps {
+  prefixCls?: string;
+  setSelectedKeys?: (selectedKeys: string[]) => void;
+  selectedKeys?: string[];
+  confirm?: () => void;
+  clearFilters?: () => void;
+  filters?: ColumnFilterItem[];
+  getPopupContainer?: (triggerNode: HTMLElement) => HTMLElement;
+  visible?: boolean;
+}
+
+export declare type CustomRenderFunction<T> = (record: RecordProps<T>) => VNodeChild | JSX.Element;
+
+export interface ColumnProps<T> {
+  /**
+   * specify how content is aligned
+   * @default 'left'
+   * @type string
+   */
+  align?: 'left' | 'right' | 'center';
+
+  /**
+   * ellipsize cell content, not working with sorter and filters for now.
+   * tableLayout would be fixed when ellipsis is true.
+   * @default false
+   * @type boolean
+   */
+  ellipsis?: boolean;
+
+  /**
+   * Span of this column's title
+   * @type number
+   */
+  colSpan?: number;
+
+  /**
+   * Display field of the data record, could be set like a.b.c
+   * @type string
+   */
+  dataIndex?: string;
+
+  /**
+   * Default filtered values
+   * @type string[]
+   */
+  defaultFilteredValue?: string[];
+
+  /**
+   * Default order of sorted values: 'ascend' 'descend' null
+   * @type string
+   */
+  defaultSortOrder?: SortOrder;
+
+  /**
+   * Customized filter overlay
+   * @type any (slot)
+   */
+  filterDropdown?:
+    | VNodeChild
+    | JSX.Element
+    | ((props: FilterDropdownProps) => VNodeChild | JSX.Element);
+
+  /**
+   * Whether filterDropdown is visible
+   * @type boolean
+   */
+  filterDropdownVisible?: boolean;
+
+  /**
+   * Whether the dataSource is filtered
+   * @default false
+   * @type boolean
+   */
+  filtered?: boolean;
+
+  /**
+   * Controlled filtered value, filter icon will highlight
+   * @type string[]
+   */
+  filteredValue?: string[];
+
+  /**
+   * Customized filter icon
+   * @default false
+   * @type any
+   */
+  filterIcon?: boolean | VNodeChild | JSX.Element;
+
+  /**
+   * Whether multiple filters can be selected
+   * @default true
+   * @type boolean
+   */
+  filterMultiple?: boolean;
+
+  /**
+   * Filter menu config
+   * @type object[]
+   */
+  filters?: ColumnFilterItem[];
+
+  /**
+   * Set column to be fixed: true(same as left) 'left' 'right'
+   * @default false
+   * @type boolean | string
+   */
+  fixed?: boolean | 'left' | 'right';
+
+  /**
+   * Unique key of this column, you can ignore this prop if you've set a unique dataIndex
+   * @type string
+   */
+  key?: string;
+
+  /**
+   * Renderer of the table cell. The return value should be a VNode, or an object for colSpan/rowSpan config
+   * @type Function | ScopedSlot
+   */
+  customRender?: CustomRenderFunction<T> | VNodeChild | JSX.Element;
+
+  /**
+   * Sort function for local sort, see Array.sort's compareFunction. If you need sort buttons only, set to true
+   * @type boolean | Function
+   */
+  sorter?: boolean | Function;
+
+  /**
+   * Order of sorted values: 'ascend' 'descend' false
+   * @type boolean | string
+   */
+  sortOrder?: boolean | SortOrder;
+
+  /**
+   * supported sort way, could be 'ascend', 'descend'
+   * @default ['ascend', 'descend']
+   * @type string[]
+   */
+  sortDirections?: SortOrder[];
+
+  /**
+   * Title of this column
+   * @type any (string | slot)
+   */
+  title?: VNodeChild | JSX.Element;
+
+  /**
+   * Width of this column
+   * @type string | number
+   */
+  width?: string | number;
+
+  /**
+   * Set props on per cell
+   * @type Function
+   */
+  customCell?: (record: T, rowIndex: number) => object;
+
+  /**
+   * Set props on per header cell
+   * @type object
+   */
+  customHeaderCell?: (column: ColumnProps<T>) => object;
+
+  /**
+   * Callback executed when the confirm filter button is clicked, Use as a filter event when using template or jsx
+   * @type Function
+   */
+  onFilter?: (value: any, record: T) => boolean;
+
+  /**
+   * Callback executed when filterDropdownVisible is changed, Use as a filterDropdownVisible event when using template or jsx
+   * @type Function
+   */
+  onFilterDropdownVisibleChange?: (visible: boolean) => void;
+
+  /**
+   * When using columns, you can setting this property to configure the properties that support the slot,
+   * such as slots: { filterIcon: 'XXX'}
+   * @type object
+   */
+  slots?: Recordable<string>;
+}

+ 14 - 0
src/components/TableCard/src/types/componentType.ts

@@ -0,0 +1,14 @@
+export type ComponentType =
+  | 'Input'
+  | 'InputNumber'
+  | 'Select'
+  | 'ApiSelect'
+  | 'AutoComplete'
+  | 'ApiTreeSelect'
+  | 'Checkbox'
+  | 'Switch'
+  | 'DatePicker'
+  | 'TimePicker'
+  | 'RadioGroup'
+  | 'RadioButtonGroup'
+  | 'ApiRadioGroup';

+ 115 - 0
src/components/TableCard/src/types/pagination.ts

@@ -0,0 +1,115 @@
+import Pagination from 'ant-design-vue/lib/pagination';
+import { VNodeChild } from 'vue';
+
+interface PaginationRenderProps {
+  page: number;
+  type: 'page' | 'prev' | 'next';
+  originalElement: any;
+}
+
+type PaginationPositon =
+  | 'topLeft'
+  | 'topCenter'
+  | 'topRight'
+  | 'bottomLeft'
+  | 'bottomCenter'
+  | 'bottomRight';
+
+export declare class PaginationConfig extends Pagination {
+  position?: PaginationPositon[];
+}
+
+export interface PaginationProps {
+  /**
+   * total number of data items
+   * @default 0
+   * @type number
+   */
+  total?: number;
+
+  /**
+   * default initial page number
+   * @default 1
+   * @type number
+   */
+  defaultCurrent?: number;
+
+  /**
+   * current page number
+   * @type number
+   */
+  current?: number;
+
+  /**
+   * default number of data items per page
+   * @default 10
+   * @type number
+   */
+  defaultPageSize?: number;
+
+  /**
+   * number of data items per page
+   * @type number
+   */
+  pageSize?: number;
+
+  /**
+   * Whether to hide pager on single page
+   * @default false
+   * @type boolean
+   */
+  hideOnSinglePage?: boolean;
+
+  /**
+   * determine whether pageSize can be changed
+   * @default false
+   * @type boolean
+   */
+  showSizeChanger?: boolean;
+
+  /**
+   * specify the sizeChanger options
+   * @default ['10', '20', '30', '40']
+   * @type string[]
+   */
+  pageSizeOptions?: string[];
+
+  /**
+   * determine whether you can jump to pages directly
+   * @default false
+   * @type boolean
+   */
+  showQuickJumper?: boolean | object;
+
+  /**
+   * to display the total number and range
+   * @type Function
+   */
+  showTotal?: (total: number, range: [number, number]) => any;
+
+  /**
+   * specify the size of Pagination, can be set to small
+   * @default ''
+   * @type string
+   */
+  size?: string;
+
+  /**
+   * whether to setting simple mode
+   * @type boolean
+   */
+  simple?: boolean;
+
+  /**
+   * to customize item innerHTML
+   * @type Function
+   */
+  itemRender?: (props: PaginationRenderProps) => VNodeChild | JSX.Element;
+
+  /**
+   * specify the position of Pagination
+   * @default ['bottomRight']
+   * @type string[]
+   */
+  position?: PaginationPositon[];
+}

+ 492 - 0
src/components/TableCard/src/types/table.ts

@@ -0,0 +1,492 @@
+import type { VNodeChild } from 'vue';
+import type { PaginationProps } from './pagination';
+import type { FormProps } from '/@/components/Form';
+import type { TableRowSelection as ITableRowSelection } from 'ant-design-vue/lib/table/interface';
+import type { ColumnProps } from 'ant-design-vue/lib/table';
+
+import { ComponentType } from './componentType';
+import { VueNode } from '/@/utils/propTypes';
+import { RoleEnum } from '/@/enums/roleEnum';
+
+export declare type SortOrder = 'ascend' | 'descend';
+
+export interface TableCurrentDataSource<T = Recordable> {
+  currentDataSource: T[];
+}
+
+export interface TableRowSelection<T = any> extends ITableRowSelection {
+  /**
+   * Callback executed when selected rows change
+   * @type Function
+   */
+  onChange?: (selectedRowKeys: string[] | number[], selectedRows: T[]) => any;
+
+  /**
+   * Callback executed when select/deselect one row
+   * @type Function
+   */
+  onSelect?: (record: T, selected: boolean, selectedRows: Object[], nativeEvent: Event) => any;
+
+  /**
+   * Callback executed when select/deselect all rows
+   * @type Function
+   */
+  onSelectAll?: (selected: boolean, selectedRows: T[], changeRows: T[]) => any;
+
+  /**
+   * Callback executed when row selection is inverted
+   * @type Function
+   */
+  onSelectInvert?: (selectedRows: string[] | number[]) => any;
+}
+
+export interface TableCustomRecord<T> {
+  record?: T;
+  index?: number;
+}
+
+export interface ExpandedRowRenderRecord<T> extends TableCustomRecord<T> {
+  indent?: number;
+  expanded?: boolean;
+}
+export interface ColumnFilterItem {
+  text?: string;
+  value?: string;
+  children?: any;
+}
+
+export interface TableCustomRecord<T = Recordable> {
+  record?: T;
+  index?: number;
+}
+
+export interface SorterResult {
+  column: ColumnProps;
+  order: SortOrder;
+  field: string;
+  columnKey: string;
+}
+
+export interface FetchParams {
+  searchInfo?: Recordable;
+  page?: number;
+  sortInfo?: Recordable;
+  filterInfo?: Recordable;
+}
+
+export interface GetColumnsParams {
+  ignoreIndex?: boolean;
+  ignoreAction?: boolean;
+  sort?: boolean;
+}
+
+export type SizeType = 'default' | 'middle' | 'small' | 'large';
+
+export interface TableActionType {
+  reload: (opt?: FetchParams) => Promise<void>;
+  setSelectedRows: (rows: Recordable[]) => void;
+  getSelectRows: <T = Recordable>() => T[];
+  clearSelectedRowKeys: () => void;
+  expandAll: () => void;
+  expandRows: (keys: string[] | number[]) => void;
+  collapseAll: () => void;
+  scrollTo: (pos: string) => void; // pos: id | "top" | "bottom"
+  getSelectRowKeys: () => string[];
+  deleteSelectRowByKey: (key: string) => void;
+  setPagination: (info: Partial<PaginationProps>) => void;
+  setTableData: <T = Recordable>(values: T[]) => void;
+  updateTableDataRecord: (rowKey: string | number, record: Recordable) => Recordable | void;
+  deleteTableDataRecord: (rowKey: string | number | string[] | number[]) => void;
+  insertTableDataRecord: (record: Recordable, index?: number) => Recordable | void;
+  findTableDataRecord: (rowKey: string | number) => Recordable | void;
+  getColumns: (opt?: GetColumnsParams) => BasicColumn[];
+  setColumns: (columns: BasicColumn[] | string[]) => void;
+  getDataSource: <T = Recordable>() => T[];
+  getRawDataSource: <T = Recordable>() => T;
+  setLoading: (loading: boolean) => void;
+  setProps: (props: Partial<BasicTableProps>) => void;
+  redoHeight: () => void;
+  setSelectedRowKeys: (rowKeys: string[] | number[]) => void;
+  getPaginationRef: () => PaginationProps | boolean;
+  getSize: () => SizeType;
+  getRowSelection: () => TableRowSelection<Recordable>;
+  getCacheColumns: () => BasicColumn[];
+  emit?: EmitType;
+  updateTableData: (index: number, key: string, value: any) => Recordable;
+  setShowPagination: (show: boolean) => Promise<void>;
+  getShowPagination: () => boolean;
+  setCacheColumnsByField?: (dataIndex: string | undefined, value: BasicColumn) => void;
+}
+
+export interface FetchSetting {
+  // 请求接口当前页数
+  pageField: string;
+  // 每页显示多少条
+  sizeField: string;
+  // 请求结果列表字段  支持 a.b.c
+  listField: string;
+  // 请求结果总数字段  支持 a.b.c
+  totalField: string;
+}
+
+export interface TableSetting {
+  redo?: boolean;
+  size?: boolean;
+  setting?: boolean;
+  fullScreen?: boolean;
+}
+
+export interface BasicTableProps<T = any> {
+  // table 唯一id
+  id?: string;
+  // 是否存储在 localstorage
+  storage: boolean;
+  // 基本搜索渲染,然后放入 searchInfo 参数中
+  basicSearch: Recordable;
+  // 点击行选中
+  clickToRowSelect?: boolean;
+  isTreeTable?: boolean;
+  // 自定义排序方法
+  sortFn?: (sortInfo: SorterResult) => any;
+  // 排序方法
+  filterFn?: (data: Partial<Recordable<string[]>>) => any;
+  // 取消表格的默认padding
+  inset?: boolean;
+  // 显示表格设置
+  showTableSetting?: boolean;
+  tableSetting?: TableSetting;
+  // 斑马纹
+  striped?: boolean;
+  // 是否自动生成key
+  autoCreateKey?: boolean;
+  // 计算合计行的方法
+  summaryFunc?: (...arg: any) => Recordable[];
+  // 自定义合计表格内容
+  summaryData?: Recordable[];
+  // 是否显示合计行
+  showSummary?: boolean;
+  // 是否可拖拽列
+  canColDrag?: boolean;
+  // 接口请求对象
+  api?: (...arg: any) => Promise<any>;
+  //批量删除接口请求对象
+  batchDelApi?: (...arg: any) => Promise<any>;
+  //批量导出接口请求对象
+  batchExportApi?: (...arg: any) => Promise<any>;
+  //设置权限字段
+  delAuthList?: any[];
+  exportAuthList?: any[];
+  // 请求之前处理参数
+  beforeFetch?: Fn;
+  // 自定义处理接口返回参数
+  afterFetch?: Fn;
+  // 查询条件请求之前处理
+  handleSearchInfoFn?: Fn;
+  // 请求接口配置
+  fetchSetting?: Partial<FetchSetting>;
+  // 立即请求接口
+  immediate?: boolean;
+  // 在开起搜索表单的时候,如果没有数据是否显示表格
+  emptyDataIsShowTable?: boolean;
+  // 额外的请求参数
+  searchInfo?: Recordable;
+  // 默认的排序参数
+  defSort?: Recordable;
+  // 使用搜索表单
+  useSearchForm?: boolean;
+  // 表单配置
+  formConfig?: Partial<FormProps>;
+  // 列配置
+  columns: BasicColumn[];
+  // 是否显示序号列
+  showIndexColumn?: boolean;
+  // 序号列配置
+  indexColumnProps?: BasicColumn;
+  actionColumn?: BasicColumn;
+  // 文本超过宽度是否显示。。。
+  ellipsis?: boolean;
+  // 是否继承父级高度(父级高度-表单高度-padding高度)
+  isCanResizeParent?: boolean;
+  // 是否可以自适应高度
+  canResize?: boolean;
+  // 自适应高度偏移, 计算结果-偏移量
+  resizeHeightOffset?: number;
+
+  // 在分页改变的时候清空选项
+  clearSelectOnPageChange?: boolean;
+  //
+  rowKey?: string | ((record: Recordable) => string);
+  // 数据
+  dataSource?: Recordable[];
+  // 标题右侧提示
+  titleHelpMessage?: string | string[];
+  // 表格滚动最大高度
+  maxHeight?: number;
+  // 是否显示边框
+  bordered?: boolean;
+  // 分页配置
+  pagination?: PaginationProps | boolean;
+  // loading加载
+  loading?: boolean;
+
+  /**
+   * The column contains children to display
+   * @default 'children'
+   * @type string | string[]
+   */
+  childrenColumnName?: string;
+
+  /**
+   * Override default table elements
+   * @type object
+   */
+  components?: object;
+
+  /**
+   * Expand all rows initially
+   * @default false
+   * @type boolean
+   */
+  defaultExpandAllRows?: boolean;
+
+  /**
+   * Initial expanded row keys
+   * @type string[]
+   */
+  defaultExpandedRowKeys?: string[];
+
+  /**
+   * Current expanded row keys
+   * @type string[]
+   */
+  expandedRowKeys?: string[];
+
+  /**
+   * Expanded container render for each row
+   * @type Function
+   */
+  expandedRowRender?: (record?: ExpandedRowRenderRecord<T>) => VNodeChild | JSX.Element;
+
+  /**
+   * Customize row expand Icon.
+   * @type Function | VNodeChild
+   */
+  expandIcon?: Function | VNodeChild | JSX.Element;
+
+  /**
+   * Whether to expand row by clicking anywhere in the whole row
+   * @default false
+   * @type boolean
+   */
+  expandRowByClick?: boolean;
+
+  /**
+   * The index of `expandIcon` which column will be inserted when `expandIconAsCell` is false. default 0
+   */
+  expandIconColumnIndex?: number;
+
+  /**
+   * Table footer renderer
+   * @type Function | VNodeChild
+   */
+  footer?: Function | VNodeChild | JSX.Element;
+
+  /**
+   * Indent size in pixels of tree data
+   * @default 15
+   * @type number
+   */
+  indentSize?: number;
+
+  /**
+   * i18n text including filter, sort, empty text, etc
+   * @default { filterConfirm: 'Ok', filterReset: 'Reset', emptyText: 'No Data' }
+   * @type object
+   */
+  locale?: object;
+
+  /**
+   * Row's className
+   * @type Function
+   */
+  rowClassName?: (record: TableCustomRecord<T>, index: number) => string;
+
+  /**
+   * Row selection config
+   * @type object
+   */
+  rowSelection?: TableRowSelection;
+
+  /**
+   * Set horizontal or vertical scrolling, can also be used to specify the width and height of the scroll area.
+   * It is recommended to set a number for x, if you want to set it to true,
+   * you need to add style .ant-table td { white-space: nowrap; }.
+   * @type object
+   */
+  scroll?: { x?: number | true; y?: number };
+
+  /**
+   * Whether to show table header
+   * @default true
+   * @type boolean
+   */
+  showHeader?: boolean;
+
+  /**
+   * Size of table
+   * @default 'default'
+   * @type string
+   */
+  size?: SizeType;
+
+  /**
+   * Table title renderer
+   * @type Function | ScopedSlot
+   */
+  title?: VNodeChild | JSX.Element | string | ((data: Recordable) => string);
+
+  /**
+   * Set props on per header row
+   * @type Function
+   */
+  customHeaderRow?: (column: ColumnProps, index: number) => object;
+
+  /**
+   * Set props on per row
+   * @type Function
+   */
+  customRow?: (record: T, index: number) => object;
+
+  /**
+   * `table-layout` attribute of table element
+   * `fixed` when header/columns are fixed, or using `column.ellipsis`
+   *
+   * @see https://developer.mozilla.org/en-US/docs/Web/CSS/table-layout
+   * @version 1.5.0
+   */
+  tableLayout?: 'auto' | 'fixed' | string;
+
+  /**
+   * the render container of dropdowns in table
+   * @param triggerNode
+   * @version 1.5.0
+   */
+  getPopupContainer?: (triggerNode?: HTMLElement) => HTMLElement;
+
+  /**
+   * Data can be changed again before rendering.
+   * The default configuration of general user empty data.
+   * You can configured globally through [ConfigProvider](https://antdv.com/components/config-provider-cn/)
+   *
+   * @version 1.5.4
+   */
+  transformCellText?: Function;
+
+  /**
+   * Callback executed before editable cell submit value, not for row-editor
+   *
+   * The cell will not submit data while callback return false
+   */
+  beforeEditSubmit?: (data: {
+    record: Recordable;
+    index: number;
+    key: string | number;
+    value: any;
+  }) => Promise<any>;
+
+  /**
+   * Callback executed when pagination, filters or sorter is changed
+   * @param pagination
+   * @param filters
+   * @param sorter
+   * @param currentDataSource
+   */
+  onChange?: (pagination: any, filters: any, sorter: any, extra: any) => void;
+
+  /**
+   * Callback executed when the row expand icon is clicked
+   *
+   * @param expanded
+   * @param record
+   */
+  onExpand?: (expande: boolean, record: T) => void;
+
+  /**
+   * Callback executed when the expanded rows change
+   * @param expandedRows
+   */
+  onExpandedRowsChange?: (expandedRows: string[] | number[]) => void;
+
+  onColumnsChange?: (data: ColumnChangeParam[]) => void;
+}
+
+export type CellFormat =
+  | string
+  | ((text: string, record: Recordable, index: number) => string | number)
+  | Map<string | number, any>;
+
+// @ts-ignore
+export interface BasicColumn extends ColumnProps<Recordable> {
+  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?: Recordable;
+
+  // 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?:
+    | ((opt: {
+        text: string | number | boolean | Recordable;
+        record: Recordable;
+        column: BasicColumn;
+        index: number;
+      }) => Recordable)
+    | Recordable;
+  editRule?: boolean | ((text: string, record: Recordable) => Promise<string>);
+  editValueMap?: (value: any) => string;
+  onEditRow?: () => void;
+  // 权限编码控制是否显示
+  auth?: RoleEnum | RoleEnum[] | string | string[];
+  // 业务控制是否显示
+  ifShow?: boolean | ((column: BasicColumn) => boolean);
+  // 自定义修改后显示的内容
+  editRender?: (opt: {
+    text: string | number | boolean | Recordable;
+    record: Recordable;
+    column: BasicColumn;
+    index: number;
+  }) => VNodeChild | JSX.Element;
+  // 动态 Disabled
+  editDynamicDisabled?: boolean | ((record: Recordable) => boolean);
+}
+
+export type ColumnChangeParam = {
+  dataIndex: string;
+  fixed: boolean | 'left' | 'right' | undefined;
+  visible: boolean;
+};
+
+export interface InnerHandlers {
+  onColumnsChange: (data: ColumnChangeParam[]) => void;
+}

+ 39 - 0
src/components/TableCard/src/types/tableAction.ts

@@ -0,0 +1,39 @@
+import { ButtonProps } from 'ant-design-vue/es/button/buttonTypes';
+import { TooltipProps } from 'ant-design-vue/es/tooltip/Tooltip';
+import { RoleEnum } from '/@/enums/roleEnum';
+export interface ActionItem extends ButtonProps {
+  onClick?: Fn;
+  label?: string;
+  color?: 'success' | 'error' | 'warning';
+  icon?: string;
+  popConfirm?: PopConfirm;
+  disabled?: boolean;
+  divider?: boolean;
+  // 权限编码控制是否显示
+  auth?: RoleEnum | RoleEnum[] | string | string[];
+  // 业务控制是否显示
+  ifShow?: boolean | ((action: ActionItem) => boolean);
+  tooltip?: string | TooltipProps;
+}
+
+export interface PopConfirm {
+  title: string;
+  okText?: string;
+  cancelText?: string;
+  confirm: Fn;
+  cancel?: Fn;
+  icon?: string;
+  placement?:
+    | 'top'
+    | 'left'
+    | 'right'
+    | 'bottom'
+    | 'topLeft'
+    | 'topRight'
+    | 'leftTop'
+    | 'leftBottom'
+    | 'rightTop'
+    | 'rightBottom'
+    | 'bottomLeft'
+    | 'bottomRight';
+}

+ 21 - 0
src/components/XTForm/src/XTForm.vue

@@ -54,6 +54,20 @@
               :disabled="item.disabled ? true : false"
               :style="{ width: item.width + 'px' }"
               :defaultValue="item.defaultValue"
+              :disabled-date="item.disabledDate"
+              @change="handleChange"
+              :size="item.size || 'default'"
+              :picker="item.picker"
+            />
+          </div>
+          <div v-if="item.componentType == ComponentEnum.RangePicker">
+            <RangePicker
+              v-model:value="formRef[item.name]"
+              :show-time="false"
+              :format="item.format || 'YYYY-MM-DD HH:mm:ss'"
+              :value-format="item.valueFormat || 'YYYY-MM-DD HH:mm:ss'"
+              :disabled="item.disabled ? true : false"
+              :style="{ width: item.width + 'px' }"
               @change="handleChange"
               :size="item.size || 'default'"
             />
@@ -89,6 +103,7 @@
     // Checkbox,
     // CheckboxGroup,
     DatePicker,
+    RangePicker,
     // Textarea,
     // message,
     // Tooltip,
@@ -113,6 +128,8 @@
       }>;
       disabled?: boolean;
       defaultValue?: string;
+      disabledDate?: any;
+      picker?: string;
       size?: string;
     }>;
   }
@@ -169,6 +186,10 @@
     align-items: center;
   }
 
+  ::v-deep(.ant-form-item) {
+    margin-right: 0;
+  }
+
   .xt-from_label {
     margin-right: 10px;
   }

+ 2 - 0
src/components/XTForm/src/componentEnum.ts

@@ -7,6 +7,8 @@ export enum ComponentEnum {
   Input = 'Input',
   // DatePicker 日期选择框
   DatePicker = 'DatePicker',
+  // DatePicker 日期选择框
+  RangePicker = 'RangePicker',
   // InputNumber 数字输入框
   InputNumber = 'InputNumber',
   // Radio 单选框

+ 4 - 0
src/components/XTList/index.ts

@@ -0,0 +1,4 @@
+import List from './src/List.vue';
+import Menu from './src/Menu.vue';
+
+export { List, Menu };

+ 26 - 1
src/design/index.less

@@ -81,6 +81,31 @@ span {
 .color-primary {
   color: @primary-color;
 }
+
+.color--warning {
+  background: #fff6e7;
+  color: #f90;
+}
+
+.color--error {
+  background: #ffeee3;
+  color: #ff5d39;
+}
+
+.color--success {
+  color: #19be6b;
+  background: #ecf8f2;
+}
+
+.color--primary {
+  color: #0075ff;
+  background: #e5f1ff;
+}
+
+.color--muted {
+  color: #828890;
+  background: #e1e3e7;
+}
 @media print {
   @page {
     size: auto;
@@ -156,5 +181,5 @@ span {
 }
 
 .ant-back-top-icon {
-  line-height: 40px;
+  // line-height: 40px;
 }

+ 2 - 1
src/layouts/default/feature/index.vue

@@ -62,9 +62,10 @@
 
 <style lang="less">
   @prefix-cls: ~'@{namespace}-setting-drawer-feature';
+
   .ant-back-top-icon {
     font-size: 24px;
-    line-height: 2.2;
+    // line-height: 2.2;
   }
   .@{prefix-cls} {
     position: absolute;

+ 0 - 1
src/layouts/default/header/index.less

@@ -13,7 +13,6 @@
   background-color: @white;
   align-items: center;
   justify-content: space-between;
-      width: calc(100vw - 190px);
 
   &--mobile {
     .@{breadcrumb-prefix-cls},

+ 21 - 0
src/utils/validate.ts

@@ -0,0 +1,21 @@
+// 邮箱验证
+export async function validateEmail(val: string) {
+  const reg = /^([a-zA-Z]|[0-9])(\w|\-)+@[a-zA-Z0-9]+\.([a-zA-Z]{2,4})$/;
+  return !reg.test(val) ? true : false;
+}
+
+/**
+ 验证 4至16位的字母+数字 
+ */
+export function validateUserName(value) {
+  const reg = /^[a-zA-Z0-9]{4,16}$/;
+  return !reg.test(value) ? true : false;
+}
+
+/**
+ 验证字母+数字 
+ */
+export function validateStr(value) {
+  const reg = /^[A-Za-z0-9]+$/;
+  return !reg.test(value) ? true : false;
+}

+ 15 - 0
src/views/biz/README.md

@@ -0,0 +1,15 @@
+## 文件夹情况说明
+
+- biz
+  - visit 透前准备
+    - check 交叉核对
+    - ready 透前准备
+    - room 透析室
+    - rounds 查房
+    - transfer 交班记录
+  - archives 透析病历
+  - bed 预约排床
+    - near 排床
+    - long 长期排床模板
+    - memo 排床备忘录
+    - person 个人排班

+ 7 - 0
src/views/biz/archives/index.vue

@@ -0,0 +1,7 @@
+<template>
+  <div> 占位符 </div>
+</template>
+
+<script setup lang="ts"></script>
+
+<style lang="less" scoped></style>

+ 7 - 0
src/views/biz/bed/long/index.vue

@@ -0,0 +1,7 @@
+<template>
+  <div> 占位符 </div>
+</template>
+
+<script setup lang="ts"></script>
+
+<style lang="less" scoped></style>

+ 7 - 0
src/views/biz/bed/memo/index.vue

@@ -0,0 +1,7 @@
+<template>
+  <div> 占位符 </div>
+</template>
+
+<script setup lang="ts"></script>
+
+<style lang="less" scoped></style>

+ 91 - 0
src/views/biz/bed/near/data.ts

@@ -0,0 +1,91 @@
+import dayjs from 'dayjs';
+import { nanoid } from 'nanoid';
+import isSameOrBefore from 'dayjs/plugin/isSameOrBefore';
+dayjs.extend(isSameOrBefore);
+const WEEKINFO = {
+  Mo: '星期一',
+  Tu: '星期二',
+  We: '星期三',
+  Th: '星期四',
+  Fr: '星期五',
+  Sa: '星期六',
+};
+
+export const COUNT = ['空班', '第一班', '第二班', '第三班', '第四班'];
+// 默认头部
+export function defaultHead() {
+  const res = [];
+  for (let i = 1; i < 7; i++) {
+    const date = dayjs().day(i).format('YYYY-MM-DD');
+    res.push({
+      monthDay: dayjs(date).format('MM-DD'),
+      week: WEEKINFO[dayjs(date).format('dd')],
+      current: dayjs().isSame(date, 'day'),
+      // 班次
+      count: ['第一班', '第二班', '第三班'],
+    });
+  }
+  return res;
+}
+const defaultBedCount = 55;
+// 默认床位
+export function defaultBed() {
+  const res = [];
+  for (let i = 0; i < defaultBedCount; i++) {
+    res.push({
+      bed: i < 10 ? '0' + i : '' + i,
+      regionName: 'A区',
+      regionCode: 'A',
+      robot: '4008s',
+      // 双机位
+      double: i % 2 == 0 ? true : false,
+    });
+  }
+  return res;
+}
+
+// 默认数据
+export function defaultData() {
+  const res = [];
+  for (let i = 0; i < defaultBedCount; i++) {
+    const obj = {
+      id: nanoid(5),
+      bed: i < 10 ? '0' + i : '' + i,
+      regionCode: 'A',
+      sailings: [],
+    };
+
+    for (let w = 1; w < 7; w++) {
+      const week = [];
+      const date = dayjs().day(w).format('YYYY-MM-DD');
+      // console.log(dayjs().isAfter(date, 'day'));
+      // console.log('date', date);
+      for (let s = 0; s < 3; s++) {
+        const bool = Math.round(Math.random() * 100) > 50;
+        week.push({
+          id: nanoid(5),
+          key: nanoid(8),
+          name: bool ? '刘' + nanoid(3) : '',
+          date: date,
+          old: dayjs().isSame(date, 'day') ? false : dayjs().isAfter(date, 'day'),
+          show: true,
+          empty: !bool,
+          edit: false,
+          error: false,
+          selected: false,
+          actived: false,
+          disabled: false,
+          // 床位
+          x: i,
+          // 时间
+          y: w,
+          // 具体班次
+          z: s,
+        });
+      }
+      obj.sailings.push(week);
+    }
+    res.push(obj);
+  }
+  return res;
+}

+ 801 - 0
src/views/biz/bed/near/index.vue

@@ -0,0 +1,801 @@
+<template>
+  <div>
+    <div class="mx-2 my-4 wrap" :style="{ maxWidth: isEdit ? 'calc(100vw - 400px)' : '' }">
+      <div class="flex">
+        <h1>排床</h1>
+        <div>
+          <a-button @click="handleEdit">编辑</a-button>
+        </div>
+      </div>
+      <div class="my-4 filter">
+        <div>
+          <XTForm :form-data="formData" @change="callFilter" />
+        </div>
+      </div>
+      <div class="detail">
+        <div class="aside">
+          <div class="aside-item aside-item--tit">床位</div>
+          <div class="mb-2 aside-item" v-for="b in bed" :key="b.bed">
+            <div class="aside-item_bed">{{ b.bed }}</div>
+            <div class="aside-item_robot">
+              <i class="iconfont icon-xt-dual-pump_default" v-if="b.double" />
+              <span>{{ b.robot }}</span>
+            </div>
+          </div>
+        </div>
+        <div class="content" ref="scrollEl">
+          <div>
+            <div
+              ref="headEl"
+              :class="[
+                'mb-2 content-head',
+                Number(refY) > 100 ? 'content-head--fixed' : '',
+                isEdit && Number(refY) > 100 ? 'content-head--edit' : '',
+              ]"
+            >
+              <div
+                class="head-item animate__animated animate__slideInLeft"
+                v-for="h in head"
+                :key="h.monthDay"
+              >
+                <div :class="['head-item_time', h.current ? 'head-item_time--current' : '']">
+                  <span class="head-item_time-week">{{ h.week }}</span>
+                  <span class="head-item_time-day">({{ h.monthDay }})</span>
+                </div>
+                <div class="head-item_sailings">
+                  <div
+                    :class="[
+                      'head-item_sailings-item',
+                      filter.sailings ? 'head-item_sailings-item--only' : '',
+                    ]"
+                    v-for="c in h.count"
+                    :key="c"
+                  >
+                    {{ c }}
+                  </div>
+                </div>
+              </div>
+            </div>
+          </div>
+
+          <div :class="['content-body', Number(refY) > 100 ? 'content-body--fixed' : '']">
+            <div class="flex mb-2" v-for="(c, cIdx) in cnt" :key="c.id">
+              <div class="flex items-center" v-if="c.bed == bed[cIdx].bed">
+                <div class="body-list" v-for="(sa, idx) in c.sailings" :key="idx">
+                  <div
+                    v-for="person in sa"
+                    :key="person.key"
+                    :class="[
+                      'animate__animated animate__slideInLeft',
+                      'body-list_item',
+                      person.show ? '' : 'body-list_item--hidden',
+                      filter.sailings && person.show ? 'body-list_item--only' : '',
+                      setClass(person),
+                    ]"
+                    @click="handlePerson(person)"
+                    @dblclick="handleDbPerson(person)"
+                  >
+                    <!-- <div>testt</div> -->
+                    <div>{{ person.name }} </div>
+                  </div>
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+    <div class="edit animate__animated animate__slideInRight" v-if="isEdit">
+      <!-- {{ refX }} {{ refY }}
+      {{ headX }}
+      {{ Number(refX) > 100 }} -->
+      <div class="edit-tit"> 本周排床编辑 </div>
+      <div class="edit-cnt">
+        <div class="edit-cnt_tit">未排床</div>
+        <div class="edit-cnt_detail">
+          <div
+            v-for="item in bedNot"
+            :key="item.id"
+            :class="['edit-item', item.id == notBedId ? 'edit-item--not' : '']"
+            @click="handleNotBed(item)"
+          >
+            <div> {{ item.name }}</div>
+            <div class="edit-item_count"> {{ item.count }}次</div>
+          </div>
+        </div>
+      </div>
+      <div class="edit-cnt">
+        <div class="edit-cnt_tit">已排床</div>
+        <div class="mb-2">
+          <Select
+            show-search
+            allow-clear
+            class="w-full"
+            @search="searchBed"
+            @change="changeBed"
+            placelholder="搜索需要排班的患者"
+            :options="bedNot"
+            v-model:value="searchBedField"
+          >
+            <template #suffixIcon>
+              <i class="iconfont icon-xt-search ant-select-suffix" />
+            </template>
+          </Select>
+        </div>
+        <div class="edit-cnt_detail">
+          <div
+            v-for="item in bedHas"
+            :key="item.id"
+            :class="['edit-item', item.id == notBedId ? 'edit-item--has' : '']"
+            @click="handleNotBed(item)"
+          >
+            {{ item.name }}
+          </div>
+        </div>
+      </div>
+      <div class="flex items-center edit-foot">
+        <a-button class="edit-foot_btn" type="default" @click="handleReset">取消</a-button>
+        <a-button class="edit-foot_btn" disabled type="primary" @click="handleSubmit"
+          >保存</a-button
+        >
+      </div>
+      <!-- <a-button @click="scrollX += 10"> Scroll right 10px </a-button> -->
+      <!-- <a-button @click="scrollY += 10"> Scroll down 10px </a-button> -->
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { computed, ref, onMounted, watch } from 'vue';
+  import dayjs from 'dayjs';
+  import { defaultHead, defaultBed, defaultData } from './data';
+  import { useMessage } from '/@/hooks/web/useMessage';
+  import { XTForm } from '/@/components/XTForm';
+  import _ from 'lodash-es';
+  import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
+  // import { useScroll } from '/@/hooks/event/useScroll';
+  import { useScroll, useThrottleFn } from '@vueuse/core';
+  import { Select } from 'ant-design-vue';
+
+  const { getCollapsed, toggleCollapsed } = useMenuSetting();
+  onMounted(() => {
+    if (getCollapsed) {
+      toggleCollapsed();
+    }
+    // 滚动到指定位置
+    scrollTo(0, 1);
+    // const scrollId = document.querySelector('.content');
+    // console.log('🚀 ~ file: index.vue:133 ~ onMounted ~ scrollId:', scrollId);
+    // scrollId.addEventListener('scroll', function (e) {
+    //   console.log('e', e.target);
+    // });
+  });
+  // console.log('🚀 ~ file: index.vue:51 ~ defaultHead:', defaultHead());
+  // console.log('🚀 ~ file: index.vue:51 ~ defaultBed:', defaultBed());
+  // console.log('🚀 ~ file: index.vue:51 ~ defaultData:', defaultData());
+  // console.log('🚀 ~ file: index.vue:51 ~ dayjs:', dayjs().day());
+  // console.log('🚀 ~ file: index.vue:51 ~ dayjs:', defaultHead());
+  // console.log('🚀 ~ file: index.vue:51 ~ dayjs:', dayjs().day(1).format('MM-DD'));
+  const { createMessage } = useMessage();
+  const head = ref(defaultHead());
+  const bed = defaultBed();
+  const cnt = ref(defaultData());
+  const notBedId = ref('');
+  const hasBedId = ref('');
+  const filter = ref({}) as any;
+  const isEdit = ref(false);
+  const searchBedField = ref('');
+  const customWeekStartEndFormat = value =>
+    `${dayjs(value).startOf('week').format('YYYY-MM-DD')} ~ ${dayjs(value)
+      .endOf('week')
+      .format('YYYY-MM-DD')}`;
+  const disabledDate = (current: any) => {
+    // Can not select days before today and today
+    return current && current > dayjs().endOf('month');
+  };
+  // scroll
+  const scrollEl = ref(null);
+  const headEl = ref(null);
+  const { x: scrollX } = useScroll(scrollEl, { behavior: 'smooth' });
+  const { y: scrollY } = useScroll(document.body, { behavior: 'smooth' });
+  const { x: headX } = useScroll(headEl, { behavior: 'smooth' });
+  const refY = computed({
+    get() {
+      return scrollY.value.toFixed(1);
+    },
+    set(val) {
+      scrollY.value = parseFloat(val);
+    },
+  });
+  // 滚动到指定位置
+  function scrollTo(x, y) {
+    console.log('🚀 ~ file: index.vue:177 ~ scrollTo ~ x:', x);
+    // scrollX.value = (x - 1) * 54 * 3;
+    scrollX.value = x > 3 ? 107 : 0;
+    if (isEdit.value && scrollY.value > 100) {
+      headX.value = x > 3 ? 107 : 0;
+    }
+    if (y == null) return;
+    scrollY.value = (y - 1) * 54 + (y - 2) * 10 + (y > 10 ? 90 : 110);
+  }
+  // 监听编辑状态下的页面竖向滚动
+  watch(
+    () => scrollY.value,
+    () => {
+      scrollThrott();
+    },
+  );
+  // 节流操作
+  const scrollThrott = useThrottleFn(() => {
+    if (isEdit.value && scrollY.value > 100) {
+      headX.value = scrollX.value > 100 ? 107 : 0;
+    }
+  }, 500);
+
+  // const refX = computed(() => {
+  //   console.log('x', scrollX.value);
+  //   return scrollX.value.toFixed(1);
+  // });
+
+  // const { x, y } = useScroll(document.body);
+
+  // scroll
+  const bedNot = [
+    {
+      id: 'n1',
+      name: '王雪(女|38)',
+      label: '王雪(女|38)',
+      count: 1,
+    },
+    {
+      id: 'n2',
+      name: '测试456456',
+      label: '测试456456',
+      count: 1,
+    },
+    {
+      id: 'n3',
+      name: '测试456456',
+      label: '测试123123',
+      count: 1,
+    },
+    {
+      id: 'n4',
+      name: '测试456456',
+      label: '测试456456',
+      count: 3,
+    },
+    {
+      id: 'n5',
+      name: '测试456456',
+      label: '测试123123',
+      count: 1,
+    },
+    {
+      id: 'n6',
+      name: '测试456456',
+      label: '测试456456',
+      count: 2,
+    },
+    {
+      id: 'n123123',
+      name: '测试456456',
+      label: '测试123123',
+      count: 1,
+    },
+    {
+      id: 'n456456',
+      name: '测试456456',
+      label: '测试456456',
+    },
+  ];
+  const bedHas = [
+    {
+      id: 'h123123',
+      name: '测试123123',
+    },
+    {
+      id: 'h456456',
+      name: '测试456456',
+    },
+  ];
+  // formdata
+  const formData = [
+    {
+      name: 'text',
+      // label: '全部',
+      componentType: 'Select',
+      placeholder: '请选择',
+      width: 120,
+      defaultValue: '0',
+      dicts: [
+        { label: '空床优先', value: '0' },
+        { label: '未称量', value: '1' },
+      ],
+    },
+    {
+      name: 'date',
+      componentType: 'DatePicker',
+      placeholder: '请输入',
+      format: customWeekStartEndFormat,
+      disabledDate: disabledDate,
+      valueFormat: 'YYYY-MM-DD',
+      picker: 'week',
+      width: 300,
+    },
+    {
+      name: 'sailings',
+      componentType: 'Select',
+      placeholder: '请选择',
+      width: 120,
+      defaultValue: 0,
+      // label: '班次',
+      dicts: [
+        { label: '全部班次', value: 0 },
+        { label: '第一班', value: 1 },
+        { label: '第二班', value: 2 },
+        { label: '第三班', value: 3 },
+      ],
+    },
+  ];
+
+  function handleNotBed(data) {
+    notBedId.value = data.id;
+    hasBedId.value = null;
+    setCnt();
+  }
+  function callFilter(data) {
+    console.log('🚀 ~ file: index.vue:173 ~ callFilter ~ data:', data);
+    if (data.sailings || data.sailings == 0) {
+      console.log('修改班次', data.sailings);
+      filter.value.sailings = data.sailings;
+      setCnt(1);
+      head.value = head.value.map(ele => {
+        const count = ['第一班', '第二班', '第三班'];
+        ele.count = data.sailings == 0 ? count : _.slice(count, data.sailings - 1, data.sailings);
+        return ele;
+      });
+      console.log(
+        '🚀 ~ file: index.vue:97 ~ handleNotBed ~ cnt.value :',
+        cnt.value[0]['sailings'][0],
+      );
+    }
+    if (data.date) {
+      filter.value.date = customWeekStartEndFormat(data.date).split('~');
+      console.log('🚀 ~ file: index.vue:214 ~ callFilter ~ filter.value.date:', filter.value.date);
+    }
+  }
+  // 设置数据
+  function setCnt(type?: number) {
+    cnt.value = cnt.value.map(ele => {
+      ele.sailings = ele.sailings.map(cele => {
+        return cele.map((sele, idx) => {
+          if (type == 1) {
+            if (filter.value.sailings == 0) {
+              sele.show = true;
+              return sele;
+            }
+            if (filter.value.sailings != 0 && filter.value.sailings - 1 == idx) {
+              sele.show = true;
+              return sele;
+            } else {
+              sele.show = false;
+              return sele;
+            }
+          } else {
+            sele.edit = true;
+            if (sele.old) {
+              sele.disabled = true;
+            } else {
+              sele.disabled = sele.name && !sele.empty ? true : false;
+            }
+            return sele;
+          }
+        });
+      });
+      return ele;
+    });
+  }
+  // 设置 class
+  function setClass(data) {
+    if (data.disabled) {
+      return 'body-list_item--disabled';
+    } else if (data.empty && data.selected && data.edit) {
+      return 'body-list_item--selected';
+    } else if (data.empty && data.actived && data.edit) {
+      return 'body-list_item--actived';
+    } else if (!data.disabled && data.empty && data.edit) {
+      return 'body-list_item--select';
+    } else if (data.old) {
+      return 'body-list_item--old';
+    } else if (data.empty && data.actived) {
+      return 'body-list_item--actived';
+    } else if (!data.name && data.empty) {
+      return 'body-list_item--empty';
+    }
+  }
+  // 双击查看详情
+  function handleDbPerson(data) {
+    console.log('🚀 ~ file: index.vue:136 ~ data:', data);
+    createMessage.info(
+      `查看详情: 床位: ${data.x}, 时间: ${data.y}, 班次: ${data.z + 1}, ${
+        data.empty ? '空床' : data.name
+      }${notBedId.value ? ', 未排床: ' + notBedId.value : ''}`,
+    );
+    console.log('cnt.value', cnt.value);
+  }
+  // 单击选中
+  function handlePerson(data) {
+    console.log('🚀 ~ file: index.vue:336 ~ handlePerson ~ data:', data);
+    if (data.disabled) return;
+    if (notBedId.value) {
+      cnt.value[data.x]['sailings'][data.y - 1][data.z]['selected'] = true;
+    }
+    if (hasBedId.value) {
+      cnt.value[data.x]['sailings'][data.y - 1][data.z]['actived'] = true;
+    }
+    scrollTo(data.y, null);
+    createMessage.info(
+      `床位: ${data.x}, 时间: ${data.y}, 班次: ${data.z + 1}, ${data.empty ? '空床' : data.name}${
+        notBedId.value ? ', 未排床: ' + notBedId.value : ''
+      }`,
+    );
+  }
+  // 取消
+  function handleReset() {
+    notBedId.value = null;
+    hasBedId.value = null;
+    cnt.value = defaultData();
+    handleEdit();
+  }
+  // 提交
+  function handleSubmit() {
+    createMessage.success('提交成功');
+  }
+  // 编辑状态
+  function handleEdit() {
+    isEdit.value = !isEdit.value;
+    if (!getCollapsed.value || !isEdit.value) {
+      toggleCollapsed();
+    }
+  }
+  function searchBed(val) {
+    console.log('🚀 ~ file: index.vue:443 ~ searchBed ~ val:', val);
+  }
+  function changeBed(val) {
+    console.log('🚀 ~ file: index.vue:443 ~ changeBed ~ val:', val);
+  }
+</script>
+
+<style lang="less" scoped>
+  // .wrap {
+  // }
+
+  .detail {
+    display: flex;
+  }
+
+  .aside {
+    &-item {
+      display: flex;
+      flex-direction: column;
+      justify-content: center;
+      align-items: center;
+      width: 82px;
+      height: 54px;
+      border-radius: 4px 0 0 4px;
+
+      &--tit {
+        text-align: center;
+        font-size: 14px;
+        font-weight: 400;
+        color: #818694;
+        margin-bottom: 0;
+      }
+
+      &_bed {
+        font-size: 14px;
+        font-weight: 600;
+        color: #000a18;
+      }
+
+      &_robot {
+        display: flex;
+        font-size: 12px;
+        font-weight: 400;
+        color: #818694;
+        align-items: center;
+
+        .iconfont {
+          color: #0075ff;
+          margin-right: 2px;
+        }
+      }
+    }
+  }
+
+  .content {
+    display: flex;
+    flex-direction: column;
+    // max-width: calc(100vw - 430px);
+    overflow: scroll;
+
+    &-head {
+      display: flex;
+      background-color: #f0f2f5;
+
+      &--fixed {
+        position: fixed;
+        top: 80px;
+        z-index: 8;
+        padding-bottom: 4px;
+      }
+
+      &--edit {
+        width: calc(100vw - 488px);
+        overflow-x: auto;
+
+        &::-webkit-scrollbar {
+          display: none;
+        }
+      }
+    }
+
+    &-body {
+      display: flex;
+      flex-direction: column;
+
+      &--fixed {
+        padding-top: 54px;
+      }
+    }
+  }
+
+  .body {
+    &-list {
+      display: flex;
+
+      &_item {
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        font-size: 14px;
+        padding: 0 10px;
+        width: 82px;
+        height: 54px;
+        text-align: center;
+        background: #fff;
+        margin-right: 1px;
+        cursor: pointer;
+        color: #000a18;
+        transition: all 0.3s ease-in-out;
+
+        &:last-child {
+          margin-right: 10px;
+          border-radius: 0 4px 4px 0;
+        }
+
+        &:first-child {
+          border-radius: 4px 0 0 4px;
+        }
+
+        &:hover {
+          transform: scale(1.1);
+          background: #fff;
+          box-shadow: 0 0 20px 0 rgb(0 37 74 / 12%);
+          border-radius: 4px;
+        }
+
+        &--empty {
+          background-color: #eaedf3;
+
+          &:hover {
+            background-color: #eaedf3;
+          }
+        }
+
+        &--only {
+          border-radius: 4px;
+          width: 246px;
+          margin-right: 10px;
+        }
+
+        &--old {
+          color: #c3cdd8;
+        }
+
+        &--selected {
+          background-color: rgb(239 255 251 / 100%);
+          border: 1px dashed rgb(68 215 182 / 100%);
+
+          &:hover {
+            background-color: rgb(239 255 251 / 100%);
+          }
+        }
+
+        &--actived {
+          background-color: rgb(233 246 255 / 100%);
+          border: 1px dashed rgb(0 109 255 / 100%);
+
+          &:hover {
+            background-color: rgb(233 246 255 / 100%);
+          }
+        }
+
+        &--disabled {
+          color: #c3cdd8;
+          background: #eaedf3;
+          cursor: not-allowed;
+
+          &:hover {
+            transform: scale(1);
+            color: #c3cdd8;
+            background: #eaedf3;
+          }
+        }
+
+        &--select {
+          background: #fff;
+        }
+
+        &--hidden {
+          display: none;
+        }
+      }
+    }
+  }
+
+  .head {
+    &-item {
+      margin-right: 10px;
+
+      &:last-child {
+        margin-right: 0;
+      }
+
+      &_time {
+        text-align: center;
+        color: #000a18;
+        margin-bottom: 6px;
+
+        &-week {
+          font-size: 14px;
+          font-weight: 600;
+          margin-right: 6px;
+        }
+
+        &-day {
+          font-size: 12px;
+          font-weight: 400;
+          color: #818694;
+        }
+
+        &--current {
+          color: #0075ff;
+
+          .head-item_time-day {
+            color: #0075ff;
+          }
+        }
+      }
+
+      &_sailings {
+        display: flex;
+
+        &-item {
+          width: 82px;
+          font-size: 12px;
+          font-weight: 400;
+          color: #818694;
+          text-align: center;
+          margin-right: 1px;
+          // background-color: #ccc;
+          &:last-child {
+            margin-right: 0;
+          }
+
+          &--only {
+            width: 246px;
+          }
+        }
+      }
+    }
+  }
+
+  .edit {
+    position: fixed;
+    top: 100px;
+    right: 20px;
+    min-width: 250px;
+    height: calc(100vh - 120px);
+    padding: 14px 0;
+    background-color: #fff;
+    border-radius: 4px;
+    z-index: 9;
+
+    &-tit {
+      padding: 0 20px 14px;
+      font-size: 16px;
+      font-weight: 600;
+      color: #000a18;
+      border-bottom: 1px solid #e1e5ea;
+    }
+
+    &-cnt {
+      height: calc(100vh - 580px);
+      padding: 20px;
+
+      &_detail {
+        height: 100%;
+        overflow-y: auto;
+      }
+
+      &_tit {
+        position: relative;
+        padding-bottom: 10px;
+        padding-left: 6px;
+        font-size: 14px;
+        font-weight: 600;
+        color: #000a18;
+
+        &::after {
+          position: absolute;
+          top: 6px;
+          left: 0;
+          content: '';
+          width: 2px;
+          height: 12px;
+          background: #000a18;
+          border-radius: 1px;
+        }
+      }
+    }
+
+    &-foot {
+      position: absolute;
+      bottom: 0;
+      width: 100%;
+      padding: 14px 20px;
+      border-top: 1px solid #e1e5ea;
+      display: flex;
+      justify-content: space-between;
+
+      &_btn {
+        width: 45%;
+      }
+    }
+
+    &-item {
+      min-width: 210px;
+      height: 40px;
+      line-height: 40px;
+      background: #f4f6f9;
+      border-radius: 4px;
+      margin-bottom: 10px;
+      padding: 0 12px;
+      cursor: pointer;
+      font-size: 14px;
+      font-weight: 500;
+      color: #000a18;
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+
+      &_count {
+        font-size: 12px;
+        font-weight: 400;
+        color: #818694;
+      }
+
+      &--not {
+        background: #effffb;
+        border: 1px dashed #44d7b6;
+      }
+
+      &--has {
+        background: #e9f6ff;
+        border: 1px dashed #006dff;
+      }
+    }
+  }
+</style>

+ 7 - 0
src/views/biz/bed/person/index.vue

@@ -0,0 +1,7 @@
+<template>
+  <div> 占位符 </div>
+</template>
+
+<script setup lang="ts"></script>
+
+<style lang="less" scoped></style>

+ 7 - 0
src/views/biz/visit/check/index.vue

@@ -0,0 +1,7 @@
+<template>
+  <div> 占位符 </div>
+</template>
+
+<script setup lang="ts"></script>
+
+<style lang="less" scoped></style>

+ 7 - 0
src/views/biz/visit/room/index.vue

@@ -0,0 +1,7 @@
+<template>
+  <div> 占位符 </div>
+</template>
+
+<script setup lang="ts"></script>
+
+<style lang="less" scoped></style>

+ 7 - 0
src/views/biz/visit/rounds/index.vue

@@ -0,0 +1,7 @@
+<template>
+  <div> 占位符 </div>
+</template>
+
+<script setup lang="ts"></script>
+
+<style lang="less" scoped></style>

+ 7 - 0
src/views/biz/visit/transfer/index.vue

@@ -0,0 +1,7 @@
+<template>
+  <div> 占位符 </div>
+</template>
+
+<script setup lang="ts"></script>
+
+<style lang="less" scoped></style>

+ 18 - 0
src/views/infra/numStrategy/data.ts

@@ -1,6 +1,7 @@
 import { listDictModel } from '/@/api/common';
 import { DescItem } from '/@/components/Description';
 import { BasicColumn, FormSchema } from '/@/components/Table';
+import { validateStr } from '/@/utils/validate';
 
 export const columns: BasicColumn[] = [
   {
@@ -58,6 +59,22 @@ export const dataFormSchema: FormSchema[] = [
     componentProps: {
       placeholder: '请输入策略编码',
     },
+    dynamicRules: () => {
+      return [
+        {
+          required: true,
+          validator: async (_, value) => {
+            if (!value) {
+              return Promise.reject('字典项编码不能为空');
+            }
+            if (validateStr(value)) {
+              return Promise.reject('字典项编码为字母或数字组成');
+            }
+            return Promise.resolve();
+          },
+        },
+      ];
+    },
   },
   {
     label: '策略名称',
@@ -107,6 +124,7 @@ export const dataFormSchema: FormSchema[] = [
     label: '生成模式',
     field: 'type',
     component: 'ApiSelect',
+    required: true,
     componentProps: {
       api: listDictModel,
       params: {

+ 1 - 1
src/views/infra/numStrategy/formDrawer.vue

@@ -27,7 +27,7 @@
   const emit = defineEmits(['success', 'register']);
 
   const getTitle = computed(() => (!unref(isUpdate) ? '新增编号策略' : '编辑编号策略'));
-  const width = '30%';
+  const width = '45%';
   const isUpdate = ref(false);
   const rowId = ref();
 

+ 7 - 24
src/views/monitor/operLog/index.vue

@@ -45,14 +45,6 @@
         </template>
       </template>
       <template #toolbar>
-        <!-- <Button
-          v-auth="['sys:log:add']"
-          type="primary"
-          @click="handleCreate"
-          preIcon="icon-plus|iconfont"
-        >
-          新增
-        </Button> -->
         <Button
           v-auth="['sys:log:remove']"
           type="primary"
@@ -73,15 +65,15 @@
   import { Tag } from 'ant-design-vue';
   import { Button } from '/@/components/Button';
 
-  import { BasicTable, useTable, TableAction } from '/@/components/Table';
+  import { BasicTable, useTable, TableAction } from '/@/components/TableCard';
 
   // import { useModal } from '/@/components/Modal';
   import { useMessage } from '/@/hooks/web/useMessage';
   import FormDrawer from './formDrawer.vue';
   import ViewDrawer from './viewDrawer.vue';
-  import { columns, searchFormSchema } from './data';
+  import { columns } from './data';
 
-  import { LogQueryPage, LogRemove } from '/@/api/monitor/LogApi';
+  import { LogQueryPage, LogRemove, LogExport } from '/@/api/monitor/LogApi';
   import { listDictModel } from '/@/api/common';
   import { formatDictColor, formatDictValue } from '/@/utils'; //
   import { useDrawer } from '/@/components/Drawer';
@@ -109,23 +101,14 @@
 
   const [registerTable, { reload, getSelectRowKeys }] = useTable({
     api: LogQueryPage,
+    batchDelApi: LogRemove,
+    batchExportApi: LogExport, // 目前调用的是删除接口
+    exportAuthList: ['sys:log:export'],
+    delAuthList: ['sys:log:remove'],
     rowKey: 'id',
     columns,
     showIndexColumn: true,
     rowSelection: { type: 'checkbox' },
-    formConfig: {
-      labelWidth: 120,
-      schemas: searchFormSchema,
-      autoSubmitOnEnter: true,
-      baseColProps: { xs: 24, sm: 12, md: 12, lg: 8 },
-      resetButtonOptions: {
-        preIcon: 'icon-delete|iconfont',
-      },
-      submitButtonOptions: {
-        preIcon: 'icon-search|iconfont',
-      },
-    },
-    useSearchForm: true,
     bordered: true,
     actionColumn: {
       width: 200,

+ 111 - 0
src/views/sys/sysConstant/ConstantConfig/data.ts

@@ -0,0 +1,111 @@
+import { DescItem } from '/@/components/Description';
+import { BasicColumn, FormSchema } from '/@/components/Table';
+
+export const columns: BasicColumn[] = [
+  {
+    title: '常量名称',
+    dataIndex: 'name',
+  },
+  {
+    title: '常量类型',
+    dataIndex: 'type',
+  },
+  {
+    title: '常量编码',
+    dataIndex: 'code',
+  },
+  {
+    title: '目录',
+    dataIndex: 'cateid',
+  },
+];
+
+// 表单列定义
+export const searchFormSchema: FormSchema[] = [
+  {
+    label: '常量名称',
+    field: 'name',
+    component: 'Input',
+    componentProps: {
+      placeholder: '请输入常量名称',
+    },
+  },
+  {
+    label: '常量编码',
+    field: 'code',
+    component: 'Input',
+    componentProps: {
+      placeholder: '请输入常量编码',
+    },
+  },
+];
+// 表单新增编辑
+export const dataFormSchema: FormSchema[] = [
+  {
+    label: '常量名称',
+    field: 'name',
+    required: true,
+    component: 'Input',
+    componentProps: {
+      placeholder: '请输入常量名称',
+    },
+  },
+  {
+    label: '常量类型',
+    field: 'type',
+    required: true,
+    component: 'Input',
+    componentProps: {
+      placeholder: '请输入常量类型',
+    },
+  },
+  {
+    label: '常量编码',
+    field: 'code',
+    required: true,
+    component: 'Input',
+    componentProps: {
+      placeholder: '请输入常量编码',
+    },
+  },
+  {
+    label: '目录',
+    field: 'cateid',
+    required: true,
+    component: 'Input',
+    componentProps: {
+      placeholder: '请输入目录id',
+    },
+  },
+  {
+    label: '备注',
+    field: 'remark',
+    component: 'InputTextArea',
+    componentProps: {
+      placeholder: '请输入备注',
+    },
+  },
+];
+// 表单详情查看
+export const viewSchema: DescItem[] = [
+  {
+    label: '常量名称',
+    field: 'name',
+  },
+  {
+    label: '常量类型',
+    field: 'type',
+  },
+  {
+    label: '备注',
+    field: 'remark',
+  },
+  {
+    label: '常量编码',
+    field: 'code',
+  },
+  {
+    label: '目录id',
+    field: 'cateid',
+  },
+];

+ 71 - 0
src/views/sys/sysConstant/ConstantConfig/formDrawer.vue

@@ -0,0 +1,71 @@
+<template>
+  <BasicDrawer
+    v-bind="$attrs"
+    destroyOnClose
+    @register="registerDrawer"
+    :title="getTitle"
+    :width="width"
+    @ok="handleSubmit"
+    :showFooter="true"
+  >
+    <BasicForm @register="registerForm" layout="vertical" />
+  </BasicDrawer>
+</template>
+<script lang="ts" setup>
+  import { ref, computed, unref } from 'vue';
+  import { BasicDrawer, useDrawerInner } from '/@/components/Drawer';
+  import { BasicForm, useForm } from '/@/components/Form';
+  import { useMessage } from '/@/hooks/web/useMessage';
+  import { dataFormSchema } from './data';
+
+  import { sysConfigAdd, sysConfigEdit, sysConfigDetail } from '/@/api/sys/sysConstantConfig';
+
+  const emit = defineEmits(['success', 'register']);
+
+  const getTitle = computed(() => (!unref(isUpdate) ? '新增常量配置' : '编辑常量配置'));
+  const width = '45%';
+  const isUpdate = ref(false);
+  const rowId = ref();
+
+  const { createMessage } = useMessage();
+  const [registerForm, { setFieldsValue, resetFields, validate }] = useForm({
+    labelWidth: 100,
+    schemas: dataFormSchema,
+    showActionButtonGroup: false,
+    actionColOptions: {
+      span: 23,
+    },
+  });
+  const [registerDrawer, { setDrawerProps, closeDrawer }] = useDrawerInner(async data => {
+    await resetFields();
+    setDrawerProps({ confirmLoading: false });
+    isUpdate.value = !!data?.isUpdate;
+
+    if (unref(isUpdate)) {
+      const resData = await sysConfigDetail(data.record.id);
+      rowId.value = resData.id;
+      await setFieldsValue({
+        ...resData,
+      });
+    }
+  });
+
+  // 提交按钮事件
+  async function handleSubmit() {
+    try {
+      const values = await validate();
+      setDrawerProps({ confirmLoading: true });
+      !unref(isUpdate)
+        ? await sysConfigAdd({ ...values })
+        : await sysConfigEdit({ ...values, id: rowId.value });
+      !unref(isUpdate) ? createMessage.success('新增成功!') : createMessage.success('编辑成功!');
+      closeDrawer();
+      emit('success', {
+        isUpdate: unref(isUpdate),
+        values: { ...values, id: rowId.value },
+      });
+    } finally {
+      setDrawerProps({ confirmLoading: false });
+    }
+  }
+</script>

+ 186 - 0
src/views/sys/sysConstant/ConstantConfig/index.vue

@@ -0,0 +1,186 @@
+<template>
+  <div>
+    <BasicTable @register="registerTable">
+      <template #bodyCell="{ column, record }">
+        <template v-if="column.key === 'action'">
+          <TableAction
+            :actions="[
+              {
+                auth: 'constant:config:query',
+                icon: 'icon-eye|iconfont',
+                tooltip: '查看',
+                label: '查看',
+                onClick: handleView.bind(null, record),
+              },
+              {
+                auth: 'constant:config:edit',
+                icon: 'icon-edit|iconfont',
+                tooltip: '编辑',
+                label: '编辑',
+                onClick: handleEdit.bind(null, record),
+              },
+              {
+                auth: 'constant:config:remove',
+                icon: 'icon-delete|iconfont',
+                tooltip: '删除',
+                label: '删除',
+                color: 'error',
+                popConfirm: {
+                  title: '是否确认删除',
+                  placement: 'left',
+                  confirm: handleDelete.bind(null, record),
+                },
+              },
+            ]"
+          />
+        </template>
+      </template>
+      <template #toolbar>
+        <Button
+          v-auth="['constant:config:add']"
+          type="primary"
+          @click="handleCreate"
+          preIcon="icon-plus|iconfont"
+        >
+          新增
+        </Button>
+        <Button
+          v-auth="['constant:config:remove']"
+          type="primary"
+          danger
+          @click="handleDelete(null)"
+          preIcon="icon-delete|iconfont"
+        >
+          批量删除
+        </Button>
+      </template>
+    </BasicTable>
+    <FormDrawer @register="registerDrawer" @success="handleSuccess" />
+    <ViewDrawer @register="registerDrawerView" @success="handleSuccess" />
+  </div>
+</template>
+<script lang="ts" setup>
+  import { onBeforeMount, ref } from 'vue';
+  import { Button } from '/@/components/Button';
+
+  import { BasicTable, useTable, TableAction } from '/@/components/Table';
+
+  // import { useModal } from '/@/components/Modal';
+  import { useMessage } from '/@/hooks/web/useMessage';
+  import FormDrawer from './formDrawer.vue';
+  import ViewDrawer from './viewDrawer.vue';
+  import { columns, searchFormSchema } from './data';
+
+  import { sysConfigQueryPage, sysConfigRemove } from '/@/api/sys/sysConstantConfig';
+  import { useDrawer } from '/@/components/Drawer';
+
+  onBeforeMount(async () => {});
+
+  const { createConfirm, createMessage } = useMessage();
+  // const [registerModal, { openModal }] = useModal();
+  const [registerDrawer, { openDrawer }] = useDrawer();
+  const [registerDrawerView, { openDrawer: openDrawerView }] = useDrawer();
+
+  const tableSort = ref([
+    {
+      field: 'create_time',
+      direction: 'DESC',
+    },
+  ]) as any;
+
+  const [registerTable, { reload, getSelectRowKeys }] = useTable({
+    title: ' ',
+    api: sysConfigQueryPage,
+    rowKey: 'id',
+    columns,
+    showIndexColumn: true,
+    rowSelection: { type: 'checkbox' },
+    formConfig: {
+      labelWidth: 120,
+      schemas: searchFormSchema,
+      autoSubmitOnEnter: true,
+      baseColProps: { xs: 24, sm: 12, md: 12, lg: 8 },
+      resetButtonOptions: {
+        preIcon: 'icon-delete|iconfont',
+      },
+      submitButtonOptions: {
+        preIcon: 'icon-search|iconfont',
+      },
+    },
+    useSearchForm: true,
+    bordered: true,
+    actionColumn: {
+      width: 200,
+      title: '操作',
+      dataIndex: 'action',
+    },
+    beforeFetch: handleBeforeFetch,
+    sortFn: handleSortFn,
+  });
+  // 详情按钮事件
+  function handleView(record: Recordable) {
+    console.log(record);
+    openDrawerView(true, {
+      record,
+    });
+  }
+
+  // 新增按钮事件
+  function handleCreate() {
+    openDrawer(true, {
+      isUpdate: false,
+    });
+  }
+
+  // 编辑按钮事件
+  function handleEdit(record: Recordable) {
+    openDrawer(true, {
+      record,
+      isUpdate: true,
+    });
+  }
+
+  // 删除按钮事件
+  async function handleDelete(record: Recordable) {
+    if (record) {
+      await sysConfigRemove([record.id]);
+      createMessage.success('删除成功!');
+      await reload();
+    } else {
+      createConfirm({
+        content: '你确定要删除?',
+        iconType: 'warning',
+        onOk: async () => {
+          const keys = getSelectRowKeys();
+          await sysConfigRemove(keys);
+          createMessage.success('删除成功!');
+          await reload();
+        },
+      });
+    }
+  }
+  // 表格点击字段排序
+  function handleSortFn(sortInfo) {
+    if (sortInfo?.order && sortInfo?.columnKey) {
+      // 默认单列排序
+      tableSort.value = [
+        {
+          field: sortInfo.columnKey,
+          direction: sortInfo.order.replace(/(\w+)(end)/g, '$1').toUpperCase(),
+        },
+      ];
+    }
+  }
+
+  // 表格请求之前,对参数进行处理, 添加默认 排序
+  function handleBeforeFetch(params) {
+    return { ...params, orders: tableSort.value };
+  }
+
+  // 弹窗回调事件
+  async function handleSuccess({ isUpdate, values }) {
+    console.log(isUpdate);
+    console.log(values);
+    await reload();
+  }
+</script>

+ 40 - 0
src/views/sys/sysConstant/ConstantConfig/viewDrawer.vue

@@ -0,0 +1,40 @@
+<template>
+  <BasicDrawer
+    v-bind="$attrs"
+    destroyOnClose
+    @register="registerDrawer"
+    :title="getTitle"
+    :width="width"
+  >
+    <Description @register="registerDesc" :data="descData" />
+  </BasicDrawer>
+</template>
+<script lang="ts" setup>
+  import { onBeforeMount, ref } from 'vue'; // onBeforeMount,
+  import { BasicDrawer, useDrawerInner } from '/@/components/Drawer';
+  import { Description, useDescription } from '/@/components/Description';
+  import { viewSchema } from './data';
+
+  import { sysConfigDetail } from '/@/api/sys/sysConstantConfig';
+
+  const descData = ref({});
+  const getTitle = '查看常量配置';
+  const width = '45%';
+
+  onBeforeMount(async () => {});
+  const [registerDrawer] = useDrawerInner(async data => {
+    console.log('::::::::::', data.record);
+    const resData = await sysConfigDetail(data.record.id);
+    descData.value = {
+      ...resData,
+    };
+  });
+  const [registerDesc] = useDescription({
+    schema: viewSchema,
+    column: 2,
+    size: 'middle',
+    labelStyle: {
+      width: '120px',
+    },
+  });
+</script>

+ 17 - 0
src/views/sys/sysDict/sysDictItemTable/data.ts

@@ -1,4 +1,5 @@
 import { BasicColumn, FormSchema } from '/@/components/Table';
+import { validateStr } from '/@/utils/validate';
 
 export const columns: BasicColumn[] = [
   {
@@ -63,6 +64,22 @@ export const dataFormSchema: FormSchema[] = [
     componentProps: {
       placeholder: '请输入字典项编码',
     },
+    dynamicRules: () => {
+      return [
+        {
+          required: true,
+          validator: async (_, value) => {
+            if (!value) {
+              return Promise.reject('字典项编码不能为空');
+            }
+            if (validateStr(value)) {
+              return Promise.reject('字典项编码为字母或数字组成');
+            }
+            return Promise.resolve();
+          },
+        },
+      ];
+    },
   },
   {
     field: 'color',

+ 4 - 4
src/views/sys/sysDict/sysDictItemTable/index.vue

@@ -11,13 +11,13 @@
           <TableAction
             :actions="[
               {
-                auth: ['system:sysDictItem:edit'],
+                auth: ['sys:dictItem:edit'],
                 icon: 'icon-edit|iconfont',
                 tooltip: '编辑',
                 onClick: handleEdit.bind(null, record),
               },
               {
-                auth: ['system:sysDictItem:remove'],
+                auth: ['sys:dictItem:remove'],
                 icon: 'icon-delete|iconfont',
                 tooltip: '删除',
                 color: 'error',
@@ -33,7 +33,7 @@
       </template>
       <template #toolbar>
         <a-button
-          v-auth="['system:sysDictItem:add']"
+          v-auth="['sys:dictItem:add']"
           v-show="!!dictId"
           type="primary"
           @click="handleCreate"
@@ -92,7 +92,7 @@
     showTableSetting: true,
     bordered: true,
     actionColumn: {
-      auth: ['system:sysDictItem:edit', 'system:sysDictItem:remove'],
+      auth: ['sys:dictItem:edit', 'sys:dictItem:remove'],
       width: 80,
       title: '操作',
       dataIndex: 'action',

+ 18 - 1
src/views/sys/sysDict/sysDictTable/data.ts

@@ -1,5 +1,6 @@
 import { BasicColumn, FormSchema } from '/@/components/Table';
 import { listDictModel } from '/@/api/common';
+import { validateStr } from '/@/utils/validate';
 
 export const columns: BasicColumn[] = [
   {
@@ -74,6 +75,22 @@ export const dataFormSchema: FormSchema[] = [
     componentProps: {
       placeholder: '请输入字典编码',
     },
+    dynamicRules: () => {
+      return [
+        {
+          required: true,
+          validator: async (_, value) => {
+            if (!value) {
+              return Promise.reject('字典项编码不能为空');
+            }
+            if (validateStr(value)) {
+              return Promise.reject('字典项编码为字母或数字组成');
+            }
+            return Promise.resolve();
+          },
+        },
+      ];
+    },
   },
   {
     field: 'dictType',
@@ -98,7 +115,7 @@ export const dataFormSchema: FormSchema[] = [
         dictCode: 'sys_disable_type',
       },
     },
-    defaultValue: 0,
+    defaultValue: '0',
   },
   {
     label: '备注',

+ 4 - 4
src/views/sys/sysDict/sysDictTable/index.vue

@@ -21,13 +21,13 @@
           <TableAction
             :actions="[
               {
-                auth: ['system:sysDict:edit'],
+                auth: ['sys:dict:edit'],
                 icon: 'icon-edit|iconfont',
                 tooltip: '编辑',
                 onClick: handleEdit.bind(null, record),
               },
               {
-                auth: ['system:sysDict:remove'],
+                auth: ['sys:dict:remove'],
                 icon: 'icon-delete|iconfont',
                 tooltip: '删除',
                 color: 'error',
@@ -42,7 +42,7 @@
       </template>
       <template #toolbar>
         <a-button
-          v-auth="['system:sysDict:add']"
+          v-auth="['sys:dict:add']"
           type="primary"
           @click="handleCreate"
           preIcon="icon-plus|iconfont"
@@ -102,7 +102,7 @@
       showTableSetting: true,
       bordered: true,
       actionColumn: {
-        auth: ['system:sysDict:edit', 'system:sysDict:remove'],
+        auth: ['sys:dict:edit', 'sys:dict:remove'],
         width: 80,
         title: '操作',
         dataIndex: 'action',

+ 14 - 3
src/views/sys/sysMenu/sysMenuTable/FormModal.vue

@@ -67,9 +67,7 @@
 
     const treeDatas = await sysMenuQueryTree({ excludeNodeIds: [data.record.id] });
     treeDatas.forEach(async item => {
-      if (item.id == data.record.id) {
-        await setDisable(item);
-      }
+      findItem(item, data.record.id);
     });
     const treeData = [
       {
@@ -80,6 +78,7 @@
     ];
     await updateSchema({ field: 'parentId', componentProps: { treeData } });
   });
+  // 设置自身及自身下级不可选择
   async function setDisable(item) {
     item.disabled = true;
     if (item.children?.length) {
@@ -88,6 +87,18 @@
       });
     }
   }
+  // 寻找自身菜单
+  async function findItem(item, recordId) {
+    if (item.id == recordId) {
+      await setDisable(item);
+    } else {
+      if (item.children?.length) {
+        item.children.map(async ele => {
+          await findItem(ele, recordId);
+        });
+      }
+    }
+  }
   // 提交按钮事件
   async function handleSubmit() {
     try {

+ 14 - 3
src/views/sys/sysOrg/sysOrgTable/FormModal.vue

@@ -55,9 +55,7 @@
     }
     const treeDatas = await sysOrgQueryTree({ excludeNodeIds: [data.record.id] });
     treeDatas.forEach(async item => {
-      if (item.id == data.record.id) {
-        await setDisable(item);
-      }
+      findItem(item, data.record.id);
     });
     const treeData = [
       {
@@ -74,6 +72,7 @@
     ]);
   });
 
+  // 设置自身及自身下级不可选择
   async function setDisable(item) {
     item.disabled = true;
     if (item.children?.length) {
@@ -82,6 +81,18 @@
       });
     }
   }
+  // 寻找自身菜单
+  async function findItem(item, recordId) {
+    if (item.id == recordId) {
+      await setDisable(item);
+    } else {
+      if (item.children?.length) {
+        item.children.map(async ele => {
+          await findItem(ele, recordId);
+        });
+      }
+    }
+  }
   // 提交按钮事件
   async function handleSubmit() {
     try {

+ 10 - 1
src/views/sys/sysOrg/sysOrgTable/index.vue

@@ -11,11 +11,13 @@
           <TableAction
             :actions="[
               {
+                auth: ['sys:org:edit'],
                 icon: 'icon-edit|iconfont',
                 tooltip: '编辑',
                 onClick: handleEdit.bind(null, record),
               },
               {
+                auth: ['sys:org:remove'],
                 icon: 'icon-delete|iconfont',
                 tooltip: '删除',
                 color: 'error',
@@ -30,7 +32,13 @@
         </template>
       </template>
       <template #toolbar>
-        <a-button type="primary" @click="handleCreate" preIcon="icon-plus|iconfont">新增</a-button>
+        <a-button
+          type="primary"
+          @click="handleCreate"
+          v-auth="['sys:org:add']"
+          preIcon="icon-plus|iconfont"
+          >新增</a-button
+        >
       </template>
     </BasicTable>
     <FormModal @register="registerModal" @success="handleSuccess" />
@@ -88,6 +96,7 @@
     showTableSetting: true,
     bordered: true,
     actionColumn: {
+      auth: ['sys:org:edit', 'sys:org:remove'],
       width: 80,
       title: '操作',
       dataIndex: 'action',

+ 1 - 1
src/views/sys/sysPortal/FormModalPortalMenu.vue

@@ -100,6 +100,6 @@
 
   // 查询树数据
   async function getTreeData() {
-    treeData.value = await sysMenuQueryTree({});
+    treeData.value = await sysMenuQueryTree({ menuType: ['menu', 'dir'] });
   }
 </script>

+ 17 - 32
src/views/sys/sysPortal/data.ts

@@ -1,6 +1,6 @@
-import { listDictModel } from '/@/api/common';
 import { DescItem } from '/@/components/Description';
 import { BasicColumn, FormSchema } from '/@/components/Table';
+import { validateStr } from '/@/utils/validate';
 
 export const columns: BasicColumn[] = [
   {
@@ -11,12 +11,6 @@ export const columns: BasicColumn[] = [
     title: '门户名称',
     dataIndex: 'name',
   },
-  {
-    title: '排序',
-    dataIndex: 'sort',
-    width: 100,
-    sorter: true,
-  },
   {
     title: '门户类型',
     dataIndex: 'type',
@@ -47,11 +41,26 @@ export const dataFormSchema: FormSchema[] = [
   {
     label: '门户编码',
     field: 'code',
-    required: true,
     component: 'Input',
     componentProps: {
       placeholder: '请输入门户编码',
     },
+    dynamicRules: () => {
+      return [
+        {
+          required: true,
+          validator: async (_, value) => {
+            if (!value) {
+              return Promise.reject('门户编码不能为空');
+            }
+            if (validateStr(value)) {
+              return Promise.reject('门户编码为字母或数字组成');
+            }
+            return Promise.resolve();
+          },
+        },
+      ];
+    },
   },
   {
     label: '门户名称',
@@ -62,26 +71,6 @@ export const dataFormSchema: FormSchema[] = [
       placeholder: '请输入门户名称',
     },
   },
-  {
-    label: '门户类型',
-    field: 'type',
-    component: 'ApiRadioGroup',
-    componentProps: {
-      api: listDictModel,
-      params: {
-        dictCode: 'sys_create_type',
-      },
-    },
-    defaultValue: 'sys',
-  },
-  {
-    label: '排序',
-    field: 'sort',
-    component: 'Input',
-    componentProps: {
-      placeholder: '请输入排序',
-    },
-  },
   {
     label: '备注',
     field: 'remark',
@@ -105,10 +94,6 @@ export const viewSchema: DescItem[] = [
     label: '门户类型',
     field: 'type',
   },
-  {
-    label: '排序',
-    field: 'sort',
-  },
   {
     label: '备注',
     field: 'remark',

+ 0 - 4
src/views/sys/sysPortal/index.vue

@@ -106,10 +106,6 @@
   const [registerModalPortalMenu, { openModal: openModalPortalMenu }] = useModal();
 
   const tableSort = ref([
-    {
-      field: 'sort',
-      direction: 'ASC',
-    },
     {
       field: 'create_time',
       direction: 'DESC',

+ 28 - 15
src/views/sys/sysPos/data.ts

@@ -1,6 +1,7 @@
 import { listDictModel } from '/@/api/common';
 import { DescItem } from '/@/components/Description';
 import { BasicColumn, FormSchema } from '/@/components/Table';
+import { validateStr } from '/@/utils/validate';
 
 export const columns: BasicColumn[] = [
   {
@@ -15,10 +16,6 @@ export const columns: BasicColumn[] = [
     title: '排序码',
     dataIndex: 'sort',
   },
-  {
-    title: '备注',
-    dataIndex: 'remark',
-  },
   {
     title: '状态',
     dataIndex: 'disable',
@@ -61,11 +58,26 @@ export const dataFormSchema: FormSchema[] = [
   {
     label: '岗位编码',
     field: 'posCode',
-    required: true,
     component: 'Input',
     componentProps: {
       placeholder: '请输入岗位编码',
     },
+    dynamicRules: () => {
+      return [
+        {
+          required: true,
+          validator: async (_, value) => {
+            if (!value) {
+              return Promise.reject('岗位编码不能为空');
+            }
+            if (validateStr(value)) {
+              return Promise.reject('岗位编码为字母或数字组成');
+            }
+            return Promise.resolve();
+          },
+        },
+      ];
+    },
   },
   {
     label: '排序码',
@@ -76,25 +88,26 @@ export const dataFormSchema: FormSchema[] = [
       placeholder: '请输入排序码',
     },
   },
-  {
-    label: '备注',
-    field: 'remark',
-    required: true,
-    component: 'Input',
-    componentProps: {
-      placeholder: '请输入备注',
-    },
-  },
   {
     label: '状态',
     field: 'disable',
-    component: 'ApiSelect',
+    required: true,
+    component: 'ApiRadioGroup',
     componentProps: {
       api: listDictModel,
       params: {
         dictCode: 'sys_disable_type',
       },
     },
+    defaultValue: '0',
+  },
+  {
+    label: '备注',
+    field: 'remark',
+    component: 'Input',
+    componentProps: {
+      placeholder: '请输入备注',
+    },
   },
 ];
 // 表单详情查看

+ 1 - 0
src/views/sys/sysPos/formDrawer.vue

@@ -46,6 +46,7 @@
       rowId.value = resData.id;
       await setFieldsValue({
         ...resData,
+        disable: String(resData.disable),
       });
     }
   });

+ 5 - 5
src/views/sys/sysPos/index.vue

@@ -11,21 +11,21 @@
           <TableAction
             :actions="[
               {
-                auth: 'sys:position:query',
+                auth: 'sys:pos:query',
                 icon: 'icon-eye|iconfont',
                 tooltip: '查看',
                 label: '查看',
                 onClick: handleView.bind(null, record),
               },
               {
-                auth: 'sys:position:edit',
+                auth: 'sys:pos:edit',
                 icon: 'icon-edit|iconfont',
                 tooltip: '编辑',
                 label: '编辑',
                 onClick: handleEdit.bind(null, record),
               },
               {
-                auth: 'sys:position:remove',
+                auth: 'sys:pos:remove',
                 icon: 'icon-delete|iconfont',
                 tooltip: '删除',
                 label: '删除',
@@ -42,7 +42,7 @@
       </template>
       <template #toolbar>
         <Button
-          v-auth="['sys:position:add']"
+          v-auth="['sys:pos:add']"
           type="primary"
           @click="handleCreate"
           preIcon="icon-plus|iconfont"
@@ -50,7 +50,7 @@
           新增
         </Button>
         <Button
-          v-auth="['sys:position:remove']"
+          v-auth="['sys:pos:remove']"
           type="primary"
           danger
           @click="handleDelete(null)"

+ 1 - 1
src/views/sys/sysRole/FormModal.vue

@@ -73,7 +73,7 @@
         .map(item => item.id);
       await setFieldsValue({
         ...data.record,
-        disable: data.record.disable ? 1 : 0,
+        disable: String(data.record.disable),
       });
     }
   });

+ 23 - 5
src/views/sys/sysRole/data.ts

@@ -1,6 +1,6 @@
+import { validateStr } from '/@/utils/validate';
 import { listDictModel } from '/@/api/common';
 import { BasicColumn, FormSchema } from '/@/components/Table';
-import { radioSwitch } from '/@/utils/filters';
 export const columns: BasicColumn[] = [
   {
     title: '角色名称',
@@ -77,10 +77,25 @@ export const dataFormSchema: FormSchema[] = [
     field: 'code',
     label: '角色编码',
     component: 'Input',
-    required: true,
     componentProps: {
       placeholder: '请输入角色编码',
     },
+    dynamicRules: () => {
+      return [
+        {
+          required: true,
+          validator: async (_, value) => {
+            if (!value) {
+              return Promise.reject('角色编码不能为空');
+            }
+            if (validateStr(value)) {
+              return Promise.reject('角色编码为字母或数字组成');
+            }
+            return Promise.resolve();
+          },
+        },
+      ];
+    },
   },
   {
     field: 'dataScope',
@@ -120,12 +135,15 @@ export const dataFormSchema: FormSchema[] = [
   {
     field: 'disable',
     label: '状态',
-    component: 'RadioGroup',
+    component: 'ApiRadioGroup',
     required: true,
     componentProps: {
-      options: radioSwitch,
+      api: listDictModel,
+      params: {
+        dictCode: 'sys_disable_type',
+      },
     },
-    defaultValue: 0,
+    defaultValue: '0',
   },
   {
     label: '备注',

+ 5 - 3
src/views/sys/sysRole/index.vue

@@ -13,8 +13,8 @@
           </Tag>
         </template>
         <template v-if="column.key === 'disable'">
-          <Tag :color="record.disable ? 'error' : 'success'">
-            {{ commonDict(record.disable, 1) }}
+          <Tag :color="formatDictColor(disableOptions, record.disable)">
+            {{ formatDictValue(disableOptions, record.disable) }}
           </Tag>
         </template>
         <template v-if="column.key === 'action'">
@@ -86,13 +86,15 @@
   import { sysRoleQueryPage, sysRoleRemove } from '/@/api/sys/sysRoleApi';
   // import { listDictModel, downloadFile } from '/@/api/common';
   import { listDictModel } from '/@/api/common';
-  import { formatDictValue, formatDictColor, commonDict } from '/@/utils';
+  import { formatDictValue, formatDictColor } from '/@/utils';
 
   const sysCreateTypeOptions = ref([]);
   const sysDataScopeOptions = ref([]);
+  const disableOptions = ref([]);
   onBeforeMount(async () => {
     sysCreateTypeOptions.value = await listDictModel({ dictCode: 'sys_create_type' });
     sysDataScopeOptions.value = await listDictModel({ dictCode: 'sys_data_scope' });
+    disableOptions.value = await listDictModel({ dictCode: 'sys_disable_type' });
   });
 
   const { createMessage } = useMessage();

+ 9 - 0
src/views/sys/sysSms/channel/data.ts

@@ -92,6 +92,7 @@ export const dataFormSchema: FormSchema[] = [
         dictCode: 'sys_sms_channel',
       },
     },
+    defaultValue: 'ALIYUN',
   },
   {
     label: '短信API账号',
@@ -119,6 +120,14 @@ export const dataFormSchema: FormSchema[] = [
     componentProps: {
       placeholder: '请输入短信发送回调URL',
     },
+    helpMessage: ({ values }) => {
+      switch (values.type) {
+        case 'ALIYUN':
+          return '阿里云回调地址:https://host:ip+/sms/callback/aliyun';
+        case 'TENCENT':
+          return '腾讯云回调地址: https://host:ip+/sms/callback/tencent';
+      }
+    },
   },
   {
     label: '是否启用',

+ 1 - 1
src/views/sys/sysSms/channel/formDrawer.vue

@@ -29,7 +29,7 @@
 
   const { createMessage } = useMessage();
   const [registerForm, { setFieldsValue, resetFields, validate }] = useForm({
-    labelWidth: 100,
+    labelWidth: 120,
     schemas: dataFormSchema,
     showActionButtonGroup: false,
     actionColOptions: {

+ 20 - 3
src/views/sys/sysSms/temp/data.ts

@@ -2,6 +2,7 @@ import { listDictModel } from '/@/api/common';
 import { smsChannelQueryPage } from '/@/api/sys/smsChannelApi';
 import { DescItem } from '/@/components/Description';
 import { BasicColumn, FormSchema } from '/@/components/Table';
+import { validateStr } from '/@/utils/validate';
 
 export const columns: BasicColumn[] = [
   {
@@ -26,7 +27,7 @@ export const columns: BasicColumn[] = [
   },
 
   {
-    title: '短信API模板编号',
+    title: 'API模板编号',
     dataIndex: 'apiTempCode',
   },
   // {
@@ -59,6 +60,22 @@ export const dataFormSchema: FormSchema[] = [
     componentProps: {
       placeholder: '请输入模板编码',
     },
+    dynamicRules: () => {
+      return [
+        {
+          required: true,
+          validator: async (_, value) => {
+            if (!value) {
+              return Promise.reject('模板编码不能为空');
+            }
+            if (validateStr(value)) {
+              return Promise.reject('模板编码为字母或数字组成');
+            }
+            return Promise.resolve();
+          },
+        },
+      ];
+    },
   },
   {
     label: '模板名称',
@@ -111,7 +128,7 @@ export const dataFormSchema: FormSchema[] = [
     },
   },
   {
-    label: '短信API模板编号',
+    label: 'API模板编号',
     field: 'apiTempCode',
     required: true,
     component: 'Input',
@@ -169,7 +186,7 @@ export const viewSchema: DescItem[] = [
     field: 'content',
   },
   {
-    label: '短信API模板编号',
+    label: 'API模板编号',
     field: 'apiTempCode',
   },
   // {

+ 1 - 1
src/views/sys/sysSms/temp/index.vue

@@ -28,7 +28,7 @@
                 onClick: handleView.bind(null, record),
               },
               {
-                auth: 'sms:temp:edit',
+                auth: 'sms:temp:send',
                 icon: 'icon-send|iconfont',
                 tooltip: '发送短信',
                 label: '发送短信',

+ 2 - 4
src/views/sys/sysTenant/page/data.ts

@@ -3,6 +3,7 @@ import { sysTenantPackageQueryPage } from '/@/api/sys/sysTenantPackageApi';
 import { validatorUsername } from '/@/api/sys/sysUserApi';
 import { DescItem } from '/@/components/Description';
 import { BasicColumn, FormSchema } from '/@/components/Table';
+import { validateUserName } from '/@/utils/validate';
 
 export const columns: BasicColumn[] = [
   {
@@ -125,10 +126,7 @@ export const dataFormSchema: FormSchema[] = [
             if (!value) {
               return Promise.reject('管理账号不能为空');
             }
-            if (value.length > 16 || value.length < 4) {
-              return Promise.reject('管理账号为4-16为字母或数字组成');
-            }
-            if (!/^[A-Za-z0-9_-]$/.test(value)) {
+            if (validateUserName(value)) {
               return Promise.reject('管理账号为4-16为字母或数字组成');
             }
             await validatorUsername({ username: value }).then(res => {

+ 9 - 0
src/views/sys/sysUser/sysUserTable/FormModal.vue

@@ -46,6 +46,15 @@
       const resData = await sysUserDetail(data.record.id);
       resData.sex = String(resData.sex || '3');
       resData.orgId = String(resData.orgId);
+      await updateSchema([
+        {
+          field: 'username',
+          componentProps: {
+            disabled: true,
+          },
+          dynamicRules: false,
+        },
+      ]);
       await setFieldsValue({
         ...resData,
       });

+ 6 - 6
src/views/sys/sysUser/sysUserTable/data.ts

@@ -5,6 +5,7 @@ import { sysOrgQueryTree } from '/@/api/sys/sysOrgApi';
 import { sysPosQueryPage } from '/@/api/sys/sysPosApi';
 import { validatorUsername } from '/@/api/sys/sysUserApi';
 import { listDictModel } from '/@/api/common';
+import { validateUserName } from '/@/utils/validate';
 export const columns: BasicColumn[] = [
   {
     title: '账号',
@@ -83,10 +84,7 @@ export const dataFormSchema: FormSchema[] = [
             if (!value) {
               return Promise.reject('管理账号不能为空');
             }
-            if (value.length > 16 || value.length < 4) {
-              return Promise.reject('管理账号为4-16为字母或数字组成');
-            }
-            if (!/^[A-Za-z0-9_-]$/.test(value)) {
+            if (validateUserName(value)) {
               return Promise.reject('管理账号为4-16为字母或数字组成');
             }
             await validatorUsername({ username: value }).then(res => {
@@ -171,6 +169,7 @@ export const dataFormSchema: FormSchema[] = [
         params: {
           pageNum: 1,
           pageSize: 999,
+          disable: 0,
         },
         mode: 'multiple',
         labelField: 'name',
@@ -193,9 +192,10 @@ export const dataFormSchema: FormSchema[] = [
         params: {
           pageNum: 1,
           pageSize: 999,
+          disable: 0,
         },
-        mode: 'single',
-        labelField: 'name',
+        mode: 'multiple',
+        labelField: 'posName',
         valueField: 'id',
         resultField: 'data',
       };

Some files were not shown because too many files changed in this diff