@creekjs/web-components 1.0.5 → 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-father$colon$build.log +35 -19
- package/README.md +97 -18
- package/dist/creek-config-provider/CreekConfigContext.d.ts +4 -0
- package/dist/creek-config-provider/CreekConfigContext.d.ts.map +1 -1
- package/dist/creek-config-provider/CreekConfigContext.js.map +2 -2
- package/dist/creek-config-provider/CreekI18nProvider.d.ts +22 -0
- package/dist/creek-config-provider/CreekI18nProvider.d.ts.map +1 -0
- package/dist/creek-config-provider/CreekI18nProvider.js +92 -0
- package/dist/creek-config-provider/CreekI18nProvider.js.map +7 -0
- package/dist/creek-config-provider/index.d.ts +5 -3
- package/dist/creek-config-provider/index.d.ts.map +1 -1
- package/dist/creek-config-provider/index.js +47 -4
- package/dist/creek-config-provider/index.js.map +3 -3
- package/dist/creek-hooks/useApp/index.d.ts +3 -3
- package/dist/creek-keep-alive/index.d.ts +24 -1
- package/dist/creek-keep-alive/index.d.ts.map +1 -1
- package/dist/creek-keep-alive/index.js +141 -4
- package/dist/creek-keep-alive/index.js.map +2 -2
- package/dist/creek-layout/ActionRender/FullScreen.d.ts.map +1 -1
- package/dist/creek-layout/ActionRender/FullScreen.js +3 -1
- package/dist/creek-layout/ActionRender/FullScreen.js.map +2 -2
- package/dist/creek-layout/ActionRender/LayoutSettings.d.ts +5 -0
- package/dist/creek-layout/ActionRender/LayoutSettings.d.ts.map +1 -0
- package/dist/creek-layout/ActionRender/LayoutSettings.js +73 -0
- package/dist/creek-layout/ActionRender/LayoutSettings.js.map +7 -0
- package/dist/creek-layout/ActionRender/UserInfo.js.map +2 -2
- package/dist/creek-layout/ActionRender/index.d.ts +1 -0
- package/dist/creek-layout/ActionRender/index.d.ts.map +1 -1
- package/dist/creek-layout/ActionRender/index.js +3 -0
- package/dist/creek-layout/ActionRender/index.js.map +2 -2
- package/dist/creek-layout/index.d.ts +5 -5
- package/dist/creek-layout/index.d.ts.map +1 -1
- package/dist/creek-layout/index.js +79 -16
- package/dist/creek-layout/index.js.map +3 -3
- package/dist/creek-layout/useLayoutSettingsStore.d.ts +20 -0
- package/dist/creek-layout/useLayoutSettingsStore.d.ts.map +1 -0
- package/dist/creek-layout/useLayoutSettingsStore.js +45 -0
- package/dist/creek-layout/useLayoutSettingsStore.js.map +7 -0
- package/dist/creek-locale-button/index.d.ts +1 -0
- package/dist/creek-locale-button/index.d.ts.map +1 -0
- package/dist/creek-locale-button/index.js +66 -0
- package/dist/creek-locale-button/index.js.map +7 -0
- package/dist/creek-page-container/index.d.ts +4 -0
- package/dist/creek-page-container/index.d.ts.map +1 -0
- package/dist/creek-page-container/index.js +68 -0
- package/dist/creek-page-container/index.js.map +7 -0
- package/dist/creek-style/index.d.ts +1 -0
- package/dist/creek-style/index.d.ts.map +1 -0
- package/dist/creek-style/index.js +24 -0
- package/dist/creek-style/index.js.map +7 -0
- package/dist/creek-style/scrollbar.d.ts +2 -0
- package/dist/creek-style/scrollbar.d.ts.map +1 -0
- package/dist/creek-style/scrollbar.js +55 -0
- package/dist/creek-style/scrollbar.js.map +7 -0
- package/dist/creek-table/SearchTable.d.ts +9 -0
- package/dist/creek-table/SearchTable.d.ts.map +1 -1
- package/dist/creek-table/SearchTable.js +109 -72
- package/dist/creek-table/SearchTable.js.map +3 -3
- package/dist/creek-table/components/DensityIcon.d.ts +9 -0
- package/dist/creek-table/components/DensityIcon.d.ts.map +1 -0
- package/dist/creek-table/components/DensityIcon.js +77 -0
- package/dist/creek-table/components/DensityIcon.js.map +7 -0
- package/dist/creek-table/components/EllipsisTooltip.d.ts +9 -0
- package/dist/creek-table/components/EllipsisTooltip.d.ts.map +1 -0
- package/dist/creek-table/components/EllipsisTooltip.js +122 -0
- package/dist/creek-table/components/EllipsisTooltip.js.map +7 -0
- package/dist/creek-table/components/index.d.ts +2 -0
- package/dist/creek-table/components/index.d.ts.map +1 -0
- package/dist/creek-table/components/index.js +26 -0
- package/dist/creek-table/components/index.js.map +7 -0
- package/dist/creek-table/hooks/index.d.ts +5 -0
- package/dist/creek-table/hooks/index.d.ts.map +1 -1
- package/dist/creek-table/hooks/index.js +10 -0
- package/dist/creek-table/hooks/index.js.map +2 -2
- package/dist/creek-table/hooks/useAutoWidthColumns.d.ts +1 -1
- package/dist/creek-table/hooks/useAutoWidthColumns.d.ts.map +1 -1
- package/dist/creek-table/hooks/useAutoWidthColumns.js +76 -17
- package/dist/creek-table/hooks/useAutoWidthColumns.js.map +2 -2
- package/dist/creek-table/hooks/useEllipsisColumns.d.ts +8 -0
- package/dist/creek-table/hooks/useEllipsisColumns.d.ts.map +1 -0
- package/dist/creek-table/hooks/useEllipsisColumns.js +58 -0
- package/dist/creek-table/hooks/useEllipsisColumns.js.map +7 -0
- package/dist/creek-table/hooks/useIndexColumn.d.ts +2 -0
- package/dist/creek-table/hooks/useIndexColumn.d.ts.map +1 -0
- package/dist/creek-table/hooks/useIndexColumn.js +52 -0
- package/dist/creek-table/hooks/useIndexColumn.js.map +7 -0
- package/dist/creek-table/hooks/useResizableColumns.d.ts +20 -0
- package/dist/creek-table/hooks/useResizableColumns.d.ts.map +1 -0
- package/dist/creek-table/hooks/useResizableColumns.js +279 -0
- package/dist/creek-table/hooks/useResizableColumns.js.map +7 -0
- package/dist/creek-table/hooks/useStatusColumns.d.ts +2 -0
- package/dist/creek-table/hooks/useStatusColumns.d.ts.map +1 -0
- package/dist/creek-table/hooks/useStatusColumns.js +215 -0
- package/dist/creek-table/hooks/useStatusColumns.js.map +7 -0
- package/dist/creek-table/hooks/useTableOptions.d.ts +15 -0
- package/dist/creek-table/hooks/useTableOptions.d.ts.map +1 -0
- package/dist/creek-table/hooks/useTableOptions.js +78 -0
- package/dist/creek-table/hooks/useTableOptions.js.map +7 -0
- package/dist/creek-table/hooks/useTableScrollHeight.d.ts +6 -1
- package/dist/creek-table/hooks/useTableScrollHeight.d.ts.map +1 -1
- package/dist/creek-table/hooks/useTableScrollHeight.js +44 -5
- package/dist/creek-table/hooks/useTableScrollHeight.js.map +2 -2
- package/dist/creek-table/type.d.ts +4 -6
- package/dist/creek-table/type.d.ts.map +1 -1
- package/dist/creek-table/type.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -0
- package/dist/index.js.map +2 -2
- package/dist/locales/en-US.d.ts +25 -0
- package/dist/locales/en-US.d.ts.map +1 -0
- package/dist/locales/en-US.js +49 -0
- package/dist/locales/en-US.js.map +7 -0
- package/dist/locales/zh-CN.d.ts +25 -0
- package/dist/locales/zh-CN.d.ts.map +1 -0
- package/dist/locales/zh-CN.js +49 -0
- package/dist/locales/zh-CN.js.map +7 -0
- package/dist/utils/i18n.d.ts +2 -0
- package/dist/utils/i18n.d.ts.map +1 -0
- package/dist/utils/i18n.js +34 -0
- package/dist/utils/i18n.js.map +7 -0
- package/i18n.config.ts +27 -0
- package/package.json +17 -3
- package/src/creek-config-provider/CreekConfigContext.tsx +5 -1
- package/src/creek-config-provider/CreekI18nProvider.tsx +87 -0
- package/src/creek-config-provider/index.tsx +53 -4
- package/src/creek-keep-alive/index.tsx +225 -6
- package/src/creek-layout/ActionRender/FullScreen.tsx +10 -6
- package/src/creek-layout/ActionRender/LayoutSettings.tsx +67 -0
- package/src/creek-layout/ActionRender/UserInfo.tsx +1 -1
- package/src/creek-layout/ActionRender/index.tsx +1 -0
- package/src/creek-layout/index.tsx +89 -22
- package/src/creek-layout/useLayoutSettingsStore.ts +25 -0
- package/src/creek-locale-button/index.tsx +42 -0
- package/src/creek-page-container/index.tsx +32 -0
- package/src/creek-style/index.ts +1 -0
- package/src/creek-style/scrollbar.ts +29 -0
- package/src/creek-table/SearchTable.tsx +125 -72
- package/src/creek-table/components/DensityIcon.tsx +63 -0
- package/src/creek-table/components/EllipsisTooltip.tsx +116 -0
- package/src/creek-table/components/index.tsx +3 -0
- package/src/creek-table/hooks/index.ts +5 -1
- package/src/creek-table/hooks/useAutoWidthColumns.tsx +93 -19
- package/src/creek-table/hooks/useEllipsisColumns.tsx +47 -0
- package/src/creek-table/hooks/useIndexColumn.tsx +27 -0
- package/src/creek-table/hooks/useResizableColumns.tsx +323 -0
- package/src/creek-table/hooks/useStatusColumns.tsx +252 -0
- package/src/creek-table/hooks/useTableOptions.tsx +81 -0
- package/src/creek-table/hooks/useTableScrollHeight.tsx +61 -6
- package/src/creek-table/type.ts +5 -7
- package/src/index.tsx +4 -0
- package/src/locales/en-US.ts +24 -0
- package/src/locales/zh-CN.ts +24 -0
- package/src/utils/i18n.ts +4 -0
- package/dist/creek-table/TableOptionRender.d.ts +0 -9
- package/dist/creek-table/TableOptionRender.d.ts.map +0 -1
- package/dist/creek-table/TableOptionRender.js +0 -74
- package/dist/creek-table/TableOptionRender.js.map +0 -7
- package/dist/creek-table/toolBarRender.d.ts +0 -5
- package/dist/creek-table/toolBarRender.d.ts.map +0 -1
- package/dist/creek-table/toolBarRender.js +0 -58
- package/dist/creek-table/toolBarRender.js.map +0 -7
- package/src/creek-table/TableOptionRender.tsx +0 -57
- package/src/creek-table/toolBarRender.tsx +0 -28
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import { ProColumns } from '@ant-design/pro-components';
|
|
2
|
+
import { useMemoizedFn } from 'ahooks';
|
|
3
|
+
import { createStyles } from 'antd-style';
|
|
4
|
+
import classnames from 'classnames';
|
|
5
|
+
import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
|
6
|
+
import { Resizable, ResizeCallbackData } from 'react-resizable';
|
|
7
|
+
|
|
8
|
+
interface ResizableTitleProps extends React.HTMLAttributes<HTMLTableCellElement> {
|
|
9
|
+
onResize?: (e: React.SyntheticEvent, data: ResizeCallbackData) => void;
|
|
10
|
+
onResizeStart?: (e: React.SyntheticEvent, data: ResizeCallbackData) => void;
|
|
11
|
+
onResizeStop?: (e: React.SyntheticEvent, data: ResizeCallbackData) => void;
|
|
12
|
+
width?: number | string;
|
|
13
|
+
minWidth?: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// 样式定义
|
|
17
|
+
const useStyles = createStyles(({ token }) => ({
|
|
18
|
+
'resizable-handle': {
|
|
19
|
+
position: 'absolute',
|
|
20
|
+
right: -5,
|
|
21
|
+
bottom: 0,
|
|
22
|
+
zIndex: 10,
|
|
23
|
+
width: 10,
|
|
24
|
+
height: '100%',
|
|
25
|
+
cursor: 'col-resize',
|
|
26
|
+
touchAction: 'none',
|
|
27
|
+
userSelect: 'none',
|
|
28
|
+
|
|
29
|
+
// 添加一个小竖线作为视觉提示
|
|
30
|
+
'&::after': {
|
|
31
|
+
content: '""',
|
|
32
|
+
position: 'absolute',
|
|
33
|
+
top: 0,
|
|
34
|
+
bottom: 0,
|
|
35
|
+
left: '50%',
|
|
36
|
+
width: 1,
|
|
37
|
+
opacity: 0,
|
|
38
|
+
transition: 'all 0.3s',
|
|
39
|
+
transform: 'translateX(-50%)',
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
'&:hover::after': {
|
|
43
|
+
opacity: 1,
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
'&:active::after': {
|
|
47
|
+
opacity: 1,
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
'resizable-th': {
|
|
51
|
+
position: 'relative',
|
|
52
|
+
'&:hover .resizable-handle::after': {
|
|
53
|
+
opacity: 0.5,
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
}));
|
|
57
|
+
|
|
58
|
+
const ResizableTitle = (props: ResizableTitleProps) => {
|
|
59
|
+
const { onResize, onResizeStart, onResizeStop, width, className, minWidth, ...restProps } = props;
|
|
60
|
+
const { styles } = useStyles();
|
|
61
|
+
|
|
62
|
+
// 2. 如果 width 不是数字,尝试测量(降级处理)
|
|
63
|
+
const ref = useRef<HTMLTableCellElement>(null);
|
|
64
|
+
const [realWidth, setRealWidth] = useState<number | undefined>(undefined);
|
|
65
|
+
|
|
66
|
+
// 本地状态,用于控制拖动过程中的平滑更新
|
|
67
|
+
// 初始化为 props.width (如果是数字) 或 undefined
|
|
68
|
+
const [localWidth, setLocalWidth] = useState<number | undefined>(typeof width === 'number' ? width : undefined);
|
|
69
|
+
const [isResizing, setIsResizing] = useState(false);
|
|
70
|
+
|
|
71
|
+
// 当外部 width 变化且非拖动状态时,同步到内部
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
if (!isResizing && typeof width === 'number') {
|
|
74
|
+
setLocalWidth(width);
|
|
75
|
+
}
|
|
76
|
+
}, [width, isResizing]);
|
|
77
|
+
|
|
78
|
+
useLayoutEffect(() => {
|
|
79
|
+
if (ref.current) {
|
|
80
|
+
const w = ref.current.getBoundingClientRect().width;
|
|
81
|
+
setRealWidth(w);
|
|
82
|
+
// 如果没有传入具体数字宽度,初始化 localWidth 为测量值
|
|
83
|
+
if (typeof width !== 'number') {
|
|
84
|
+
setLocalWidth(w);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}, []); // 只在挂载时测量一次
|
|
88
|
+
|
|
89
|
+
const handleResizeStart = (e: React.SyntheticEvent, data: ResizeCallbackData) => {
|
|
90
|
+
setIsResizing(true);
|
|
91
|
+
// 增加全局样式防止选中文字
|
|
92
|
+
document.body.style.userSelect = 'none';
|
|
93
|
+
document.body.style.cursor = 'col-resize';
|
|
94
|
+
if (onResizeStart) {
|
|
95
|
+
onResizeStart(e, data);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const handleResizeStop = (e: React.SyntheticEvent, data: ResizeCallbackData) => {
|
|
100
|
+
setIsResizing(false);
|
|
101
|
+
document.body.style.userSelect = '';
|
|
102
|
+
document.body.style.cursor = '';
|
|
103
|
+
// 拖动结束时,确保最后一次更新传递出去
|
|
104
|
+
if (onResizeStop) {
|
|
105
|
+
onResizeStop(e, data);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const handleResize = (e: React.SyntheticEvent, data: ResizeCallbackData) => {
|
|
110
|
+
// 1. 立即更新本地状态,保证 handle 跟手
|
|
111
|
+
setLocalWidth(data.size.width);
|
|
112
|
+
|
|
113
|
+
// 2. 调用外部 onResize (可能会被节流)
|
|
114
|
+
if (onResize) {
|
|
115
|
+
onResize(e, data);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// 1. 如果 width 是数字,直接使用,避免任何 state/effect 开销
|
|
120
|
+
// 修改逻辑:只要有 localWidth 就使用 Resizable
|
|
121
|
+
const currentWidth = localWidth ?? realWidth;
|
|
122
|
+
|
|
123
|
+
if (typeof currentWidth === 'number' && onResize) {
|
|
124
|
+
return (
|
|
125
|
+
<Resizable
|
|
126
|
+
width={currentWidth}
|
|
127
|
+
height={0}
|
|
128
|
+
minConstraints={[minWidth || 0, 0]}
|
|
129
|
+
handle={
|
|
130
|
+
<span
|
|
131
|
+
className={classnames(styles['resizable-handle'], 'resizable-handle')}
|
|
132
|
+
onClick={(e) => {
|
|
133
|
+
e.stopPropagation();
|
|
134
|
+
}}
|
|
135
|
+
onMouseEnter={(e) => {
|
|
136
|
+
// 鼠标进入 handle 时触发一次 onResizeStart,用于预先锁定其他列宽度
|
|
137
|
+
// 这样可以避免在 onResizeStart (mousedown) 时触发重渲染导致 drag 失败
|
|
138
|
+
if (onResizeStart) {
|
|
139
|
+
// 构造一个假的 event 和 data,因为这里只需要触发锁定逻辑
|
|
140
|
+
onResizeStart(e, { node: e.currentTarget, size: { width: currentWidth, height: 0 }, handle: 'e' as any });
|
|
141
|
+
}
|
|
142
|
+
}}
|
|
143
|
+
/>
|
|
144
|
+
}
|
|
145
|
+
onResize={handleResize}
|
|
146
|
+
onResizeStart={handleResizeStart}
|
|
147
|
+
onResizeStop={handleResizeStop}
|
|
148
|
+
draggableOpts={{ enableUserSelectHack: false }} // 我们自己处理了 userSelect
|
|
149
|
+
axis="x"
|
|
150
|
+
>
|
|
151
|
+
<th {...restProps} className={classnames(className, styles['resizable-th'])} style={{ ...restProps.style, width: currentWidth }} />
|
|
152
|
+
</Resizable>
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (!onResize || !realWidth) {
|
|
157
|
+
return <th {...restProps} className={className} ref={ref} />;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Fallback (虽然逻辑上可能不需要了,保留以防万一)
|
|
161
|
+
return (
|
|
162
|
+
<Resizable
|
|
163
|
+
width={realWidth}
|
|
164
|
+
height={0}
|
|
165
|
+
minConstraints={[minWidth || 0, 0]}
|
|
166
|
+
handle={
|
|
167
|
+
<span
|
|
168
|
+
className={classnames(styles['resizable-handle'], 'resizable-handle')}
|
|
169
|
+
onClick={(e) => {
|
|
170
|
+
e.stopPropagation();
|
|
171
|
+
}}
|
|
172
|
+
onMouseEnter={(e) => {
|
|
173
|
+
if (onResizeStart) {
|
|
174
|
+
onResizeStart(e, { node: e.currentTarget, size: { width: realWidth, height: 0 }, handle: 'e' as any });
|
|
175
|
+
}
|
|
176
|
+
}}
|
|
177
|
+
/>
|
|
178
|
+
}
|
|
179
|
+
onResize={handleResize}
|
|
180
|
+
onResizeStart={handleResizeStart}
|
|
181
|
+
onResizeStop={handleResizeStop}
|
|
182
|
+
draggableOpts={{ enableUserSelectHack: false }}
|
|
183
|
+
axis="x"
|
|
184
|
+
>
|
|
185
|
+
<th {...restProps} className={classnames(className, styles['resizable-th'])} style={{ ...restProps.style, width: realWidth }} />
|
|
186
|
+
</Resizable>
|
|
187
|
+
);
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
export const useResizableColumns = <T, ValueType>(
|
|
191
|
+
columns: ProColumns<T, ValueType>[] | undefined,
|
|
192
|
+
resizable: boolean = true,
|
|
193
|
+
resizedWidthsProp?: Record<string, number>,
|
|
194
|
+
setResizedWidthsProp?: React.Dispatch<React.SetStateAction<Record<string, number>>>,
|
|
195
|
+
tableRef?: React.RefObject<HTMLDivElement>,
|
|
196
|
+
): {
|
|
197
|
+
columns: ProColumns<T, ValueType>[] | undefined;
|
|
198
|
+
components: { header: { cell: React.ComponentType<ResizableTitleProps> } } | undefined;
|
|
199
|
+
resizedWidths: Record<string, number>;
|
|
200
|
+
} => {
|
|
201
|
+
// 存储用户手动调整的列宽
|
|
202
|
+
const [internalResizedWidths, setInternalResizedWidths] = useState<Record<string, number>>({});
|
|
203
|
+
// 存储列的初始自动计算宽度,作为最小宽度限制
|
|
204
|
+
const [minWidths, setMinWidths] = useState<Record<string, number>>({});
|
|
205
|
+
|
|
206
|
+
const resizedWidths = resizedWidthsProp || internalResizedWidths;
|
|
207
|
+
const setResizedWidths = setResizedWidthsProp || setInternalResizedWidths;
|
|
208
|
+
|
|
209
|
+
// 性能优化:Resize 过程中不更新 state,只在 Stop 时更新
|
|
210
|
+
// handleResize 仅用于 potential future usage (e.g. events) or removed if not needed
|
|
211
|
+
const handleResize = useMemoizedFn((key: string) => (_: React.SyntheticEvent, { size }: ResizeCallbackData) => {
|
|
212
|
+
// Intentionally empty or minimal logic to avoid re-renders during drag
|
|
213
|
+
// ResizableTitle handles visual updates locally
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
const handleResizeStop = useMemoizedFn((key: string) => (_: React.SyntheticEvent, { size }: ResizeCallbackData) => {
|
|
217
|
+
setResizedWidths((prev) => ({
|
|
218
|
+
...prev,
|
|
219
|
+
[key]: size.width,
|
|
220
|
+
}));
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
const handleResizeStart = useMemoizedFn(() => {
|
|
224
|
+
// 只有当 tableRef 存在时才能获取当前宽度
|
|
225
|
+
if (!tableRef?.current) return;
|
|
226
|
+
|
|
227
|
+
// 查找所有表头单元格
|
|
228
|
+
const thElements = tableRef.current.querySelectorAll('th[data-column-key]');
|
|
229
|
+
if (!thElements || thElements.length === 0) return;
|
|
230
|
+
|
|
231
|
+
const currentWidths: Record<string, number> = {};
|
|
232
|
+
thElements.forEach((th) => {
|
|
233
|
+
const key = th.getAttribute('data-column-key');
|
|
234
|
+
const width = th.getBoundingClientRect().width;
|
|
235
|
+
if (key && width) {
|
|
236
|
+
currentWidths[key] = width;
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
setResizedWidths((prev) => {
|
|
241
|
+
// 只有当之前的状态中缺少某些列的宽度时,才合并当前宽度
|
|
242
|
+
// 这样可以避免覆盖用户已经调整过的值,同时填补未调整列的宽度
|
|
243
|
+
// 实际上,我们希望一旦开始 resize,所有列都有明确的宽度
|
|
244
|
+
const next = { ...currentWidths, ...prev };
|
|
245
|
+
|
|
246
|
+
// 简单的浅比较,如果内容没变就不更新,避免重渲染
|
|
247
|
+
const isSame = Object.keys(next).every(k => next[k] === prev[k]) && Object.keys(prev).length === Object.keys(next).length;
|
|
248
|
+
if (isSame) return prev;
|
|
249
|
+
|
|
250
|
+
return next;
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
// 记录初始宽度作为 minWidth
|
|
254
|
+
setMinWidths((prev) => {
|
|
255
|
+
const next = { ...prev };
|
|
256
|
+
let hasChange = false;
|
|
257
|
+
Object.keys(currentWidths).forEach((key) => {
|
|
258
|
+
// 只有当没有记录该列的最小宽度时才记录
|
|
259
|
+
// 这样可以确保 minWidth 始终是第一次捕获时的宽度(即 auto 宽度)
|
|
260
|
+
if (next[key] === undefined) {
|
|
261
|
+
next[key] = currentWidths[key];
|
|
262
|
+
hasChange = true;
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
return hasChange ? next : prev;
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
const resizableColumns = useMemo(() => {
|
|
270
|
+
if (!resizable || !columns) return columns;
|
|
271
|
+
|
|
272
|
+
return columns.map((col, index) => {
|
|
273
|
+
// 优先使用 key,如果没有则使用 dataIndex,如果都没有则使用 index
|
|
274
|
+
const key = (col.key as string) || (col.dataIndex as string) || `col-${index}`;
|
|
275
|
+
|
|
276
|
+
// 针对 valueType 为 'option' 的操作列,不启用 resizing
|
|
277
|
+
if (col.valueType === 'option') {
|
|
278
|
+
return col;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// 只有当存在 resizedWidths[key] 时才使用它,否则完全依赖外部传入的 col.width(即 useAutoWidthColumns 计算出的宽度)
|
|
282
|
+
const width = resizedWidths[key] ?? col.width;
|
|
283
|
+
|
|
284
|
+
// 从 col 中获取 calculatedWidth 作为 minWidth
|
|
285
|
+
const calculatedMinWidth = (col as any).calculatedWidth || 0;
|
|
286
|
+
// 使用捕获到的 auto 宽度作为最小宽度,如果没有则使用 0
|
|
287
|
+
const autoMinWidth = minWidths[key] || 0;
|
|
288
|
+
|
|
289
|
+
// 最终的 minWidth 取两者的最大值,确保至少是 auto 宽度
|
|
290
|
+
const minWidth = Math.max(calculatedMinWidth, autoMinWidth);
|
|
291
|
+
|
|
292
|
+
return {
|
|
293
|
+
...col,
|
|
294
|
+
width, // 应用调整后的宽度
|
|
295
|
+
onHeaderCell: (column: any) => ({
|
|
296
|
+
width: width, // 传递给 ResizableTitle
|
|
297
|
+
minWidth: minWidth,
|
|
298
|
+
onResize: handleResize(key),
|
|
299
|
+
onResizeStart: handleResizeStart,
|
|
300
|
+
onResizeStop: handleResizeStop(key),
|
|
301
|
+
'data-column-key': key, // 用于在 DOM 中查找
|
|
302
|
+
...(col.onHeaderCell ? col.onHeaderCell(column) : null),
|
|
303
|
+
}) as React.HTMLAttributes<HTMLElement>,
|
|
304
|
+
};
|
|
305
|
+
});
|
|
306
|
+
}, [columns, resizable, resizedWidths, handleResize, handleResizeStart, handleResizeStop]);
|
|
307
|
+
|
|
308
|
+
const components = useMemo(() => {
|
|
309
|
+
if (!resizable) return undefined;
|
|
310
|
+
|
|
311
|
+
return {
|
|
312
|
+
header: {
|
|
313
|
+
cell: ResizableTitle,
|
|
314
|
+
},
|
|
315
|
+
};
|
|
316
|
+
}, [resizable]);
|
|
317
|
+
|
|
318
|
+
return {
|
|
319
|
+
columns: resizableColumns,
|
|
320
|
+
components,
|
|
321
|
+
resizedWidths,
|
|
322
|
+
};
|
|
323
|
+
};
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { ProColumns } from '@ant-design/pro-components';
|
|
2
|
+
import { Badge, Tag } from 'antd';
|
|
3
|
+
import { get } from 'lodash';
|
|
4
|
+
import React, { useMemo } from 'react';
|
|
5
|
+
|
|
6
|
+
// Preset colors for automatic assignment when no semantic match is found
|
|
7
|
+
const PRESET_COLORS = [
|
|
8
|
+
'blue',
|
|
9
|
+
'green',
|
|
10
|
+
'volcano',
|
|
11
|
+
'orange',
|
|
12
|
+
'gold',
|
|
13
|
+
'lime',
|
|
14
|
+
'cyan',
|
|
15
|
+
'geekblue',
|
|
16
|
+
'purple',
|
|
17
|
+
'magenta',
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
// Semantic keywords mapping
|
|
21
|
+
const SEMANTIC_STATUS_MAP: Record<string, string> = {
|
|
22
|
+
// Success / Green
|
|
23
|
+
success: 'success',
|
|
24
|
+
ok: 'success',
|
|
25
|
+
pass: 'success',
|
|
26
|
+
complete: 'success',
|
|
27
|
+
finish: 'success',
|
|
28
|
+
online: 'success',
|
|
29
|
+
active: 'success',
|
|
30
|
+
enable: 'success',
|
|
31
|
+
published: 'success',
|
|
32
|
+
open: 'success',
|
|
33
|
+
正常: 'success',
|
|
34
|
+
成功: 'success',
|
|
35
|
+
完成: 'success',
|
|
36
|
+
通过: 'success',
|
|
37
|
+
启用: 'success',
|
|
38
|
+
在线: 'success',
|
|
39
|
+
发布: 'success',
|
|
40
|
+
开启: 'success',
|
|
41
|
+
已发布: 'success',
|
|
42
|
+
|
|
43
|
+
// Error / Red
|
|
44
|
+
fail: 'error',
|
|
45
|
+
error: 'error',
|
|
46
|
+
reject: 'error',
|
|
47
|
+
stop: 'error',
|
|
48
|
+
close: 'error',
|
|
49
|
+
offline: 'error',
|
|
50
|
+
disable: 'error',
|
|
51
|
+
banned: 'error',
|
|
52
|
+
exception: 'error',
|
|
53
|
+
blocked: 'error',
|
|
54
|
+
失败: 'error',
|
|
55
|
+
错误: 'error',
|
|
56
|
+
拒绝: 'error',
|
|
57
|
+
停止: 'error',
|
|
58
|
+
关闭: 'error',
|
|
59
|
+
离线: 'error',
|
|
60
|
+
禁用: 'error',
|
|
61
|
+
异常: 'error',
|
|
62
|
+
封禁: 'error',
|
|
63
|
+
|
|
64
|
+
// Processing / Blue
|
|
65
|
+
process: 'processing',
|
|
66
|
+
running: 'processing',
|
|
67
|
+
pending: 'processing',
|
|
68
|
+
waiting: 'processing',
|
|
69
|
+
loading: 'processing',
|
|
70
|
+
init: 'processing',
|
|
71
|
+
doing: 'processing',
|
|
72
|
+
进行中: 'processing',
|
|
73
|
+
处理中: 'processing',
|
|
74
|
+
等待: 'processing',
|
|
75
|
+
加载: 'processing',
|
|
76
|
+
初始化: 'processing',
|
|
77
|
+
启动: 'processing',
|
|
78
|
+
运行中: 'processing',
|
|
79
|
+
|
|
80
|
+
// Warning / Warning
|
|
81
|
+
warn: 'warning',
|
|
82
|
+
timeout: 'warning',
|
|
83
|
+
expire: 'warning',
|
|
84
|
+
risk: 'warning',
|
|
85
|
+
abnormal: 'warning',
|
|
86
|
+
警告: 'warning',
|
|
87
|
+
超时: 'warning',
|
|
88
|
+
过期: 'warning',
|
|
89
|
+
风险: 'warning',
|
|
90
|
+
|
|
91
|
+
// Default
|
|
92
|
+
default: 'default',
|
|
93
|
+
normal: 'default',
|
|
94
|
+
unknown: 'default',
|
|
95
|
+
默认: 'default',
|
|
96
|
+
未知: 'default',
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// Helper to determine status color based on text or key
|
|
100
|
+
const inferStatusColor = (key: string | number, text: string): string => {
|
|
101
|
+
const normalizedKey = String(key).toLowerCase();
|
|
102
|
+
const normalizedText = String(text).toLowerCase();
|
|
103
|
+
|
|
104
|
+
// 1. Try to match key first
|
|
105
|
+
for (const keyword in SEMANTIC_STATUS_MAP) {
|
|
106
|
+
if (normalizedKey.includes(keyword)) {
|
|
107
|
+
return SEMANTIC_STATUS_MAP[keyword];
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// 2. Try to match text
|
|
112
|
+
for (const keyword in SEMANTIC_STATUS_MAP) {
|
|
113
|
+
if (normalizedText.includes(keyword)) {
|
|
114
|
+
return SEMANTIC_STATUS_MAP[keyword];
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// 3. Fallback to deterministic hash color
|
|
119
|
+
// Use a simple hash of the key to pick a color from PRESET_COLORS
|
|
120
|
+
let hash = 0;
|
|
121
|
+
for (let i = 0; i < normalizedKey.length; i++) {
|
|
122
|
+
hash = normalizedKey.charCodeAt(i) + ((hash << 5) - hash);
|
|
123
|
+
}
|
|
124
|
+
const index = Math.abs(hash) % PRESET_COLORS.length;
|
|
125
|
+
return PRESET_COLORS[index];
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
export const useStatusColumns = <T, ValueType>(
|
|
129
|
+
columns?: ProColumns<T, ValueType>[],
|
|
130
|
+
) => {
|
|
131
|
+
return useMemo(() => {
|
|
132
|
+
return columns?.map((col) => {
|
|
133
|
+
// If user has custom render, we respect it and do nothing
|
|
134
|
+
if (col.render) {
|
|
135
|
+
return col;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// If no valueEnum, we do nothing
|
|
139
|
+
if (!col.valueEnum) {
|
|
140
|
+
return col;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Check fieldProps for explicit status render mode: 'tag' | 'badge' | 'none' | undefined (auto)
|
|
144
|
+
// We use fieldProps because it's a standard place to put custom column props without TS errors
|
|
145
|
+
const statusRender = (col.fieldProps as any)?.statusRender;
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
if (statusRender === 'none') {
|
|
149
|
+
return col;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// We wrap the render function to support Tag and Badge automatically based on valueEnum properties
|
|
153
|
+
return {
|
|
154
|
+
...col,
|
|
155
|
+
render: (
|
|
156
|
+
dom: React.ReactNode,
|
|
157
|
+
entity: T,
|
|
158
|
+
index: number,
|
|
159
|
+
action: any,
|
|
160
|
+
schema: any,
|
|
161
|
+
) => {
|
|
162
|
+
const valueEnum = schema?.valueEnum;
|
|
163
|
+
if (!valueEnum) {
|
|
164
|
+
return dom;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Get raw value
|
|
168
|
+
const dataIndex = col.dataIndex || col.key;
|
|
169
|
+
// dataIndex can be array or string
|
|
170
|
+
const rawValue =
|
|
171
|
+
typeof dataIndex === 'string' || Array.isArray(dataIndex)
|
|
172
|
+
? get(entity, dataIndex as any)
|
|
173
|
+
: undefined;
|
|
174
|
+
|
|
175
|
+
if (rawValue === undefined || rawValue === null) {
|
|
176
|
+
return dom;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// valueEnum can be Map or Object
|
|
180
|
+
let enumItem = null;
|
|
181
|
+
if (valueEnum instanceof Map) {
|
|
182
|
+
enumItem = valueEnum.get(rawValue);
|
|
183
|
+
// Try string key if number key fails, or vice versa
|
|
184
|
+
if (!enumItem && typeof rawValue === 'number') {
|
|
185
|
+
enumItem = valueEnum.get(String(rawValue));
|
|
186
|
+
} else if (!enumItem && typeof rawValue === 'string') {
|
|
187
|
+
enumItem = valueEnum.get(Number(rawValue));
|
|
188
|
+
}
|
|
189
|
+
} else {
|
|
190
|
+
enumItem = valueEnum[rawValue as string];
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (!enumItem) {
|
|
194
|
+
return dom;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// 1. If color is present, render Tag (unless forced to badge)
|
|
198
|
+
if (enumItem.color && statusRender !== 'badge') {
|
|
199
|
+
return (
|
|
200
|
+
<Tag color={enumItem.color} style={{ margin: 0 }}>
|
|
201
|
+
{enumItem.text || dom}
|
|
202
|
+
</Tag>
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// 2. If status is present, render Badge (unless forced to tag)
|
|
207
|
+
// ProTable usually handles this, but since we overrode render, we must handle it
|
|
208
|
+
if (enumItem.status && statusRender !== 'tag') {
|
|
209
|
+
const status = String(enumItem.status).toLowerCase();
|
|
210
|
+
return <Badge status={status as any} text={enumItem.text || dom} />;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// 3. If neither is present, try to infer color automatically
|
|
214
|
+
// Only infer if it's NOT explicitly set to null/false (in case user wants plain text)
|
|
215
|
+
if (enumItem.status === null || enumItem.status === false) {
|
|
216
|
+
return dom;
|
|
217
|
+
}
|
|
218
|
+
const inferred = inferStatusColor(
|
|
219
|
+
rawValue,
|
|
220
|
+
enumItem.text || String(rawValue),
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
// Force Tag mode
|
|
224
|
+
if (statusRender === 'tag') {
|
|
225
|
+
// Map semantic status to actual colors for Tag
|
|
226
|
+
let tagColor = inferred;
|
|
227
|
+
if (inferred === 'processing') tagColor = 'blue';
|
|
228
|
+
if (inferred === 'error') tagColor = 'error';
|
|
229
|
+
if (inferred === 'success') tagColor = 'success';
|
|
230
|
+
if (inferred === 'warning') tagColor = 'warning';
|
|
231
|
+
if (inferred === 'default') tagColor = 'default';
|
|
232
|
+
|
|
233
|
+
return (
|
|
234
|
+
<Tag color={tagColor} style={{ margin: 0 }}>
|
|
235
|
+
{enumItem.text || dom}
|
|
236
|
+
</Tag>
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Default / Force Badge mode
|
|
241
|
+
// If it is a semantic status, use status prop
|
|
242
|
+
if (['success', 'processing', 'error', 'default', 'warning'].includes(inferred)) {
|
|
243
|
+
return <Badge status={inferred as any} text={enumItem.text || dom} />;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Otherwise use color prop (for preset colors like 'volcano', 'gold', etc.)
|
|
247
|
+
return <Badge color={inferred} text={enumItem.text || dom} />;
|
|
248
|
+
},
|
|
249
|
+
} as ProColumns<T, ValueType>;
|
|
250
|
+
});
|
|
251
|
+
}, [columns]);
|
|
252
|
+
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { ParamsType, ProTableProps } from '@ant-design/pro-components';
|
|
2
|
+
import { useSafeState } from 'ahooks';
|
|
3
|
+
import { useEffect, useRef } from 'react';
|
|
4
|
+
import { DensityIcon } from '../components';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Hook to manage ProTable options logic including density and settings
|
|
8
|
+
*
|
|
9
|
+
* @param options User provided options configuration
|
|
10
|
+
* @param size User provided size (density)
|
|
11
|
+
* @returns { finalOptions, tableSize, setTableSize }
|
|
12
|
+
*/
|
|
13
|
+
export const useTableOptions = <T extends ParamsType, U extends ParamsType, ValueType = 'text'>(
|
|
14
|
+
options: ProTableProps<T, U, ValueType>['options'],
|
|
15
|
+
size?: ProTableProps<T, U, ValueType>['size'],
|
|
16
|
+
optionsRender?: ProTableProps<T, U, ValueType>['optionsRender']
|
|
17
|
+
): {
|
|
18
|
+
finalOptions: ProTableProps<T, U, ValueType>['options'];
|
|
19
|
+
tableSize: ProTableProps<T, U, ValueType>['size'];
|
|
20
|
+
setTableSize: React.Dispatch<React.SetStateAction<ProTableProps<T, U, ValueType>['size']>>;
|
|
21
|
+
showDensity: boolean;
|
|
22
|
+
finalOptionsRender: ProTableProps<T, U, ValueType>['optionsRender'];
|
|
23
|
+
} => {
|
|
24
|
+
// Manage table density state, defaulting to 'small' (compact)
|
|
25
|
+
// Supports controlled mode and switching
|
|
26
|
+
const [tableSize, setTableSize] = useSafeState<ProTableProps<T, U, ValueType>['size']>(size || 'small');
|
|
27
|
+
|
|
28
|
+
// Sync internal state if size prop changes
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
if (size) {
|
|
31
|
+
setTableSize(size);
|
|
32
|
+
}
|
|
33
|
+
}, [size]);
|
|
34
|
+
|
|
35
|
+
// Use ref to store latest tableSize to avoid stale closures in optionsRender
|
|
36
|
+
const tableSizeRef = useRef(tableSize);
|
|
37
|
+
tableSizeRef.current = tableSize;
|
|
38
|
+
|
|
39
|
+
// Merge default options with user provided options
|
|
40
|
+
// Default: show density and setting, hide reload and fullScreen
|
|
41
|
+
const showDensity = options !== false && (options?.density !== false);
|
|
42
|
+
|
|
43
|
+
const finalOptions: ProTableProps<T, U, ValueType>['options'] =
|
|
44
|
+
options === false
|
|
45
|
+
? false
|
|
46
|
+
: {
|
|
47
|
+
setting: true,
|
|
48
|
+
reload: false,
|
|
49
|
+
fullScreen: false,
|
|
50
|
+
...options,
|
|
51
|
+
density: false,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const finalOptionsRender: ProTableProps<T, U, ValueType>['optionsRender'] = (opts, defaultDom) => {
|
|
55
|
+
const doms = [...defaultDom];
|
|
56
|
+
const currentTableSize = tableSizeRef.current;
|
|
57
|
+
|
|
58
|
+
if (showDensity) {
|
|
59
|
+
doms.unshift(
|
|
60
|
+
<DensityIcon
|
|
61
|
+
key="density"
|
|
62
|
+
tableSize={currentTableSize}
|
|
63
|
+
setTableSize={setTableSize}
|
|
64
|
+
/>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (optionsRender) {
|
|
69
|
+
return optionsRender(opts, doms);
|
|
70
|
+
}
|
|
71
|
+
return doms;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
finalOptions,
|
|
76
|
+
tableSize,
|
|
77
|
+
setTableSize,
|
|
78
|
+
showDensity,
|
|
79
|
+
finalOptionsRender,
|
|
80
|
+
};
|
|
81
|
+
};
|