Skip to content

Commit 5428d51

Browse files
committed
feat: 添加完整的table相关的组件和hook
1 parent 5f62215 commit 5428d51

File tree

6 files changed

+216
-2
lines changed

6 files changed

+216
-2
lines changed

src/features/tab/tabHooks.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,7 @@ export function useTabManager() {
276276

277277
const updateTabs = useUpdateTabs();
278278

279-
function _addTab(route: Router.Route<unknown, unknown>) {
279+
function _addTab(route: Router.Route) {
280280
const tab = getTabByRoute(route);
281281

282282
if (!isInit.current) {

src/features/table/DragContent.tsx

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import type { DragEndEvent } from '@dnd-kit/core';
2+
import { DndContext } from '@dnd-kit/core';
3+
import { SortableContext, arrayMove, useSortable, verticalListSortingStrategy } from '@dnd-kit/sortable';
4+
import { CSS } from '@dnd-kit/utilities';
5+
import type { FC } from 'react';
6+
import React from 'react';
7+
8+
interface Props {
9+
columns: AntDesign.TableColumnCheck[];
10+
setColumnChecks: (checks: AntDesign.TableColumnCheck[]) => void;
11+
}
12+
13+
/** 单个可拖拽项组件 */
14+
const SortableItem: FC<{
15+
index: number;
16+
item: AntDesign.TableColumnCheck;
17+
onCheckChange: (oldValue: boolean, index: number) => void;
18+
}> = ({ index, item, onCheckChange }) => {
19+
// 使用 useSortable 获取拖拽属性
20+
const { attributes, listeners, setNodeRef, transform, transition } = useSortable({
21+
id: item.key // 每个可拖拽对象的唯一标识
22+
});
23+
24+
// inline 样式,用于在拖拽时移动/过渡
25+
const style: React.CSSProperties = {
26+
transform: CSS.Transform.toString(transform),
27+
transition
28+
};
29+
30+
return (
31+
<div
32+
ref={setNodeRef}
33+
style={style}
34+
// 把拖拽所需的属性都附加上去
35+
{...attributes}
36+
{...listeners}
37+
className="h-36px flex-y-center rd-4px hover:(bg-primary bg-opacity-20)"
38+
>
39+
<IconMdiDrag className="mr-8px h-full cursor-move text-icon" />
40+
<ACheckbox
41+
checked={item.checked}
42+
className="none_draggable flex-1"
43+
onClick={() => onCheckChange(item.checked, index)}
44+
>
45+
{item.title}
46+
</ACheckbox>
47+
</div>
48+
);
49+
};
50+
51+
/** 列表容器组件 */
52+
const DragContent: FC<Props> = ({ columns, setColumnChecks }) => {
53+
// 拖拽结束时的回调
54+
const handleDragEnd = (event: DragEndEvent) => {
55+
const { active, over } = event;
56+
if (!over) return;
57+
58+
// 如果拖拽开始和结束位置的 id 不同,则表示有重新排序
59+
if (active.id !== over.id) {
60+
const oldIndex = columns.findIndex(item => item.key === active.id);
61+
const newIndex = columns.findIndex(item => item.key === over.id);
62+
63+
// arrayMove 是 DnD Kit 提供的辅助函数,用于在数组中移动元素
64+
const newColumns = arrayMove(columns, oldIndex, newIndex);
65+
setColumnChecks(newColumns);
66+
}
67+
};
68+
69+
// 点击复选框时更改“checked”
70+
const handleChange = (value: boolean, index: number) => {
71+
columns[index].checked = !value;
72+
// 这里要注意保持新数组引用,确保触发 React 重新渲染
73+
setColumnChecks([...columns]);
74+
};
75+
76+
return (
77+
// DndContext 相当于顶层的拖拽环境容器
78+
<DndContext onDragEnd={handleDragEnd}>
79+
{/*
80+
SortableContext 用于告诉 DnD Kit,这个区域内的一组元素可以“排序”;
81+
items 传入当前这批可排序对象的 key(或整个对象,但 key 必须唯一);
82+
strategy 指定排序策略,如 verticalListSortingStrategy 适合竖直列表。
83+
*/}
84+
<SortableContext
85+
items={columns.map(item => item.key)}
86+
strategy={verticalListSortingStrategy}
87+
>
88+
{columns.map((item, index) => (
89+
<SortableItem
90+
index={index}
91+
item={item}
92+
key={item.key}
93+
onCheckChange={handleChange}
94+
/>
95+
))}
96+
</SortableContext>
97+
</DndContext>
98+
);
99+
};
100+
101+
export default DragContent;
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { Button, Popconfirm, Space } from 'antd';
2+
import type { SpaceProps } from 'antd';
3+
import classNames from 'classnames';
4+
import React from 'react';
5+
import type { FC } from 'react';
6+
7+
import DragContent from './DragContent';
8+
9+
interface Props {
10+
add: () => void;
11+
children?: React.ReactNode;
12+
columns: AntDesign.TableColumnCheck[];
13+
disabledDelete?: boolean;
14+
itemAlign?: SpaceProps['align'];
15+
loading?: boolean;
16+
onDelete: () => void;
17+
prefix?: React.ReactNode;
18+
refresh: () => void;
19+
setColumnChecks: (checks: AntDesign.TableColumnCheck[]) => void;
20+
suffix?: React.ReactNode;
21+
}
22+
23+
const TableHeaderOperation: FC<Props> = ({
24+
add,
25+
children,
26+
columns,
27+
disabledDelete,
28+
itemAlign,
29+
loading,
30+
onDelete,
31+
prefix,
32+
refresh,
33+
setColumnChecks,
34+
suffix
35+
}) => {
36+
const { t } = useTranslation();
37+
38+
return (
39+
<Space
40+
wrap
41+
align={itemAlign}
42+
className="lt-sm:w-200px"
43+
>
44+
{prefix}
45+
{children || (
46+
<>
47+
<Button
48+
ghost
49+
icon={<IconIcRoundPlus className="text-icon" />}
50+
size="small"
51+
type="primary"
52+
onClick={add}
53+
>
54+
{t('common.add')}
55+
</Button>
56+
<Popconfirm
57+
title={t('common.confirmDelete')}
58+
onConfirm={onDelete}
59+
>
60+
<Button
61+
danger
62+
ghost
63+
disabled={disabledDelete}
64+
icon={<IconIcRoundDelete className="text-icon" />}
65+
size="small"
66+
>
67+
{t('common.batchDelete')}
68+
</Button>
69+
</Popconfirm>
70+
</>
71+
)}
72+
<Button
73+
icon={<IconMdiRefresh className={classNames('text-icon', { 'animate-spin': loading })} />}
74+
size="small"
75+
onClick={refresh}
76+
>
77+
{t('common.refresh')}
78+
</Button>
79+
80+
<APopover
81+
placement="bottomRight"
82+
trigger="click"
83+
content={
84+
<DragContent
85+
columns={columns}
86+
setColumnChecks={setColumnChecks}
87+
/>
88+
}
89+
>
90+
<Button
91+
icon={<IconAntDesignSettingOutlined />}
92+
size="small"
93+
>
94+
{t('common.columnSetting')}
95+
</Button>
96+
</APopover>
97+
98+
{suffix}
99+
</Space>
100+
);
101+
};
102+
103+
export default TableHeaderOperation;

src/features/table/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1+
export { default as TableHeaderOperation } from './TableHeaderOperation';
12
export * from './use-table';

src/features/table/use-table.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ export function useTable<A extends AntDesign.TableApiFn>(
134134
form,
135135
reset,
136136
search: run,
137-
searchParams
137+
searchParams: searchParams as NonNullable<Parameters<A>[0]>
138138
},
139139
setColumnChecks,
140140
tableProps: {

src/types/auto-imports.d.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ declare global {
2525
const AMenu: typeof import('antd')['Menu']
2626
const AModal: typeof import('antd')['Modal']
2727
const APopconfirm: typeof import('antd')['Popconfirm']
28+
const APopover: typeof import('antd')['Popover']
2829
const ARow: typeof import('antd')['Row']
2930
const ASegmented: typeof import('antd')['Segmented']
3031
const ASpace: typeof import('antd')['Space']
@@ -46,16 +47,21 @@ declare global {
4647
const IconAntDesignInboxOutlined: typeof import('~icons/ant-design/inbox-outlined.tsx')['default']
4748
const IconAntDesignReloadOutlined: typeof import('~icons/ant-design/reload-outlined.tsx')['default']
4849
const IconAntDesignSendOutlined: typeof import('~icons/ant-design/send-outlined.tsx')['default']
50+
const IconAntDesignSettingOutlined: typeof import('~icons/ant-design/setting-outlined.tsx')['default']
4951
const IconGridiconsFullscreen: typeof import('~icons/gridicons/fullscreen.tsx')['default']
5052
const IconGridiconsFullscreenExit: typeof import('~icons/gridicons/fullscreen-exit.tsx')['default']
53+
const IconIcRoundDelete: typeof import('~icons/ic/round-delete.tsx')['default']
54+
const IconIcRoundPlus: typeof import('~icons/ic/round-plus.tsx')['default']
5155
const IconIcRoundRefresh: typeof import('~icons/ic/round-refresh.tsx')['default']
5256
const IconIcRoundSearch: typeof import('~icons/ic/round-search.tsx')['default']
5357
const IconLocalBanner: typeof import('~icons/local/banner.tsx')['default']
5458
const IconLocalLogo: typeof import('~icons/local/logo.tsx')['default']
5559
const IconMdiArrowDownThin: typeof import('~icons/mdi/arrow-down-thin.tsx')['default']
5660
const IconMdiArrowUpThin: typeof import('~icons/mdi/arrow-up-thin.tsx')['default']
61+
const IconMdiDrag: typeof import('~icons/mdi/drag.tsx')['default']
5762
const IconMdiKeyboardEsc: typeof import('~icons/mdi/keyboard-esc.tsx')['default']
5863
const IconMdiKeyboardReturn: typeof import('~icons/mdi/keyboard-return.tsx')['default']
64+
const IconMdiRefresh: typeof import('~icons/mdi/refresh.tsx')['default']
5965
const IconUilSearch: typeof import('~icons/uil/search.tsx')['default']
6066
const Link: typeof import('react-router-dom')['Link']
6167
const LookForward: typeof import('../components/LookForward')['default']
@@ -169,6 +175,9 @@ declare global {
169175
const useState: typeof import('react')['useState']
170176
const useSvgIcon: typeof import('../hooks/common/icon')['useSvgIcon']
171177
const useSyncExternalStore: typeof import('react')['useSyncExternalStore']
178+
const useTable: typeof import('../hooks/common/table')['useTable']
179+
const useTableOperate: typeof import('../hooks/common/table')['useTableOperate']
180+
const useTableScroll: typeof import('../hooks/common/table')['useTableScroll']
172181
const useTextSelection: typeof import('ahooks')['useTextSelection']
173182
const useTheme: typeof import('ahooks')['useTheme']
174183
const useThrottle: typeof import('ahooks')['useThrottle']

0 commit comments

Comments
 (0)