Explorar el Código

feat: 表格增加宽度和排序配置

xyh hace 3 años
padre
commit
52878a5e17

+ 6 - 0
packages/app/package.json

@@ -18,11 +18,13 @@
     "@hookform/resolvers": "^2.9.10",
     "@icon-park/react": "^1.4.2",
     "@tanstack/react-query": "^4.23.0",
+    "@tanstack/react-table": "^8.8.5",
     "ahooks": "^3.7.4",
     "antd": "^5.2.0",
     "axios": "^1.2.6",
     "classnames": "^2.3.2",
     "dayjs": "^1.11.7",
+    "events": "^3.3.0",
     "fast-deep-equal": "^3.1.3",
     "idb": "^7.1.1",
     "immer": "^9.0.19",
@@ -36,6 +38,8 @@
     "react-hook-form": "^7.43.0",
     "react-modal": "^3.16.1",
     "react-router-dom": "^6.8.0",
+    "react-sticky-box": "^2.0.4",
+    "react-stickynode": "^4.1.0",
     "react-virtualized-auto-sizer": "^1.0.15",
     "react-window": "^1.8.9",
     "recharts": "^2.4.3",
@@ -49,10 +53,12 @@
     "@testing-library/jest-dom": "^5.16.5",
     "@testing-library/react": "^13.4.0",
     "@total-typescript/ts-reset": "^0.4.2",
+    "@types/events": "^3.0.0",
     "@types/lodash-es": "^4.17.6",
     "@types/react": "^18.0.27",
     "@types/react-dom": "^18.0.10",
     "@types/react-modal": "^3.13.1",
+    "@types/react-stickynode": "^4.0.0",
     "@types/react-window": "^1.8.5",
     "jest-canvas-mock": "^2.4.0"
   }

+ 28 - 0
packages/app/src/apis/adjustment.ts

@@ -0,0 +1,28 @@
+import {
+  AddSettingsParams,
+  BaseResult,
+  GetSettingsParams,
+  SettingsData,
+} from '@models';
+import {request} from './request';
+
+const BASE_URL = '/adjustment';
+
+/** 上传配置信息 */
+export function addSetting(data: AddSettingsParams): BaseResult {
+  return request({
+    url: `${BASE_URL}/addTableAdjustment`,
+    method: 'POST',
+    data,
+  });
+}
+
+/** 获取配置信息 */
+export function getSettings(data: GetSettingsParams): BaseResult<SettingsData> {
+  return request({
+    url: `${BASE_URL}/getTableAdjustment`,
+    data,
+    method: 'GET',
+    skipError: true,
+  });
+}

+ 1 - 0
packages/app/src/apis/index.ts

@@ -16,3 +16,4 @@ export * from './gs';
 export * from './inventory';
 export * from './warehousing';
 export * from './traceBack';
+export * from './adjustment';

+ 12 - 2
packages/app/src/components/page-provider/index.tsx

@@ -1,18 +1,28 @@
-import {createPageContext, usePageContextReducer} from '@hooks';
+import {
+  createPageContext,
+  usePageContextReducer,
+  usePreloadSettingData,
+} from '@hooks';
 import {ChildrenFC, PAGE_SIZE_LIST} from '@utils';
 
 type Props = {
   context: ReturnType<typeof createPageContext>;
   initPageSize?: string;
+  preloadKey?: string;
 };
 
 const PageProvider: ChildrenFC<Props> = function ({
   children,
   context,
   initPageSize = PAGE_SIZE_LIST[0],
+  preloadKey,
 }) {
+  const preloadData = usePreloadSettingData(preloadKey);
+
   const state = usePageContextReducer({
-    pageSize: Number(initPageSize),
+    pageSize: preloadData?.pageLimit
+      ? Number(preloadData.pageLimit)
+      : Number(initPageSize),
     page: 1,
   });
   const {Provider} = context;

+ 62 - 0
packages/app/src/components/table-tools/hooks.ts

@@ -0,0 +1,62 @@
+import {addSetting} from '@apis';
+import {openFilterDB} from '@hooks';
+import {AddSettingsParams} from '@models';
+import {userStore} from '@stores';
+import {useMutation} from '@tanstack/react-query';
+import {RECEIVE_SETTING_EVENT, SAVE_SETTING_EVENT, eventBus} from '@utils';
+import {message} from 'antd';
+import {useEffect} from 'react';
+import {useLocation} from 'react-router-dom';
+import {useStore} from 'zustand';
+
+export function useSaveSetting(settingId?: string) {
+  const {isLoading, mutate} = useMutation({
+    mutationFn: addSetting,
+    onSuccess({msg}) {
+      if (msg === '200') {
+        message.success('保存完成');
+      }
+    },
+  });
+
+  function onSaveClick() {
+    eventBus.emit(SAVE_SETTING_EVENT);
+  }
+
+  const {pathname} = useLocation();
+  const {id} = useStore(userStore);
+  useEffect(
+    function () {
+      async function listener(
+        val: Omit<
+          AddSettingsParams & {key: string},
+          'userId' | 'track' | 'conditions'
+        >,
+      ) {
+        const selfKey = settingId ?? pathname;
+        if (val.key !== selfKey) return;
+
+        const db = await openFilterDB();
+        const data = await db.get('filterGroup', IDBKeyRange.only(selfKey));
+
+        const params: AddSettingsParams = {
+          ...val,
+          userId: String(id),
+          track: selfKey,
+          conditions: data?.auths ?? '',
+        };
+
+        mutate(params);
+      }
+
+      eventBus.addListener(RECEIVE_SETTING_EVENT, listener);
+
+      return function () {
+        eventBus.removeListener(RECEIVE_SETTING_EVENT, listener);
+      };
+    },
+    [id, mutate, pathname, settingId],
+  );
+
+  return [isLoading, onSaveClick] as const;
+}

+ 11 - 0
packages/app/src/components/table-tools/index.tsx

@@ -12,6 +12,7 @@ import {useLocation} from 'react-router-dom';
 import {useMemo} from 'react';
 import {useSessionStorageState} from 'ahooks';
 import {TreeRoleMenuData} from '@models';
+import {useSaveSetting} from './hooks';
 
 type Props = {
   onAdd?: () => void;
@@ -23,6 +24,7 @@ type Props = {
   onDownload?: () => void;
   uploadProps?: UploadProps;
   isUploading?: boolean;
+  preloadKey?: string;
 };
 
 const TableTools: ChildrenFC<Props> = function ({
@@ -36,6 +38,7 @@ const TableTools: ChildrenFC<Props> = function ({
   onDownload,
   uploadProps,
   isUploading,
+  preloadKey,
 }) {
   const [pages] = useSessionStorageState<TreeRoleMenuData[]>(MENU_STORAGE);
   const {pathname} = useLocation();
@@ -45,6 +48,7 @@ const TableTools: ChildrenFC<Props> = function ({
     },
     [pages, pathname],
   );
+  const [isSaveing, onSaveClick] = useSaveSetting(preloadKey);
 
   return (
     <section className='table-tool'>
@@ -100,6 +104,13 @@ const TableTools: ChildrenFC<Props> = function ({
           </Button>
         )}
         {children}
+        <Button
+          data-testid='save_setting_btn'
+          loading={isSaveing}
+          onClick={onSaveClick}
+        >
+          保存配置
+        </Button>
       </Space>
     </section>
   );

+ 73 - 0
packages/app/src/components/table/header-th/index.tsx

@@ -0,0 +1,73 @@
+import {Header, flexRender} from '@tanstack/react-table';
+import classNames from 'classnames';
+import {FC, CSSProperties} from 'react';
+import css from '../index.module.css';
+import {useSortable} from '@dnd-kit/sortable';
+
+type Props = {
+  header: Header<Record<string, any>, unknown>;
+  useDiv?: boolean;
+};
+
+const HeaderTh: FC<Props> = function ({header, useDiv}) {
+  const {setNodeRef, attributes, listeners} = useSortable({
+    id: header.id,
+    data: {header},
+  });
+
+  const style: CSSProperties = {
+    width: header.getSize(),
+    cursor: 'pointer',
+    opacity: useDiv ? '0.5' : '1',
+  };
+
+  if (useDiv) {
+    return (
+      <div
+        key={header.id}
+        style={style}
+        ref={setNodeRef}
+        {...attributes}
+        {...listeners}
+        className={css.tableHeadTh}
+      >
+        {header.isPlaceholder
+          ? null
+          : flexRender(header.column.columnDef.header, header.getContext())}
+
+        <div
+          onMouseDown={header.getResizeHandler()}
+          onPointerDown={e => e.stopPropagation()}
+          className={classNames(css.resizer, {
+            [css.resizing]: header.column.getIsResizing(),
+          })}
+        />
+      </div>
+    );
+  }
+
+  return (
+    <th
+      key={header.id}
+      style={style}
+      ref={setNodeRef}
+      {...attributes}
+      {...listeners}
+      className={css.tableHeadTh}
+    >
+      {header.isPlaceholder
+        ? null
+        : flexRender(header.column.columnDef.header, header.getContext())}
+
+      <div
+        onMouseDown={header.getResizeHandler()}
+        onPointerDown={e => e.stopPropagation()}
+        className={classNames(css.resizer, {
+          [css.resizing]: header.column.getIsResizing(),
+        })}
+      />
+    </th>
+  );
+};
+
+export default HeaderTh;

+ 159 - 0
packages/app/src/components/table/hooks.ts

@@ -0,0 +1,159 @@
+import {
+  createColumnHelper,
+  useReactTable,
+  getCoreRowModel,
+  Header,
+} from '@tanstack/react-table';
+import {ColumnType} from 'antd/es/table';
+import {useEffect, useRef, useState} from 'react';
+import {DragStartEvent, DragEndEvent} from '@dnd-kit/core';
+import {arrayMove} from '@dnd-kit/sortable';
+import {RECEIVE_SETTING_EVENT, SAVE_SETTING_EVENT, eventBus} from '@utils';
+import {AddSettingsParams} from '@models';
+import {usePreloadSettingData} from '@hooks';
+import {useLocation} from 'react-router-dom';
+
+function parseColumn<T>(columns: ColumnType<T>[]) {
+  const hepler = createColumnHelper<Record<string, any>>();
+
+  const tableColumns = columns.map(function (val) {
+    const {dataIndex, title, render, align} = val;
+    if (render) {
+      return hepler.display({
+        id: dataIndex?.toString() ?? 'id',
+        header: title?.toString(),
+        cell(props) {
+          return render(
+            props.getValue(),
+            props.row.original as T,
+            props.row.index,
+          );
+        },
+        meta: {align},
+      });
+    }
+
+    return hepler.accessor(dataIndex!.toString(), {
+      header: title?.toString(),
+      cell: props => props.getValue(),
+      minSize: 0,
+      meta: {align},
+    });
+  });
+
+  tableColumns.unshift(
+    hepler.display({
+      header: '序号',
+      id: 'no',
+      cell({row}) {
+        return row.index;
+      },
+      meta: {align: 'center'},
+    }),
+  );
+
+  return tableColumns;
+}
+
+export function useTable<T extends Record<string, any>>(
+  columns: ColumnType<T>[],
+  data: T[],
+  options: {pageSize: number; preloadKey?: string},
+) {
+  const columnList = useRef(parseColumn(columns));
+  const preload = usePreloadSettingData(options.preloadKey);
+
+  const [columnSizing, setColumnSizing] = useState(function () {
+    if (preload?.tableWidth)
+      return JSON.parse(preload.tableWidth) as Record<string, number>;
+
+    const obj: Record<string, number> = {no: 64};
+
+    columns.forEach(function ({dataIndex, width}) {
+      if (dataIndex && width) {
+        obj[dataIndex.toString()] = Number(width);
+      }
+    });
+
+    return obj;
+  });
+
+  const [columnOrder, setColumnOrder] = useState(function () {
+    if (preload?.tableOrder) return JSON.parse(preload?.tableOrder) as string[];
+
+    const nextList = columns.map(val => val.dataIndex!.toString());
+
+    return ['no', ...nextList];
+  });
+
+  const table = useReactTable({
+    data,
+    columns: columnList.current,
+    state: {
+      columnSizing,
+      columnOrder,
+    },
+    getCoreRowModel: getCoreRowModel(),
+    onColumnSizingChange: setColumnSizing,
+    columnResizeMode: 'onChange',
+    onColumnOrderChange: setColumnOrder,
+  });
+
+  const [active, setActive] = useState<Header<
+    Record<string, any>,
+    unknown
+  > | null>(null);
+
+  function onDragStart(event: DragStartEvent) {
+    setActive(event.active.data.current?.header);
+  }
+
+  function onDragEnd({active, over}: DragEndEvent) {
+    setActive(null);
+
+    if (!over) return;
+
+    const {id: activeId} = active,
+      {id: overId} = over;
+    if (activeId === overId) return;
+    const {setColumnOrder} = table;
+
+    setColumnOrder(function (prev) {
+      const fromIdx = prev.findIndex(val => val === activeId),
+        toIdx = prev.findIndex(val => val === overId);
+
+      return arrayMove(prev, fromIdx, toIdx);
+    });
+  }
+
+  const {pathname} = useLocation();
+  useEffect(
+    function () {
+      function listener() {
+        const params: Omit<
+          AddSettingsParams & {key: string},
+          'userId' | 'track' | 'conditions'
+        > = {
+          tableOrder: JSON.stringify(columnOrder),
+          tableWidth: JSON.stringify(columnSizing),
+          pageLimit: String(options.pageSize),
+          key: options.preloadKey ?? pathname,
+        };
+
+        eventBus.emit(RECEIVE_SETTING_EVENT, params);
+      }
+
+      eventBus.addListener(SAVE_SETTING_EVENT, listener);
+
+      return function () {
+        eventBus.removeListener(SAVE_SETTING_EVENT, listener);
+      };
+    },
+    [columnOrder, columnSizing, options.pageSize, options.preloadKey, pathname],
+  );
+
+  return [
+    {...table, active},
+    {onDragStart, onDragEnd},
+  ] as const;
+}

+ 101 - 0
packages/app/src/components/table/index.module.css

@@ -0,0 +1,101 @@
+.table-wrapper {
+  width: 100%;
+  overflow: auto;
+  border-inline-start: 1px solid #f0f0f0;
+  border-start-start-radius: 8px;
+  border-start-end-radius: 8px;
+}
+
+.table {
+  min-width: 100%;
+  padding: 0;
+  margin: 0;
+  overflow: unset;
+  font-size: 14px;
+  line-height: 1.5714;
+  color: rgb(0 0 0 / 88%);
+  text-align: start;
+  list-style: none;
+  border-spacing: 0;
+  border-collapse: separate;
+  background: #fff;
+  border-top: 1px solid #f0f0f0;
+  border-radius: 8px 8px 0 0;
+
+  & td {
+    min-width: 0;
+  }
+}
+
+.table-head {
+  position: sticky;
+  top: 0;
+  text-align: center;
+}
+
+.table-head-th {
+  position: relative;
+  min-width: 0;
+  padding: 16px;
+  margin: 0;
+  font-weight: 600;
+  color: rgb(0 0 0 / 88%);
+  text-align: center;
+  overflow-wrap: anywhere;
+  list-style: none;
+  background: #fafafa;
+  border-inline-end: 1px solid #f0f0f0;
+  border-bottom: 1px solid #f0f0f0;
+  border-start-start-radius: 8px;
+  transition: background 0.2s ease;
+
+  &:last-child {
+    border-start-end-radius: 8px;
+  }
+}
+
+.table-body {
+  & td {
+    position: relative;
+    min-width: 0;
+    padding: 16px;
+    overflow-wrap: anywhere;
+    border-inline-end: 1px solid #f0f0f0;
+    border-bottom: 1px solid #f0f0f0;
+    transition: background 0.2s, border-color 0.2s;
+  }
+}
+
+.resizer {
+  position: absolute;
+  top: 0;
+  right: 0;
+  width: 5px;
+  height: 100%;
+  touch-action: none;
+  cursor: col-resize;
+  user-select: none;
+  background: rgb(0 0 0 / 50%);
+  opacity: 0;
+
+  &:hover {
+    opacity: 1;
+  }
+}
+
+.resizing {
+  opacity: 1;
+}
+
+.sticky {
+  width: 100%;
+}
+
+.pagination {
+  width: 100%;
+  margin-top: 16px;
+
+  &:global(.ant-pagination) {
+    display: flex;
+  }
+}

+ 99 - 44
packages/app/src/components/table/index.tsx

@@ -1,13 +1,23 @@
-import {Table as OriTable} from 'antd';
-import {ColumnsType} from 'antd/es/table';
+import {Spin, Pagination} from 'antd';
+import {ColumnsType, ColumnType} from 'antd/es/table';
 import {
   createPageContext,
   createSearchContext,
   usePage,
   useTableSearchState,
 } from '@hooks';
-import {PAGE_SIZE_LIST, calcColumnsWidth} from '@utils';
-import {ReactElement, useMemo} from 'react';
+import {PAGE_SIZE_LIST} from '@utils';
+import {ReactElement} from 'react';
+import {useTable} from './hooks';
+import {flexRender} from '@tanstack/react-table';
+import css from './index.module.css';
+import HeaderTh from './header-th';
+import {DndContext, PointerSensor, useSensor, DragOverlay} from '@dnd-kit/core';
+import {
+  SortableContext,
+  horizontalListSortingStrategy,
+} from '@dnd-kit/sortable';
+import {restrictToHorizontalAxis} from '@dnd-kit/modifiers';
 
 type Props<T> = {
   columns: ColumnsType<T>;
@@ -15,9 +25,11 @@ type Props<T> = {
   pageContext: ReturnType<typeof createPageContext>;
   searchContext: ReturnType<typeof createSearchContext>;
   count: number;
+  /** @deprecated */
   rowKey?: string;
   'data-testid'?: string;
   pageSizeList?: string[];
+  preloadKey?: string;
 };
 
 function Table<T extends Record<string, any>>(props: Props<T>): ReactElement {
@@ -27,53 +39,96 @@ function Table<T extends Record<string, any>>(props: Props<T>): ReactElement {
     pageContext,
     searchContext,
     count,
-    rowKey,
     pageSizeList = PAGE_SIZE_LIST,
+    preloadKey,
   } = props;
   const [{page, pageSize}, {onPageChange}] = usePage(pageContext);
   const [isSearching] = useTableSearchState(searchContext);
-  const colWidth = calcColumnsWidth(columns);
-  const scrollX = colWidth > 0 ? {x: colWidth} : void 0;
 
-  const tableColumns = useMemo(
-    function () {
-      return [
-        {
-          title: '序号',
-          width: 64,
-          render(_: any, __: any, idx: number) {
-            return idx + 1;
-          },
-          align: 'center',
-        },
-        ...columns,
-      ];
-    },
-    [columns],
-  );
+  const [
+    {getHeaderGroups, getRowModel, getCenterTotalSize, active},
+    {onDragEnd, onDragStart},
+  ] = useTable(columns, data, {pageSize, preloadKey});
+
+  const sensor = useSensor(PointerSensor);
 
   return (
-    <OriTable
-      data-testid={props['data-testid']}
-      columns={tableColumns as ColumnsType<T>}
-      dataSource={data}
-      pagination={{
-        pageSize,
-        showQuickJumper: true,
-        pageSizeOptions: pageSizeList,
-        total: count,
-        showSizeChanger: true,
-        onChange: onPageChange,
-        current: page,
-        showTotal(total) {
-          return `共${total}条数据`;
-        },
-      }}
-      rowKey={rowKey ?? 'id'}
-      loading={isSearching}
-      scroll={scrollX}
-      bordered
-    />
+    <Spin spinning={isSearching}>
+      <div className={css.tableWrapper} id='table_wrapper'>
+        <DndContext
+          onDragStart={onDragStart}
+          onDragEnd={onDragEnd}
+          sensors={[sensor]}
+          modifiers={[restrictToHorizontalAxis]}
+        >
+          <SortableContext
+            items={(columns as ColumnType<T>[]).map(val =>
+              val.dataIndex!.toString(),
+            )}
+            strategy={horizontalListSortingStrategy}
+          >
+            <table className={css.table} style={{width: getCenterTotalSize()}}>
+              <thead className={css.tableHead}>
+                {getHeaderGroups().map(function ({id, headers}) {
+                  return (
+                    <tr key={id}>
+                      {headers.map(header => (
+                        <HeaderTh key={header.id} header={header} />
+                      ))}
+                    </tr>
+                  );
+                })}
+              </thead>
+              <tbody className={css.tableBody}>
+                {getRowModel().rows.map(function ({id, getVisibleCells}) {
+                  return (
+                    <tr key={id}>
+                      {getVisibleCells().map(function ({
+                        id,
+                        column,
+                        getContext,
+                      }) {
+                        const align =
+                          (
+                            column.columnDef.meta as {
+                              align: 'left' | 'right' | 'center';
+                            }
+                          )?.align ?? 'left';
+                        return (
+                          <td
+                            key={id}
+                            style={{
+                              width: column.getSize(),
+                              textAlign: align,
+                            }}
+                          >
+                            {flexRender(column.columnDef.cell, getContext())}
+                          </td>
+                        );
+                      })}
+                    </tr>
+                  );
+                })}
+              </tbody>
+            </table>
+            <DragOverlay>
+              {active ? <HeaderTh header={active} useDiv /> : null}
+            </DragOverlay>
+          </SortableContext>
+        </DndContext>
+      </div>
+      <Pagination
+        className={css.pagination}
+        pageSize={pageSize}
+        showQuickJumper
+        pageSizeOptions={pageSizeList}
+        total={count}
+        showSizeChanger
+        onChange={onPageChange}
+        current={page}
+        showTotal={total => `共${total}条数据`}
+      />
+    </Spin>
   );
 }
 

+ 1 - 0
packages/app/src/hooks/index.ts

@@ -18,3 +18,4 @@ export * from './use-filter-db';
 export * from './use-select-filter-options';
 export * from './use-download';
 export * from './use-upload';
+export * from './use-preload-setting-data';

+ 14 - 9
packages/app/src/hooks/use-filter-db/index.ts

@@ -1,3 +1,4 @@
+import {usePreloadSettingData} from '../use-preload-setting-data';
 import {DBSchema, openDB} from 'idb';
 import {useCallback, useEffect, useState} from 'react';
 import {useLocation} from 'react-router-dom';
@@ -12,7 +13,7 @@ type FilterDB = {
   };
 } & DBSchema;
 
-function openFilterDB() {
+export function openFilterDB() {
   return openDB<FilterDB>('filterGroup', 1, {
     upgrade(db) {
       if (!db.objectStoreNames.contains('filterGroup')) {
@@ -22,35 +23,39 @@ function openFilterDB() {
   });
 }
 
-export function useFilterDB() {
-  const [state, setState] = useState('');
+export function useFilterDB(settingId?: string) {
   const {pathname} = useLocation();
+  const key = settingId ?? pathname;
+  const preload = usePreloadSettingData(settingId);
+  const [state, setState] = useState(preload?.conditions ?? '');
 
   const update = useCallback(
     async function () {
       const db = await openFilterDB();
-      const data = await db.get('filterGroup', IDBKeyRange.only(pathname));
+      const data = await db.get('filterGroup', IDBKeyRange.only(key));
       db.close();
       data && setState(data.auths);
     },
-    [pathname],
+    [key],
   );
 
   const set = useCallback(
     async function (auths: string) {
       const db = await openFilterDB();
-      await db.put('filterGroup', {path: pathname, auths});
+      await db.put('filterGroup', {path: key, auths});
       db.close();
       setState(auths);
     },
-    [pathname],
+    [key],
   );
 
   useEffect(
     function () {
-      update();
+      if (preload) {
+        set(preload.conditions);
+      }
     },
-    [update],
+    [preload, set],
   );
 
   return [state, {update, set}] as const;

+ 16 - 0
packages/app/src/hooks/use-preload-setting-data/index.tsx

@@ -0,0 +1,16 @@
+import {SettingsData} from '@models';
+import {useMemo} from 'react';
+import {useLoaderData, useLocation} from 'react-router-dom';
+
+export function usePreloadSettingData(key?: string) {
+  const {pathname} = useLocation();
+  const preloadKey = key ?? pathname;
+  const preloadData = useLoaderData() as Map<string, SettingsData | null>;
+
+  return useMemo(
+    function () {
+      return preloadData.get(preloadKey);
+    },
+    [preloadData, preloadKey],
+  );
+}

+ 3 - 5
packages/app/src/index.tsx

@@ -1,9 +1,9 @@
 import '@icon-park/react/styles/index.css';
 import 'react-contexify/dist/ReactContexify.css';
 import '@styles/index.css';
-import {RootRoutes} from '@routes';
+import {router} from '@routes';
 import {createRoot} from 'react-dom/client';
-import {BrowserRouter} from 'react-router-dom';
+import {RouterProvider} from 'react-router-dom';
 import {QueryClientProvider} from '@tanstack/react-query';
 import {StrictMode, Suspense} from 'react';
 import {ConfigProvider} from 'antd';
@@ -34,9 +34,7 @@ root.render(
         <Suspense
           fallback={<Loading tip='正在加载' width='100vw' height='100vh' />}
         >
-          <BrowserRouter>
-            <RootRoutes />
-          </BrowserRouter>
+          <RouterProvider router={router} />
         </Suspense>
       </ConfigProvider>
     </QueryClientProvider>

+ 23 - 0
packages/app/src/models/request/adjustment.ts

@@ -0,0 +1,23 @@
+/** 提交配置信息 */
+export type AddSettingsParams = {
+  /** 用户id */
+  userId: string;
+  /** 路由 */
+  track: string;
+  /** 查询条件 */
+  conditions: string;
+  /** 表格宽度 */
+  tableWidth: string;
+  /** 表格顺序 */
+  tableOrder: string;
+  /** 页面内数量数量 */
+  pageLimit: string;
+};
+
+/** 查询配置信息 */
+export type GetSettingsParams = {
+  /** 用户id */
+  userId: string;
+  /** 页面路由 */
+  track: string;
+};

+ 1 - 0
packages/app/src/models/request/index.ts

@@ -39,3 +39,4 @@ export * from './gs';
 export * from './inventory';
 export * from './warehousing';
 export * from './traceBack';
+export * from './adjustment';

+ 11 - 0
packages/app/src/models/response/adjustment.ts

@@ -0,0 +1,11 @@
+/** 查询配置信息 */
+export type SettingsData = {
+  /** 查询条件 */
+  conditions: string;
+  /** 表格宽度 */
+  tableWidth: string;
+  /** 表格顺序 */
+  tableOrder: string;
+  /** 页面内数量数量 */
+  pageLimit: string;
+};

+ 1 - 0
packages/app/src/models/response/index.ts

@@ -54,3 +54,4 @@ export * from './materialBind';
 export * from './gs';
 export * from './inventory';
 export * from './traceBack';
+export * from './adjustment';

+ 2 - 0
packages/app/src/pages/container-scrap/table/index.tsx

@@ -68,6 +68,7 @@ const TableList: FC = function () {
         onExport={onExport}
         isRefreshing={isFetching}
         onRefresh={refetch}
+        preloadKey='/container_scrap'
       />
 
       <Table
@@ -77,6 +78,7 @@ const TableList: FC = function () {
         pageContext={pageContext}
         searchContext={searchContext}
         count={count}
+        preloadKey='/container_scrap'
       />
     </Card>
   );

+ 2 - 0
packages/app/src/pages/menu-id/table/index.tsx

@@ -28,6 +28,7 @@ const TableList: FC = function () {
           onAdd={onAdd}
           onRefresh={refetch}
           isRefreshing={isFetching}
+          preloadKey='/menu_second_level'
         />
 
         <Table
@@ -38,6 +39,7 @@ const TableList: FC = function () {
           searchContext={searchContext}
           count={count}
           pageSizeList={MODAL_PAGE_SIZE_LIST}
+          preloadKey='/menu_second_level'
         />
       </Card>
       <MenuModal

+ 281 - 41
packages/app/src/routes/index.tsx

@@ -1,5 +1,4 @@
-import {FC} from 'react';
-import {RouteObject, useRoutes} from 'react-router-dom';
+import {RouteObject, createBrowserRouter} from 'react-router-dom';
 import {
   HOME_PATH,
   MAIN_PATH,
@@ -83,6 +82,43 @@ import {
 } from './routes';
 import Main from '@pages/main';
 import Home from '@pages/home';
+import {userStore} from '@stores';
+import {getSettings} from '@apis';
+import {SettingsData} from '@models';
+
+async function preloadSettings(url: string) {
+  try {
+    const {id} = userStore.getState();
+
+    const result = await getSettings({userId: String(id), track: url});
+
+    if (result.msg === '200') return new Map([[url, result.data]]);
+
+    return new Map([[url, null]]);
+  } catch (error) {
+    return new Map([[url, null]]);
+  }
+}
+
+async function preloadSettingsList(keys: string[]) {
+  const data = await Promise.allSettled([
+    preloadSettings(keys[0]),
+    preloadSettings(keys[1]),
+  ]);
+  const map = new Map<string, SettingsData | null>();
+
+  data.forEach(function (val, idx) {
+    const key = keys[idx];
+
+    if (val.status === 'fulfilled') {
+      map.set(key, val.value.get(key) || null);
+    } else {
+      map.set(key, null);
+    }
+  });
+
+  return map;
+}
 
 export const routes: RouteObject[] = [
   {path: LOGIN_PATH, element: <Login />},
@@ -91,52 +127,256 @@ export const routes: RouteObject[] = [
     element: <Home />,
   },
   {path: MAIN_PATH, element: <Main />},
-  {path: MENU_PATH, element: <Menu />},
-  {path: ROLE_PATH, element: <Role />},
-  {path: USER_PATH, element: <User />},
-  {path: STORAGE_PATH, element: <Storage />},
-  {path: GOODS_PATH, element: <Goods />},
-  {path: PDA_MENU_PATH, element: <Pda />},
-  {path: CONTAINER_PATH, element: <Container />},
-  {path: MATTER_PATH, element: <Matter />},
-  {path: QUALITY_PATH, element: <Quality />},
-  {path: RAW_IN_STREAM_PATH, element: <RawInStream />},
-  {path: RAW_OUT_STREAM_PATH, element: <RawOutStream />},
-  {path: SEMI_REPORT_PATH, element: <SemiReport />},
-  {path: SEMI_DRAW_PATH, element: <SemiDraw />},
-  {path: SEMI_IN_STREAM_PATH, element: <SemiInStream />},
-  {path: SEMI_OUT_STREAM_PATH, element: <SemiOutStream />},
-  {path: FINISH_PRODUCT_IN_STREAM_PATH, element: <ProductInStream />},
-  {path: FINISH_PRODUCT_OUT_STREAM_PATH, element: <ProductOutStream />},
-  {path: DEAD_PRODUCT_PATH, element: <DeadProduct />},
-  {path: RESERVE_WARNING_PATH, element: <ReserveWarning />},
-  {path: PURCHASE_PATH, element: <PurchaseOrder />},
-  {path: MATERIAL_BIND_PATH, element: <MaterialBind />},
-  {path: STOCK_PATH, element: <Stock />},
-  {path: DICTIONARY_PATH, element: <Dictionary />},
-  {path: GS_INTERFACE_LOG_PATH, element: <GSInterfaceLog />},
+  {
+    path: MENU_PATH,
+    element: <Menu />,
+    async loader() {
+      const keys = [MENU_PATH, '/menu_second_level'];
+
+      return preloadSettingsList(keys);
+    },
+  },
+  {
+    path: ROLE_PATH,
+    element: <Role />,
+    async loader() {
+      return await preloadSettings(ROLE_PATH);
+    },
+  },
+  {
+    path: USER_PATH,
+    element: <User />,
+    async loader() {
+      return await preloadSettings(USER_PATH);
+    },
+  },
+  {
+    path: STORAGE_PATH,
+    element: <Storage />,
+    async loader() {
+      return await preloadSettings(USER_PATH);
+    },
+  },
+  {
+    path: GOODS_PATH,
+    element: <Goods />,
+    async loader() {
+      return await preloadSettings(GOODS_PATH);
+    },
+  },
+  {
+    path: PDA_MENU_PATH,
+    element: <Pda />,
+    async loader() {
+      return await preloadSettings(PDA_MENU_PATH);
+    },
+  },
+  {
+    path: CONTAINER_PATH,
+    element: <Container />,
+    async loader() {
+      const keys = [CONTAINER_PATH, '/container_scrap'];
+
+      return preloadSettingsList(keys);
+    },
+  },
+  {
+    path: MATTER_PATH,
+    element: <Matter />,
+    async loader() {
+      return await preloadSettings(MATTER_PATH);
+    },
+  },
+  {
+    path: QUALITY_PATH,
+    element: <Quality />,
+    async loader() {
+      return await preloadSettings(QUALITY_PATH);
+    },
+  },
+  {
+    path: RAW_IN_STREAM_PATH,
+    element: <RawInStream />,
+    async loader() {
+      return await preloadSettings(RAW_IN_STREAM_PATH);
+    },
+  },
+  {
+    path: RAW_OUT_STREAM_PATH,
+    element: <RawOutStream />,
+    async loader() {
+      return await preloadSettings(RAW_OUT_STREAM_PATH);
+    },
+  },
+  {
+    path: SEMI_REPORT_PATH,
+    element: <SemiReport />,
+    async loader() {
+      return await preloadSettings(SEMI_REPORT_PATH);
+    },
+  },
+  {
+    path: SEMI_DRAW_PATH,
+    element: <SemiDraw />,
+    async loader() {
+      return await preloadSettings(SEMI_DRAW_PATH);
+    },
+  },
+  {
+    path: SEMI_IN_STREAM_PATH,
+    element: <SemiInStream />,
+    async loader() {
+      return await preloadSettings(SEMI_IN_STREAM_PATH);
+    },
+  },
+  {
+    path: SEMI_OUT_STREAM_PATH,
+    element: <SemiOutStream />,
+    async loader() {
+      return await preloadSettings(SEMI_OUT_STREAM_PATH);
+    },
+  },
+  {
+    path: FINISH_PRODUCT_IN_STREAM_PATH,
+    element: <ProductInStream />,
+    async loader() {
+      return await preloadSettings(FINISH_PRODUCT_IN_STREAM_PATH);
+    },
+  },
+  {
+    path: FINISH_PRODUCT_OUT_STREAM_PATH,
+    element: <ProductOutStream />,
+    async loader() {
+      return await preloadSettings(FINISH_PRODUCT_OUT_STREAM_PATH);
+    },
+  },
+  {
+    path: DEAD_PRODUCT_PATH,
+    element: <DeadProduct />,
+    async loader() {
+      return await preloadSettings(DEAD_PRODUCT_PATH);
+    },
+  },
+  {
+    path: RESERVE_WARNING_PATH,
+    element: <ReserveWarning />,
+    async loader() {
+      return await preloadSettings(RESERVE_WARNING_PATH);
+    },
+  },
+  {
+    path: PURCHASE_PATH,
+    element: <PurchaseOrder />,
+    async loader() {
+      return await preloadSettings(PURCHASE_PATH);
+    },
+  },
+  {
+    path: MATERIAL_BIND_PATH,
+    element: <MaterialBind />,
+    async loader() {
+      return await preloadSettings(MATERIAL_BIND_PATH);
+    },
+  },
+  {
+    path: STOCK_PATH,
+    element: <Stock />,
+    async loader() {
+      return await preloadSettings(STOCK_PATH);
+    },
+  },
+  {
+    path: DICTIONARY_PATH,
+    element: <Dictionary />,
+    async loader() {
+      return await preloadSettings(DICTIONARY_PATH);
+    },
+  },
+  {
+    path: GS_INTERFACE_LOG_PATH,
+    element: <GSInterfaceLog />,
+    async loader() {
+      return await preloadSettings(GS_INTERFACE_LOG_PATH);
+    },
+  },
   {
     path: PRODUCTION_REQUISITION_PATH,
     element: <ProductionRequisitionOrder />,
+    async loader() {
+      return await preloadSettings(PRODUCTION_REQUISITION_PATH);
+    },
+  },
+  {
+    path: SELL_ORDER_PATH,
+    element: <SellOrder />,
+    async loader() {
+      return await preloadSettings(SELL_ORDER_PATH);
+    },
+  },
+  {
+    path: RELOCAT_ORDER_PATH,
+    element: <RelcationOrder />,
+    async loader() {
+      return await preloadSettings(RELOCAT_ORDER_PATH);
+    },
+  },
+  {
+    path: PRODUCT_REPORT_PATH,
+    element: <ProductReport />,
+    async loader() {
+      return await preloadSettings(PRODUCT_REPORT_PATH);
+    },
+  },
+  {
+    path: ORDER_LOG_PATH,
+    element: <OrderDeleteLog />,
+    async loader() {
+      return await preloadSettings(ORDER_LOG_PATH);
+    },
+  },
+  {
+    path: INVENTORY_PATH,
+    element: <Inventory />,
+    async loader() {
+      return await preloadSettings(INVENTORY_PATH);
+    },
+  },
+  {
+    path: GS_ERROR_LOG_PATH,
+    element: <GSErrorLog />,
+    async loader() {
+      return await preloadSettings(GS_ERROR_LOG_PATH);
+    },
+  },
+  {
+    path: OTHER_IN_PATH,
+    element: <OtherIn />,
+    async loader() {
+      return await preloadSettings(OTHER_IN_PATH);
+    },
+  },
+  {
+    path: OTHER_OUT_PATH,
+    element: <OtherOut />,
+    async loader() {
+      return await preloadSettings(OTHER_OUT_PATH);
+    },
+  },
+  {
+    path: SCREEN_PURCHASE_PATH,
+    element: <ScreenPurchase />,
+  },
+  {
+    path: DELIVER_TRACE_PATH,
+    element: <DeliverTraces />,
+    async loader() {
+      return await preloadSettings(PRODUCTION_REQUISITION_PATH);
+    },
   },
-  {path: SELL_ORDER_PATH, element: <SellOrder />},
-  {path: RELOCAT_ORDER_PATH, element: <RelcationOrder />},
-  {path: PRODUCT_REPORT_PATH, element: <ProductReport />},
-  {path: ORDER_LOG_PATH, element: <OrderDeleteLog />},
-  {path: INVENTORY_PATH, element: <Inventory />},
-  {path: GS_ERROR_LOG_PATH, element: <GSErrorLog />},
-  {path: OTHER_IN_PATH, element: <OtherIn />},
-  {path: OTHER_OUT_PATH, element: <OtherOut />},
   {path: NO_PERMISSION_PATH, element: <NoPermision />},
-  {path: SCREEN_PURCHASE_PATH, element: <ScreenPurchase />},
-  {path: DELIVER_TRACE_PATH, element: <DeliverTraces />},
   {path: '*', element: <NotFound />},
 ];
 
-export const RootRoutes: FC = function () {
-  const Routes = useRoutes(routes);
-
-  return Routes;
-};
+export const router = createBrowserRouter(routes);
 
 export * from './name';

+ 9 - 0
packages/app/src/utils/eventBus.ts

@@ -0,0 +1,9 @@
+import EventEmitter from 'events';
+
+export const eventBus = new EventEmitter();
+
+/** 触发保存操作 要求table将数据发送给cnterer */
+export const SAVE_SETTING_EVENT = 'sendSettingSave';
+
+/** 触发消息接收 接收到消息后进行保存 */
+export const RECEIVE_SETTING_EVENT = 'receiveSetting';

+ 1 - 0
packages/app/src/utils/index.ts

@@ -6,3 +6,4 @@ export * from './types';
 export * from './sortMenu';
 export * from './deleteConfirm';
 export * from './calc-column-width';
+export * from './eventBus';

+ 84 - 3
pnpm-lock.yaml

@@ -116,6 +116,9 @@ importers:
       '@tanstack/react-query':
         specifier: ^4.23.0
         version: 4.23.0(react-dom@18.2.0)(react@18.2.0)
+      '@tanstack/react-table':
+        specifier: ^8.8.5
+        version: 8.8.5(react-dom@18.2.0)(react@18.2.0)
       ahooks:
         specifier: ^3.7.4
         version: 3.7.4(react@18.2.0)
@@ -131,6 +134,9 @@ importers:
       dayjs:
         specifier: ^1.11.7
         version: 1.11.7
+      events:
+        specifier: ^3.3.0
+        version: 3.3.0
       fast-deep-equal:
         specifier: ^3.1.3
         version: 3.1.3
@@ -170,6 +176,12 @@ importers:
       react-router-dom:
         specifier: ^6.8.0
         version: 6.8.0(react-dom@18.2.0)(react@18.2.0)
+      react-sticky-box:
+        specifier: ^2.0.4
+        version: 2.0.4(react@18.2.0)
+      react-stickynode:
+        specifier: ^4.1.0
+        version: 4.1.0(react-dom@18.2.0)(react@18.2.0)
       react-virtualized-auto-sizer:
         specifier: ^1.0.15
         version: 1.0.15(react-dom@18.2.0)(react@18.2.0)
@@ -204,6 +216,9 @@ importers:
       '@total-typescript/ts-reset':
         specifier: ^0.4.2
         version: 0.4.2
+      '@types/events':
+        specifier: ^3.0.0
+        version: 3.0.0
       '@types/lodash-es':
         specifier: ^4.17.6
         version: 4.17.6
@@ -216,6 +231,9 @@ importers:
       '@types/react-modal':
         specifier: ^3.13.1
         version: 3.13.1
+      '@types/react-stickynode':
+        specifier: ^4.0.0
+        version: 4.0.0
       '@types/react-window':
         specifier: ^1.8.5
         version: 1.8.5
@@ -3043,6 +3061,23 @@ packages:
       react-dom: 18.2.0(react@18.2.0)
       use-sync-external-store: 1.2.0(react@18.2.0)
 
+  /@tanstack/react-table@8.8.5(react-dom@18.2.0)(react@18.2.0):
+    resolution: {integrity: sha512-g/t21E/ICHvaCOJOhsDNr5QaB/6aDQEHFbx/YliwwU/CJThMqG+dS28vnToIBV/5MBgpeXoGRi2waDJVJlZrtg==}
+    engines: {node: '>=12'}
+    peerDependencies:
+      react: '>=16'
+      react-dom: '>=16'
+    dependencies:
+      '@tanstack/table-core': 8.8.5
+      react: 18.2.0
+      react-dom: 18.2.0(react@18.2.0)
+    dev: false
+
+  /@tanstack/table-core@8.8.5:
+    resolution: {integrity: sha512-Xnwa1qxpgvSW7ozLiexmKp2PIYcLBiY/IizbdGriYCL6OOHuZ9baRhrrH51zjyz+61ly6K59rmt6AI/3RR+97Q==}
+    engines: {node: '>=12'}
+    dev: false
+
   /@testing-library/dom@8.19.0:
     resolution: {integrity: sha512-6YWYPPpxG3e/xOo6HIWwB/58HukkwIVTOaZ0VwdMVjhRUX/01E4FtQbck9GazOOj7MXHc5RBzMrU86iBJHbI+A==}
     engines: {node: '>=12'}
@@ -3239,6 +3274,10 @@ packages:
     resolution: {integrity: sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==}
     dev: true
 
+  /@types/events@3.0.0:
+    resolution: {integrity: sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==}
+    dev: true
+
   /@types/express-serve-static-core@4.17.31:
     resolution: {integrity: sha512-DxMhY+NAsTwMMFHBTtJFNp5qiHKJ7TeqOo23zVEM9alT1Ml27Q3xcTH0xwxn7Q0BbMcVEJOs/7aQtUWupUQN3Q==}
     dependencies:
@@ -3382,6 +3421,12 @@ packages:
       '@types/react': 18.0.27
     dev: true
 
+  /@types/react-stickynode@4.0.0:
+    resolution: {integrity: sha512-PKkmOzF6WCNuyIKrvhidGeUPLfe8htPwfEljKnQBF4bA5v74ADvXtwkjavOH8i6aCSw9J14AyDDl1Ul0VNQJUg==}
+    dependencies:
+      '@types/react': 18.0.27
+    dev: true
+
   /@types/react-window@1.8.5:
     resolution: {integrity: sha512-V9q3CvhC9Jk9bWBOysPGaWy/Z0lxYcTXLtLipkt2cnRj1JOSFNF7wqGpkScSXMgBwC+fnVRg/7shwgddBG5ICw==}
     dependencies:
@@ -4976,7 +5021,6 @@ packages:
   /core-js@3.26.1:
     resolution: {integrity: sha512-21491RRQVzUn0GGM9Z1Jrpr6PNPxPi+Za8OM9q4tksTSnlbXXGKK1nXNg/QvwFYettXvSX6zWKCtHHfjN4puyA==}
     requiresBuild: true
-    dev: true
 
   /core-util-is@1.0.2:
     resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==}
@@ -6300,13 +6344,16 @@ packages:
     resolution: {integrity: sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg==}
     dev: false
 
+  /eventemitter3@3.1.2:
+    resolution: {integrity: sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==}
+    dev: false
+
   /eventemitter3@4.0.7:
     resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==}
 
   /events@3.3.0:
     resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
     engines: {node: '>=0.8.x'}
-    dev: true
 
   /execa@4.1.0:
     resolution: {integrity: sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==}
@@ -10334,7 +10381,6 @@ packages:
     resolution: {integrity: sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==}
     dependencies:
       performance-now: 2.1.0
-    dev: true
 
   /randombytes@2.1.0:
     resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==}
@@ -11025,6 +11071,29 @@ packages:
       react-transition-group: 2.9.0(react-dom@18.2.0)(react@18.2.0)
     dev: false
 
+  /react-sticky-box@2.0.4(react@18.2.0):
+    resolution: {integrity: sha512-EcnT4fpyyrBwyy5tsWViBTFBMsJZijDC78s2D/GlqrKuU0vsmW+jNc/Ubs1C4/HF/YmTZ90Y5GIGLQMzScQQbA==}
+    peerDependencies:
+      react: '>=16.8.0'
+    dependencies:
+      react: 18.2.0
+    dev: false
+
+  /react-stickynode@4.1.0(react-dom@18.2.0)(react@18.2.0):
+    resolution: {integrity: sha512-zylWgfad75jLfh/gYIayDcDWIDwO4weZrsZqDpjZ/axhF06zRjdCWFBgUr33Pvv2+htKWqPSFksWTyB6aMQ1ZQ==}
+    peerDependencies:
+      react: ^0.14.2 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0
+      react-dom: ^0.14.2 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0
+    dependencies:
+      classnames: 2.3.2
+      core-js: 3.26.1
+      prop-types: 15.8.1
+      react: 18.2.0
+      react-dom: 18.2.0(react@18.2.0)
+      shallowequal: 1.1.0
+      subscribe-ui-event: 2.0.7
+    dev: false
+
   /react-transition-group@2.9.0(react-dom@18.2.0)(react@18.2.0):
     resolution: {integrity: sha512-+HzNTCHpeQyl4MJ/bdE0u6XRMe9+XG/+aL4mCxVN4DnPBQ0/5bfHWPDuOZUzYdMj94daZaZdCCc1Dzt9R/xSSg==}
     peerDependencies:
@@ -11519,6 +11588,10 @@ packages:
       kind-of: 6.0.3
     dev: true
 
+  /shallowequal@1.1.0:
+    resolution: {integrity: sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==}
+    dev: false
+
   /shebang-command@2.0.0:
     resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
     engines: {node: '>=8'}
@@ -11977,6 +12050,14 @@ packages:
     resolution: {integrity: sha512-GP6WDNWf+o403jrEp9c5jibKavrtLW+/qYGhFxFrG8maXhwTBI7gLLhiBb0o7uFccWN+EOS9aMO6cGHWAO07OA==}
     dev: false
 
+  /subscribe-ui-event@2.0.7:
+    resolution: {integrity: sha512-Acrtf9XXl6lpyHAWYeRD1xTPUQHDERfL4GHeNuYAtZMc4Z8Us2iDBP0Fn3xiRvkQ1FO+hx+qRLmPEwiZxp7FDQ==}
+    dependencies:
+      eventemitter3: 3.1.2
+      lodash: 4.17.21
+      raf: 3.4.1
+    dev: false
+
   /superjson@1.12.0:
     resolution: {integrity: sha512-B4tefmFqj8KDShHi2br2rz0kBlUJuQHtxMCydEuHvooL+6EscROTNWRfOLMDxW1dS/daK2zZr3C3N9DU+jXATQ==}
     engines: {node: '>=10'}