소스 검색

feat: 增加多选和多选删除功能

xyh 2 년 전
부모
커밋
407a374ae2

+ 1 - 1
package.json

@@ -44,7 +44,7 @@
     "commitizen": "^4.3.0",
     "cz-conventional-changelog": "^3.3.0",
     "eslint": "^8.29.0",
-    "eslint-config-proste": "^6.1.1",
+    "eslint-config-proste": "^7.4.0",
     "eslint-plugin-cypress": "^2.12.1",
     "eslint-plugin-import": "^2.27.5",
     "eslint-plugin-import-newlines": "^1.3.1",

+ 96 - 22
packages/app/src/components/table/hooks.ts

@@ -3,9 +3,18 @@ import {
   useReactTable,
   getCoreRowModel,
   Header,
+  RowSelectionState,
 } from '@tanstack/react-table';
 import {ColumnType} from 'antd/es/table';
-import {useEffect, useMemo, useState} from 'react';
+import {
+  Dispatch,
+  FC,
+  SetStateAction,
+  useEffect,
+  useMemo,
+  useRef,
+  useState,
+} from 'react';
 import {DragStartEvent, DragEndEvent} from '@dnd-kit/core';
 import {arrayMove} from '@dnd-kit/sortable';
 import {
@@ -14,8 +23,41 @@ import {
   useTableSearchDispatch,
 } from '@hooks';
 
-function parseColumn<T>(columns: ColumnType<T>[]) {
-  const hepler = createColumnHelper<Record<string, any>>();
+type TableCheckProps = {
+  checked: boolean;
+  indeterminate: boolean;
+  onChange: (e: unknown) => void;
+  enabled?: boolean;
+};
+
+const TableCheck: FC<TableCheckProps> = function({
+  checked,
+  indeterminate,
+  onChange,
+  enabled = true,
+}) {
+  const inputRef = useRef<HTMLInputElement>(null);
+
+  useEffect(function() {
+    if (!inputRef.current) return;
+
+    inputRef.current.indeterminate = !checked && indeterminate;
+  }, [checked, indeterminate]);
+
+  return (
+    <input
+      ref={inputRef}
+      type="checkbox"
+      checked={checked}
+      disabled={!enabled}
+      onChange={onChange}
+      style={{cursor: 'pointer'}}
+    />
+  );
+};
+
+function parseColumn<T>(columns: ColumnType<T>[], enableSelect?: boolean) {
+  const helper = createColumnHelper<Record<string, any>>();
 
   const tableColumns = columns.map(function(val) {
     const {dataIndex, title, render, align, fixed} = val;
@@ -26,7 +68,7 @@ function parseColumn<T>(columns: ColumnType<T>[]) {
     };
 
     if (render) {
-      return hepler.display({
+      return helper.display({
         id: dataIndex!.toString(),
         header: title?.toString(),
         cell(props) {
@@ -40,7 +82,7 @@ function parseColumn<T>(columns: ColumnType<T>[]) {
       });
     }
 
-    return hepler.accessor(dataIndex!.toString(), {
+    return helper.accessor(dataIndex!.toString(), {
       header: title?.toString(),
       cell: props => props.getValue(),
       minSize: 0,
@@ -48,8 +90,37 @@ function parseColumn<T>(columns: ColumnType<T>[]) {
     });
   });
 
+  enableSelect && tableColumns.unshift(
+    helper.accessor('select', {
+      header({table}) {
+        return (
+          <TableCheck
+            checked={table.getIsAllRowsSelected()}
+            indeterminate={table.getIsSomeRowsSelected()}
+            onChange={table.getToggleAllRowsSelectedHandler()}
+          />
+        );
+      },
+      cell({row}) {
+        return (
+          <TableCheck
+            checked={row.getIsSelected()}
+            indeterminate={row.getIsSomeSelected()}
+            onChange={row.getToggleSelectedHandler()}
+            enabled={row.getCanSelect()}
+          />
+        );
+      },
+      meta: {
+        align: 'center',
+        fixed: false,
+        disabledSort: true,
+      },
+    }),
+  );
+
   tableColumns.unshift(
-    hepler.display({
+    helper.display({
       header: '序号',
       id: 'no',
       cell({row}) {
@@ -73,40 +144,41 @@ export function useTable<T extends Record<string, any>>(
     pageSize: number;
     preloadKey?: string;
     searchContext: ReturnType<typeof createSearchContext>;
+    rowSelection?: RowSelectionState;
+    setRowSelection?: Dispatch<SetStateAction<RowSelectionState>>;
+    enableSelect?: boolean;
   },
 ) {
+  const {enableSelect} = options;
   const columnList = useMemo(
     function() {
-      return parseColumn(columns);
+      return parseColumn(columns, enableSelect);
     },
-    [columns],
+    [columns, enableSelect],
   );
   const preload = usePreloadSettingData(options.preloadKey);
 
   const [columnSizing, setColumnSizing] = useState(function() {
-    if (preload?.tableWidth) {
+    if (preload?.tableWidth)
       return JSON.parse(preload.tableWidth) as Record<string, number>;
-    }
 
-    const obj: Record<string, number> = {no: 64};
+    const obj: Record<string, number> = {no: 64, select: 20};
 
     columns.forEach(function({dataIndex, width}) {
-      if (dataIndex && width) {
+      if (dataIndex && width)
         obj[dataIndex.toString()] = Number(width);
-      }
     });
 
     return obj;
   });
 
   const [columnOrder, setColumnOrder] = useState(function() {
-    if (preload?.tableOrder) {
+    if (preload?.tableOrder)
       return JSON.parse(preload?.tableOrder) as string[];
-    }
 
     const nextList = columns.map(val => val.dataIndex!.toString());
 
-    return ['no', ...nextList];
+    return ['select', 'no', ...nextList];
   });
 
   const dispatch = useTableSearchDispatch(options.searchContext);
@@ -124,17 +196,21 @@ export function useTable<T extends Record<string, any>>(
     [columnOrder, columnSizing, dispatch],
   );
 
+  const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
+
   const table = useReactTable({
     data,
     columns: columnList,
     state: {
       columnSizing,
       columnOrder,
+      rowSelection,
     },
     getCoreRowModel: getCoreRowModel(),
     onColumnSizingChange: setColumnSizing,
     columnResizeMode: 'onChange',
     onColumnOrderChange: setColumnOrder,
+    onRowSelectionChange: setRowSelection,
   });
 
   const [active, setActive] = useState<Header<
@@ -149,22 +225,20 @@ export function useTable<T extends Record<string, any>>(
   function onDragEnd({active, over}: DragEndEvent) {
     setActive(null);
 
-    if (!over) {
+    if (!over)
       return;
-    }
 
     const {id: activeId} = active,
           {id: overId} = over;
     const {disabledSort} = (over.data.current as any).header.column.columnDef
       .meta;
 
-    if (disabledSort) {
+    if (disabledSort)
       return;
-    }
 
-    if (activeId === overId) {
+    if (activeId === overId)
       return;
-    }
+
     const {setColumnOrder} = table;
 
     setColumnOrder(function(prev) {

+ 16 - 5
packages/app/src/components/table/index.tsx

@@ -7,9 +7,9 @@ import {
   useTableSearchState,
 } from '@hooks';
 import {PAGE_SIZE_LIST} from '@utils';
-import {ReactElement} from 'react';
+import {Dispatch, ReactElement, SetStateAction} from 'react';
 import {useTable, useTableShadow} from './hooks';
-import {flexRender} from '@tanstack/react-table';
+import {RowSelectionState, flexRender} from '@tanstack/react-table';
 import css from './index.module.css';
 import HeaderTh from './header-th';
 import {
@@ -32,11 +32,14 @@ type Props<T> = {
   searchContext: ReturnType<typeof createSearchContext>;
   count: number;
   /** @deprecated */
-  rowKey?: string;
+  rawKey?: string;
   'data-testid'?: string;
   pageSizeList?: string[];
   preloadKey?: string;
   isSecondLevel?: boolean;
+  enableSelect?: boolean;
+  rowSelection?: RowSelectionState;
+  setRowSelection?: Dispatch<SetStateAction<RowSelectionState>>;
 };
 
 function Table<T extends Record<string, any>>(props: Props<T>): ReactElement {
@@ -49,6 +52,8 @@ function Table<T extends Record<string, any>>(props: Props<T>): ReactElement {
     pageSizeList = PAGE_SIZE_LIST,
     preloadKey,
     isSecondLevel,
+    rowSelection,
+    setRowSelection,
   } = props;
   const [{page, pageSize}, {onPageChange}] = usePage(pageContext);
   const [isSearching] = useTableSearchState(searchContext);
@@ -56,7 +61,13 @@ function Table<T extends Record<string, any>>(props: Props<T>): ReactElement {
   const [
     {getHeaderGroups, getRowModel, getCenterTotalSize, active},
     {onDragEnd, onDragStart},
-  ] = useTable(columns, data, {pageSize, preloadKey, searchContext});
+  ] = useTable(columns, data, {
+    pageSize,
+    preloadKey,
+    searchContext,
+    rowSelection,
+    setRowSelection,
+  });
 
   const sensor = useSensor(PointerSensor);
 
@@ -66,7 +77,7 @@ function Table<T extends Record<string, any>>(props: Props<T>): ReactElement {
     <Spin spinning={isSearching}>
       <div
         className={css.tableWrapper}
-        id='table_wrapper'
+        id="table_wrapper"
         data-testid={props['data-testid']}
       >
         <DndContext

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

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

+ 66 - 0
packages/app/src/hooks/use-table-row-select/index.tsx

@@ -0,0 +1,66 @@
+import {BaseResult} from '@models';
+import {useMutation} from '@tanstack/react-query';
+import {RowSelectionState} from '@tanstack/react-table';
+import {deleteConfirm} from '@utils';
+import {message} from 'antd';
+import {useCallback, useEffect, useState} from 'react';
+
+export function useTableRowSelect<T extends Record<string, unknown>>(
+  data: T[],
+  fn: (params: string[]) => BaseResult,
+  options: {
+    label: string;
+    refetch: () => void;
+    rawKey?: keyof T;
+  },
+) {
+  const {refetch, label, rawKey} = options;
+  const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
+
+  // 数据变化后重置已选择内容防止选择与实际数据不符
+  useEffect(function() {
+    setRowSelection({});
+  }, [data]);
+
+  const {isLoading, mutate} = useMutation({
+    mutationFn: fn,
+    onSuccess({msg}) {
+      if (msg === '200') {
+        refetch();
+        message.success(`${label}批量删除成功`);
+      }
+    },
+  });
+
+  const onBatchDelete = useCallback(
+    function() {
+      const params: string[] = [];
+
+      for (const [key, value] of Object.entries(rowSelection)) {
+        if (value) {
+          // key返回的对应的数组索引
+          const id = data[Number(key)][rawKey ?? 'id'] as string;
+
+          params.push(id);
+        }
+      }
+
+      deleteConfirm(
+        label,
+        function() {
+          mutate(params);
+        },
+        {
+          title: `${label}批量删除`,
+          content: `您确定要删除${params.length}个${label}吗?`,
+        },
+      );
+    },
+    [data, label, mutate, rawKey, rowSelection],
+  );
+
+  return [
+    {rowSelection, isBatchDeleteing: isLoading},
+    {onBatchDelete, setRowSelection},
+  ] as const;
+}

+ 3 - 3
packages/app/src/utils/deleteConfirm.ts

@@ -3,13 +3,13 @@ import {Modal} from 'antd';
 export function deleteConfirm(
   label: string,
   onOk: () => void,
-  options?: {title?: string, extra?: string},
+  options?: {title?: string, extra?: string, content?: string},
 ) {
-  const {title, extra} = options ?? {};
+  const {title, extra, content} = options ?? {};
 
   Modal.confirm({
     title: title ?? `删除${label}`,
-    content: `你确定要删除当前${label}吗?${extra ?? ''}`,
+    content: content ?? `你确定要删除当前${label}吗?${extra ?? ''}`,
     onOk,
   });
 }

+ 7 - 4
pnpm-lock.yaml

@@ -36,8 +36,8 @@ importers:
         specifier: ^8.29.0
         version: 8.29.0
       eslint-config-proste:
-        specifier: ^6.1.1
-        version: 6.1.1(@typescript-eslint/eslint-plugin@5.46.1)(@typescript-eslint/parser@5.46.1)(eslint-plugin-import-newlines@1.3.1)(eslint-plugin-import@2.27.5)(eslint-plugin-react-hooks@4.6.0)(eslint-plugin-react@7.32.2)(eslint@8.29.0)
+        specifier: ^7.4.0
+        version: 7.4.0(@typescript-eslint/eslint-plugin@5.46.1)(@typescript-eslint/parser@5.46.1)(eslint-plugin-import-newlines@1.3.1)(eslint-plugin-import@2.27.5)(eslint-plugin-react-hooks@4.6.0)(eslint-plugin-react@7.32.2)(eslint@8.29.0)
       eslint-plugin-cypress:
         specifier: ^2.12.1
         version: 2.12.1(eslint@8.29.0)
@@ -5979,8 +5979,8 @@ packages:
       source-map: 0.6.1
     dev: true
 
-  /eslint-config-proste@6.1.1(@typescript-eslint/eslint-plugin@5.46.1)(@typescript-eslint/parser@5.46.1)(eslint-plugin-import-newlines@1.3.1)(eslint-plugin-import@2.27.5)(eslint-plugin-react-hooks@4.6.0)(eslint-plugin-react@7.32.2)(eslint@8.29.0):
-    resolution: {integrity: sha512-4SbgCFO9Hub42Ui3/bQi870BZJeqFUNZEbSe29URA6pfg0tn15vh9wRAXjzkdz4PhpIhNCSjzeQsUDhhGEkp9A==}
+  /eslint-config-proste@7.4.0(@typescript-eslint/eslint-plugin@5.46.1)(@typescript-eslint/parser@5.46.1)(eslint-plugin-import-newlines@1.3.1)(eslint-plugin-import@2.27.5)(eslint-plugin-react-hooks@4.6.0)(eslint-plugin-react@7.32.2)(eslint@8.29.0):
+    resolution: {integrity: sha512-DJckpPELQCwqoXN8ucan54NRbhg0ry2x5ELnUtAJYEX1NCj8HTqrBfOkE0+UZVSyNfANLFI7+jUx2/hBXGU8+A==}
     peerDependencies:
       '@typescript-eslint/eslint-plugin': '>=5.4.0'
       '@typescript-eslint/parser': '>=5.4.0'
@@ -5989,6 +5989,7 @@ packages:
       eslint-plugin-import-newlines: '>=1.3.0'
       eslint-plugin-react: '>=7.27.1'
       eslint-plugin-react-hooks: '>=4.3.0'
+      eslint-plugin-vue: '>=9.13.0'
     peerDependenciesMeta:
       '@typescript-eslint/eslint-plugin':
         optional: true
@@ -6000,6 +6001,8 @@ packages:
         optional: true
       eslint-plugin-react-hooks:
         optional: true
+      eslint-plugin-vue:
+        optional: true
     dependencies:
       '@typescript-eslint/eslint-plugin': 5.46.1(@typescript-eslint/parser@5.46.1)(eslint@8.29.0)(typescript@4.9.4)
       '@typescript-eslint/parser': 5.46.1(eslint@8.29.0)(typescript@4.9.4)