diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e1d95cd6a..b73076fb6d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,46 @@ toc: false spline: explain --- + ## 🌈 1.2.6 `2023-09-28` +### 🚀 Features +- `Table`: 优化渲染次数 @chaishi ([#2514](https://github.com/Tencent/tdesign-react/pull/2514)) +- `card`: title使用`div`取代`span` 在自定义场景下更符合规范 @uyarn ([#2517](https://github.com/Tencent/tdesign-react/pull/2517)) +- `Tree`: Tree支持通过key匹配单一value指定滚动到特定位置,具体使用方式请参考示例代码 @uyarn ([#2519](https://github.com/Tencent/tdesign-react/pull/2519)) +### 🐞 Bug Fixes +- `Form`: 修复 formList 嵌套数据获取异常 @honkinglin ([#2529](https://github.com/Tencent/tdesign-react/pull/2529)) +- `Table`: 修复数据切换时 `rowspanAndColspan` 渲染问题,[issue#2513](https://github.com/Tencent/tdesign-react/issues/2513) @chaishi ([#2514](https://github.com/Tencent/tdesign-react/pull/2514)) +- `Cascader`: hover 没有子节点数据的父节点时未更新子节点 @betavs ([#2528](https://github.com/Tencent/tdesign-react/pull/2528)) +- `Datepicker`: 修复切换月份失效问题 @honkinglin ([#2531](https://github.com/Tencent/tdesign-react/pull/2531)) +- `Dropdown`: 修复`Dropdown` disabled API失效的问题 @uyarn ([#2532](https://github.com/Tencent/tdesign-react/pull/2532)) + + ## 🌈 1.2.5 `2023-09-14` +### 🚀 Features +- `steps`: 全局配置添加步骤条的已完成图标自定义 @Zzongke ([#2491](https://github.com/Tencent/tdesign-react/pull/2491)) +- `Table`: 可筛选表格,`onFilterChange` 事件新增参数 `trigger: 'filter-change' | 'confirm' | 'reset' | 'clear'`,表示触发筛选条件变化的来源 @chaishi ([#2492](https://github.com/Tencent/tdesign-react/pull/2492)) +- `Form`: trigger新增`submit`选项 @honkinglin ([#2507](https://github.com/Tencent/tdesign-react/pull/2507)) +- `ImageViewer`: `onIndexChange` 事件新增 `trigger` 枚举值 `current` @chaishi ([#2494](https://github.com/Tencent/tdesign-react/pull/2494)) +- `Image`: + - 新增 `fallback`,表示图片的兜底图,原始图片加载失败时会显示兜底图 @chaishi ([#2494](https://github.com/Tencent/tdesign-react/pull/2494)) + - 新增支持 `src` 类型为 `File`,支持通过 `File` 预览图片 @chaishi ([#2494](https://github.com/Tencent/tdesign-react/pull/2494)) +- `Upload`: 文案列表支持显示缩略图 @chaishi ([#2494](https://github.com/Tencent/tdesign-react/pull/2494)) +- `Tree`: + - 支持虚拟滚动场景下通过`key`滚动到特定节点 @uyarn ([#2509](https://github.com/Tencent/tdesign-react/pull/2509)) + - 虚拟滚动下 低于`threshold` 仍可运行scrollTo操作 @uyarn ([#2509](https://github.com/Tencent/tdesign-react/pull/2509)) +### 🐞 Bug Fixes +- `GlobalConfig`: 修复切换多语言失效的问题 @uyarn ([#2501](https://github.com/Tencent/tdesign-react/pull/2501)) +- `Table`: + - 可筛选表格,修复 `resetValue` 在清空筛选时,未能重置到指定 `resetValue` 值的问题 @chaishi ([#2492](https://github.com/Tencent/tdesign-react/pull/2492)) + - 树形结构表格,修复 expandedTreeNodes.sync 和 @expanded-tree-nodes-change 使用 expandTreeNodeOnClick 时无效问题 [tdesign-vue#2756](https://github.com/Tencent/tdesign-vue/issues/2756) @chaishi ([#2492](https://github.com/Tencent/tdesign-react/pull/2492)) + - 单元格在编辑模式下,保存的时候对于链式的colKey处理错误,未能覆盖原来的值 @Empire-suy ([#2493](https://github.com/Tencent/tdesign-react/pull/2493)) + - 可编辑表格,修复多个可编辑表格同时存在时,校验互相影响问题 @chaishi ([#2498](https://github.com/Tencent/tdesign-react/pull/2498)) + - 单元格在编辑模式下,保存的时候对于链式的colKey处理错误,未能覆盖原来的值 @Empire-suy ([#2493](https://github.com/Tencent/tdesign-react/pull/2493)) + - 修复使用 list 传 props 且 destroyOnHide 为 false 下, 会丢失 panel 内容的问题 @lzy2014love ([#2500](https://github.com/Tencent/tdesign-react/pull/2500)) +- `TagInput`: 修复折叠展示选项尺寸大小问题 @uyarn ([#2503](https://github.com/Tencent/tdesign-react/pull/2503)) +- `Tabs`: 修复使用 list 传 props 且 destroyOnHide 为 false 下, 会丢失 panel 内容的问题 @lzy2014love ([#2500](https://github.com/Tencent/tdesign-react/pull/2500)) +- `menu`: 修复菜单expandType默认模式下menuitem传递onClick不触发的问题 @Zzongke ([#2502](https://github.com/Tencent/tdesign-react/pull/2502)) +- `ImageViewer`: 修复无法通过 `visible` 直接打开预览弹框问题 @chaishi ([#2494](https://github.com/Tencent/tdesign-react/pull/2494)) +- `Tree`: 修复1.2.0版本后部分`TreeNodeModel`的操作失效的异常 @uyarn + ## 🌈 1.2.4 `2023-08-31` ### 🚀 Features - `Table`: 树形结构,没有设置 `expandedTreeNodes` 情况下,data 数据发生变化时,自动重置收起所有展开节点(如果希望保持展开节点,请使用属性 `expandedTreeNodes` 控制,[tdesign-vue#2735](https://github.com/Tencent/tdesign-vue/issues/2735) @chaishi ([#2470](https://github.com/Tencent/tdesign-react/pull/2470)) diff --git a/README-zh_CN.md b/README-zh_CN.md index be6e1af28e..b9a0ebd201 100644 --- a/README-zh_CN.md +++ b/README-zh_CN.md @@ -80,6 +80,12 @@ npm package 中提供了多种构建产物,可以阅读 [这里](https://githu TDesign 欢迎任何愿意参与贡献的参与者。如果需要本地运行代码或参与贡献,请先阅读[参与贡献](https://github.com/Tencent/tdesign-react/blob/develop/CONTRIBUTING.md)。 +# 反馈 + +有任何问题,建议通过 [Github issues](https://github.com/Tencent/tdesign-react/issues) 反馈或扫码加入用户微信群。 + + + # 开源协议 TDesign 遵循 [MIT 协议](https://github.com/Tencent/tdesign-react/LICENSE)。 diff --git a/README.md b/README.md index 1f9deee5ab..fb65a17ded 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,12 @@ TDesign also provides component libraries for other platforms and frameworks. Contributing is welcome. Read [guidelines for contributing](https://github.com/Tencent/tdesign-react/blob/develop/CONTRIBUTING.md) before submitting your [Pull Request](https://github.com/Tencent/tdesign-react/pulls). +# Feedback + +Create your [Github issues](https://github.com/Tencent/tdesign-react/issues) or scan the QR code below to join our user groups + + + # License The MIT License. Please see [the license file](./LICENSE) for more information. \ No newline at end of file diff --git a/package.json b/package.json index 15f7b3fc68..266b612331 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "tdesign-react", "purename": "tdesign", - "version": "1.2.4", + "version": "1.2.6", "description": "TDesign Component for React", "title": "tdesign-react", "main": "lib/index.js", @@ -189,7 +189,7 @@ "ts-node": "^10.4.0", "typescript": "~4.5.4", "vite": "^2.9.15", - "vite-plugin-istanbul": "^2.3.0", + "vite-plugin-istanbul": "^5.0.0", "vite-plugin-pwa": "^0.12.8", "vite-plugin-tdoc": "^2.0.1", "vitest": "^0.24.1", diff --git a/src/_common b/src/_common index 9cfa842e87..4440b1f5da 160000 --- a/src/_common +++ b/src/_common @@ -1 +1 @@ -Subproject commit 9cfa842e877482e3f86f92fca49da0abb3ac8144 +Subproject commit 4440b1f5da531bacc412c9748cab48609054f827 diff --git a/src/card/Card.tsx b/src/card/Card.tsx index b3f422ca21..0a034d385a 100644 --- a/src/card/Card.tsx +++ b/src/card/Card.tsx @@ -85,9 +85,9 @@ const Card = forwardRef((props, ref) => { [`${classPrefix}-card__description`]: description, }); - const renderTitle = title ? {title} : null; + const renderTitle = title ?
{title}
: null; - const renderSubtitle = subtitle ? {subtitle} : null; + const renderSubtitle = subtitle ?
{subtitle}
: null; const renderDescription = description ?

{description}

: null; diff --git a/src/cascader/core/effect.ts b/src/cascader/core/effect.ts index 7006c653fb..75ea42344b 100644 --- a/src/cascader/core/effect.ts +++ b/src/cascader/core/effect.ts @@ -24,7 +24,7 @@ export function expendClickEffect( if (isDisabled) return; // 点击展开节点,设置展开状态 - if (propsTrigger === trigger && !node.isLeaf()) { + if (propsTrigger === trigger) { const expanded = node.setExpanded(true); treeStore.refreshNodes(); treeStore.replaceExpanded(expanded); diff --git a/src/config-provider/ConfigContext.tsx b/src/config-provider/ConfigContext.tsx index b559f4341c..92d8a1fb7e 100644 --- a/src/config-provider/ConfigContext.tsx +++ b/src/config-provider/ConfigContext.tsx @@ -17,17 +17,16 @@ export const defaultAnimation = { exclude: [], }; -type DefaultGlobalConfig = Partial +type DefaultGlobalConfig = Partial; export const defaultGlobalConfig: DefaultGlobalConfig = { animation: defaultAnimation, classPrefix: defaultClassPrefix, - ...merge(defaultLocale, defaultConfig), + ...merge({}, defaultLocale, defaultConfig), }; export type Locale = typeof defaultLocale; - export const defaultContext = { globalConfig: defaultGlobalConfig, }; diff --git a/src/config-provider/ConfigProvider.tsx b/src/config-provider/ConfigProvider.tsx index 1b8746360f..27891d94c4 100644 --- a/src/config-provider/ConfigProvider.tsx +++ b/src/config-provider/ConfigProvider.tsx @@ -16,12 +16,8 @@ export const merge = (src: GlobalConfigProvider, config: GlobalConfigProvider) = }); export default function ConfigProvider({ children, globalConfig }: ConfigProviderProps) { - const mergedGlobalConfig = merge(defaultGlobalConfig, globalConfig); - return ( - - {children} - - ); + const mergedGlobalConfig = merge({ ...defaultGlobalConfig }, globalConfig); + return {children}; } ConfigProvider.displayName = 'ConfigProvider'; diff --git a/src/config-provider/config-provider.en-US.md b/src/config-provider/config-provider.en-US.md index 9fa9270fc7..4504d00c2d 100644 --- a/src/config-provider/config-provider.en-US.md +++ b/src/config-provider/config-provider.en-US.md @@ -272,6 +272,7 @@ closeIcon | Function | - | Typescript:`TNode`。[see more ts definition](https name | type | default | description | required -- | -- | -- | -- | -- +checkIcon | TElement | - | Typescript:`TNode`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/src/common.ts) | N errorIcon | TElement | - | Typescript:`TNode`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/src/common.ts) | N ### AlertConfig diff --git a/src/config-provider/config-provider.md b/src/config-provider/config-provider.md index 7f4b1cccc4..bd97571d74 100644 --- a/src/config-provider/config-provider.md +++ b/src/config-provider/config-provider.md @@ -302,6 +302,7 @@ closeIcon | Function | - | 关闭图标,【注意】使用渲染函数输出 名称 | 类型 | 默认值 | 说明 | 必传 -- | -- | -- | -- | -- +checkIcon | TElement | - | 已完成步骤图标,【注意】使用渲染函数输出图标组件。TS 类型:`TNode`。[通用类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/common.ts) | N errorIcon | TElement | - | 错误步骤图标,【注意】使用渲染函数输出图标组件。TS 类型:`TNode`。[通用类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/common.ts) | N ### AlertConfig diff --git a/src/config-provider/type.ts b/src/config-provider/type.ts index 3691413744..f25337dbc6 100644 --- a/src/config-provider/type.ts +++ b/src/config-provider/type.ts @@ -789,6 +789,10 @@ export interface TagConfig { } export interface StepsConfig { + /** + * 已完成步骤图标,【注意】使用渲染函数输出图标组件 + */ + checkIcon?: TElement; /** * 错误步骤图标,【注意】使用渲染函数输出图标组件 */ diff --git a/src/date-picker/panel/PanelContent.tsx b/src/date-picker/panel/PanelContent.tsx index 56a2b962f4..79e30cfc54 100644 --- a/src/date-picker/panel/PanelContent.tsx +++ b/src/date-picker/panel/PanelContent.tsx @@ -62,20 +62,29 @@ export default function PanelContent(props: PanelContentProps) { const defaultTime = '00:00:00'; - const onMonthChangeInner = useCallback((val: number) => { - onMonthChange?.(val, { partial }); + const onMonthChangeInner = useCallback( + (val: number) => { + onMonthChange?.(val, { partial }); + }, // eslint-disable-next-line - }, []); + [partial], + ); - const onYearChangeInner = useCallback((val: number) => { - onYearChange?.(val, { partial }); + const onYearChangeInner = useCallback( + (val: number) => { + onYearChange?.(val, { partial }); + }, // eslint-disable-next-line - }, []); + [partial], + ); - const onJumperClickInner = useCallback(({ trigger }) => { - onJumperClick?.({ trigger, partial }); + const onJumperClickInner = useCallback( + ({ trigger }) => { + onJumperClick?.({ trigger, partial }); + }, // eslint-disable-next-line - }, []); + [partial], + ); return (
diff --git a/src/dropdown/Dropdown.tsx b/src/dropdown/Dropdown.tsx index b473d0a20c..3924f22a2c 100644 --- a/src/dropdown/Dropdown.tsx +++ b/src/dropdown/Dropdown.tsx @@ -48,6 +48,7 @@ const Dropdown: React.FC & { }; const handleVisibleChange = (visible: boolean, context: PopupVisibleChangeContext) => { + if (disabled) return; togglePopupVisible(visible); popupProps?.onVisibleChange?.(visible, context); }; diff --git a/src/form/form.en-US.md b/src/form/form.en-US.md index 355e63aad7..df0aacadb4 100644 --- a/src/form/form.en-US.md +++ b/src/form/form.en-US.md @@ -97,7 +97,7 @@ number | Boolean | - | \- | N pattern | Object | - | Typescript:`RegExp` | N required | Boolean | - | \- | N telnumber | Boolean | - | \- | N -trigger | String | change | options: change/blur | N +trigger | String | change | options: change/blur/submit | N type | String | error | options: error/warning | N url | Boolean / Object | - | Typescript:`boolean \| IsURLOptions` `import { IsURLOptions } from 'validator/es/lib/isURL'`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/src/form/type.ts) | N validator | Function | - | Typescript:`CustomValidator` `type CustomValidator = (val: ValueType) => CustomValidateResolveType \| Promise` `type CustomValidateResolveType = boolean \| CustomValidateObj` `interface CustomValidateObj { result: boolean; message: string; type?: 'error' \| 'warning' \| 'success' }` `type ValueType = any`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/src/form/type.ts) | N diff --git a/src/form/form.md b/src/form/form.md index 7bffc42941..c29eefa879 100644 --- a/src/form/form.md +++ b/src/form/form.md @@ -187,7 +187,7 @@ number | Boolean | - | 内置校验方法,校验值是否为数字(1.2 、 1 pattern | Object | - | 内置校验方法,校验值是否符合正则表达式匹配结果,示例:`{ pattern: /@qq.com/, message: '请输入 QQ 邮箱' }`。TS 类型:`RegExp` | N required | Boolean | - | 内置校验方法,校验值是否已经填写。该值为 true,默认显示必填标记,可通过设置 `requiredMark: false` 隐藏必填标记 | N telnumber | Boolean | - | 内置校验方法,校验值是否为手机号码,校验正则为 `/^1[3-9]\d{9}$/`,示例:`{ telnumber: true, message: '请输入正确的手机号码' }` | N -trigger | String | change | 校验触发方式。可选项:change/blur | N +trigger | String | change | 校验触发方式。可选项:change/blur/submit | N type | String | error | 校验未通过时呈现的错误信息类型,有 告警信息提示 和 错误信息提示 等两种。可选项:error/warning | N url | Boolean / Object | - | 内置校验方法,校验值是否为网络链接地址,[参数文档](https://github.com/validatorjs/validator.js),示例:`{ url: { protocols: ['http','https','ftp'] }, message: '请输入正确的 Url 地址' }`。TS 类型:`boolean \| IsURLOptions` `import { IsURLOptions } from 'validator/es/lib/isURL'`。[详细类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/form/type.ts) | N validator | Function | - | 自定义校验规则,示例:`{ validator: (val) => val.length > 0, message: '请输入内容'}`。TS 类型:`CustomValidator` `type CustomValidator = (val: ValueType) => CustomValidateResolveType \| Promise` `type CustomValidateResolveType = boolean \| CustomValidateObj` `interface CustomValidateObj { result: boolean; message: string; type?: 'error' \| 'warning' \| 'success' }` `type ValueType = any`。[详细类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/form/type.ts) | N diff --git a/src/form/hooks/useInstance.tsx b/src/form/hooks/useInstance.tsx index 21aea8697d..74123687c9 100644 --- a/src/form/hooks/useInstance.tsx +++ b/src/form/hooks/useInstance.tsx @@ -115,7 +115,8 @@ export default function useInstance(props: TdFormProps, formRef, formMapRef: Rea const fieldsValue = {}; if (nameList === true) { - for (const [name, formItemRef] of formMapRef.current.entries()) { + // 嵌套数组子节点先添加导致外层数据覆盖因而需要倒序遍历 + for (const [name, formItemRef] of [...formMapRef.current.entries()].reverse()) { const fieldValue = calcFieldValue(name, formItemRef?.current.getValue?.()); merge(fieldsValue, fieldValue); } diff --git a/src/form/type.ts b/src/form/type.ts index 440b0f94ca..178119685b 100644 --- a/src/form/type.ts +++ b/src/form/type.ts @@ -300,7 +300,7 @@ export interface FormRule { * 校验触发方式 * @default change */ - trigger?: 'change' | 'blur'; + trigger?: 'change' | 'blur' | 'submit'; /** * 校验未通过时呈现的错误信息类型,有 告警信息提示 和 错误信息提示 等两种 * @default error diff --git a/src/hooks/useImagePreviewUrl.ts b/src/hooks/useImagePreviewUrl.ts new file mode 100644 index 0000000000..24690cf6a6 --- /dev/null +++ b/src/hooks/useImagePreviewUrl.ts @@ -0,0 +1,27 @@ +import { useEffect, useState } from 'react'; +import log from '../_common/js/log'; +import { getFileUrlByFileRaw } from '../_common/js/upload/utils'; + +export function useImagePreviewUrl(imgUrl: string | File) { + const [previewUrl, setPreviewUrl] = useState(''); + + useEffect(() => { + if (!imgUrl) return; + if (typeof imgUrl === 'string') { + setPreviewUrl(imgUrl); + return; + } + getFileUrlByFileRaw(imgUrl).then( + (url) => { + setPreviewUrl(url); + }, + () => { + log.error('Image', 'Image.src is not a valid file'); + }, + ); + }, [imgUrl]); + + return { previewUrl }; +} + +export default useImagePreviewUrl; diff --git a/src/hooks/useVirtualScroll.ts b/src/hooks/useVirtualScroll.ts index 2307790bac..617e8e669f 100644 --- a/src/hooks/useVirtualScroll.ts +++ b/src/hooks/useVirtualScroll.ts @@ -123,7 +123,8 @@ const useVirtualScroll = (container: MutableRefObject, params: UseV }; const updateScrollTop = ({ index, top = 0, behavior }: ScrollToElementParams) => { - const scrollTop = trScrollTopHeightList.current[index] - containerHeight.current - top; + const containerCurrentHeight = containerHeight.current || container.current.getBoundingClientRect().height; + const scrollTop = trScrollTopHeightList.current[index] - containerCurrentHeight - top; container.current?.scrollTo({ top: scrollTop, behavior: behavior || 'auto', @@ -151,7 +152,14 @@ const useVirtualScroll = (container: MutableRefObject, params: UseV // 固定高度场景,可直接通过数据长度计算出最大滚动高度 useEffect( () => { - if (!isVirtualScroll) return; + if (!isVirtualScroll) { + trScrollTopHeightList.current = getTrScrollTopHeightList( + trHeightList, + container.current?.getBoundingClientRect().height, + ); + return; + } + // 给数据添加下标 addIndexToData(data); setScrollHeight(data.length * tScroll.rowHeight); diff --git a/src/image-viewer/ImageViewer.tsx b/src/image-viewer/ImageViewer.tsx index 354283e8e2..1fc1330be4 100644 --- a/src/image-viewer/ImageViewer.tsx +++ b/src/image-viewer/ImageViewer.tsx @@ -45,7 +45,7 @@ const ImageViewer: React.FC = (originalProps) => { return ( <> {uiImage} - {visibled && + {(visibled || visible) && createPortal( { const { classPrefix } = useConfig(); @@ -42,8 +44,8 @@ interface ImageModelItemProps { rotateZ: number; scale: number; mirror: number; - src: string; - preSrc?: string; + src: string | File; + preSrc?: string | File; errorText: string; } @@ -62,9 +64,12 @@ export const ImageModelItem: React.FC = ({ rotateZ, scale, const preImgStyle = { transform: `rotateZ(${rotateZ}deg) scale(${scale})`, display: !loaded ? 'block' : 'none' }; const boxStyle = { transform: `translate(${position[0]}px, ${position[1]}px) scale(${mirror}, 1)` }; + const { previewUrl: preSrcImagePreviewUrl } = useImagePreviewUrl(preSrc); + const { previewUrl: mainImagePreviewUrl } = useImagePreviewUrl(src); + useEffect(() => { setError(false); - }, [src]); + }, [preSrcImagePreviewUrl, mainImagePreviewUrl]); return (
@@ -77,7 +82,7 @@ export const ImageModelItem: React.FC = ({ rotateZ, scale, event.stopPropagation(); onMouseDown(event); }} - src={preSrc} + src={preSrcImagePreviewUrl} style={preImgStyle} data-testid="img-drag" alt="image" @@ -91,7 +96,7 @@ export const ImageModelItem: React.FC = ({ rotateZ, scale, event.stopPropagation(); onMouseDown(event); }} - src={src} + src={mainImagePreviewUrl} onLoad={() => setLoaded(true)} onError={() => setError(true)} style={imgStyle} @@ -220,11 +225,16 @@ export const ImageViewerUtils: React.FC = ({ }; type ImageViewerHeaderProps = { - onImgClick: (index: number) => void; + onImgClick: (index: number, ctx: { trigger: 'current' }) => void; images: ImageInfo[]; currentIndex: number; }; +function OneImagePreview({ image, classPrefix }: { image: ImageInfo; classPrefix: string }) { + const { previewUrl } = useImagePreviewUrl(image.thumbnail || image.mainImage); + return ; +} + const ImageViewerHeader = (props: ImageViewerHeaderProps) => { const { classPrefix } = useConfig(); const { images, currentIndex, onImgClick } = props; @@ -253,13 +263,9 @@ const ImageViewerHeader = (props: ImageViewerHeaderProps) => { className={classNames(`${classPrefix}-image-viewer__header-box`, { [`${classPrefix}-is-active`]: index === currentIndex, })} - onClick={() => onImgClick(index)} + onClick={() => onImgClick(index, { trigger: 'current' })} > - +
))}
@@ -369,6 +375,7 @@ export const ImageModal: React.FC = (props) => { if (!isArray(images) || images.length < 1) return null; const currentImage: ImageInfo = images[index]; + const tipText = { mirror: t(locale.mirrorTipText), rotate: t(locale.rotateTipText), diff --git a/src/image-viewer/_example/base.jsx b/src/image-viewer/_example/base.jsx index 550d05d8d0..5dfbe3d4ff 100644 --- a/src/image-viewer/_example/base.jsx +++ b/src/image-viewer/_example/base.jsx @@ -43,6 +43,9 @@ export default function BasicImageViewer() { return ( + + {/* TODO: fix visible=true can not show image previewer */} + {/* */} ); } diff --git a/src/image-viewer/defaultProps.ts b/src/image-viewer/defaultProps.ts index 71f62ed6dd..e0b99ebe3b 100644 --- a/src/image-viewer/defaultProps.ts +++ b/src/image-viewer/defaultProps.ts @@ -8,8 +8,9 @@ export const imageViewerDefaultProps: TdImageViewerProps = { closeBtn: true, draggable: undefined, images: [], + defaultIndex: 0, mode: 'modal', + navigationArrow: true, showOverlay: undefined, defaultVisible: false, - defaultIndex: 0, }; diff --git a/src/image-viewer/hooks/useList.ts b/src/image-viewer/hooks/useList.ts index 4fe4a76df7..3e473ce20c 100644 --- a/src/image-viewer/hooks/useList.ts +++ b/src/image-viewer/hooks/useList.ts @@ -3,17 +3,16 @@ import { ImageInfo } from '../type'; const checkImages = (images) => images.map((image) => { - const result: ImageInfo = { mainImage: '' }; - if (typeof image === 'string' || !image) result.mainImage = image; - else { - result.mainImage = image.mainImage; - result.thumbnail = image.thumbnail; - result.download = image.download; + let result: ImageInfo = { mainImage: '' }; + if (typeof image === 'object' && !(image instanceof File)) { + result = image; + } else { + result.mainImage = image; + result.thumbnail = image; } return result; }); -// 业务组件 const useList = (images) => { const [list, setList] = useState(() => checkImages(images)); diff --git a/src/image-viewer/image-viewer.en-US.md b/src/image-viewer/image-viewer.en-US.md index dadfa859b5..57a1e380ce 100644 --- a/src/image-viewer/image-viewer.en-US.md +++ b/src/image-viewer/image-viewer.en-US.md @@ -11,17 +11,17 @@ closeBtn | TNode | true | Typescript:`boolean \| TNode`。[see more ts definit closeOnOverlay | Boolean | - | \- | N draggable | Boolean | undefined | \- | N imageScale | Object | - | Typescript:`ImageScale` `interface ImageScale { max: number; min: number; step: number }`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/src/image-viewer/type.ts) | N -images | Array | [] | Typescript:`Array` `interface ImageInfo { mainImage: string; thumbnail?: string; download?: boolean }`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/src/image-viewer/type.ts) | N -index | Number | - | \- | N -defaultIndex | Number | - | uncontrolled property | N -mode | String | modal | options:modal/modeless | N +images | Array | [] | Typescript:`Array` `interface ImageInfo { mainImage: string \| File; thumbnail?: string \| File; download?: boolean }`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/src/image-viewer/type.ts) | N +index | Number | 0 | \- | N +defaultIndex | Number | 0 | uncontrolled property | N +mode | String | modal | options: modal/modeless | N navigationArrow | TNode | true | Typescript:`boolean \| TNode`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/src/common.ts) | N showOverlay | Boolean | undefined | \- | N title | TNode | - | preview title。Typescript:`string \| TNode`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/src/common.ts) | N -trigger | TNode | - | trigger element。Typescript:`string \| TNode<{ open: () => void }>`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/src/common.ts) | N +trigger | TNode | - | trigger element。Typescript:`TNode \| TNode<{ open: () => void }>`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/src/common.ts) | N viewerScale | Object | - | Typescript:`ImageViewerScale` `interface ImageViewerScale { minWidth: number; minHeight: number }`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/src/image-viewer/type.ts) | N visible | Boolean | false | \- | N defaultVisible | Boolean | false | uncontrolled property | N zIndex | Number | - | \- | N onClose | Function | | Typescript:`(context: { trigger: 'close-btn' \| 'overlay' \| 'esc'; e: MouseEvent \| KeyboardEvent }) => void`
| N -onIndexChange | Function | | Typescript:`(index: number, context: { trigger: 'prev' \| 'next' }) => void`
| N +onIndexChange | Function | | Typescript:`(index: number, context: { trigger: 'prev' \| 'next' \| 'current' }) => void`
| N diff --git a/src/image-viewer/image-viewer.md b/src/image-viewer/image-viewer.md index 9d2a86ec51..80211f04f5 100644 --- a/src/image-viewer/image-viewer.md +++ b/src/image-viewer/image-viewer.md @@ -11,17 +11,17 @@ closeBtn | TNode | true | 是否展示关闭按钮,值为 `true` 显示默认 closeOnOverlay | Boolean | - | 是否在点击遮罩层时,触发预览关闭 | N draggable | Boolean | undefined | 是否允许拖拽调整位置。`mode=modal` 时,默认不允许拖拽;`mode=modeless` 时,默认允许拖拽 | N imageScale | Object | - | 图片缩放相关配置。`imageScale.max` 缩放的最大比例;`imageScale.min` 缩放的最小比例;`imageScale.step` 缩放的步长速度。TS 类型:`ImageScale` `interface ImageScale { max: number; min: number; step: number }`。[详细类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/image-viewer/type.ts) | N -images | Array | [] | 图片数组。`mainImage` 表示主图,必传;`thumbnail` 表示缩略图,如果不存在,则使用主图显示;`download` 是否允许下载图片,默认允许下载。示例: `['img_url_1', 'img_url_2']`,`[{ thumbnail: 'small_image_url', mainImage: 'big_image_url', download: false }]`。TS 类型:`Array` `interface ImageInfo { mainImage: string; thumbnail?: string; download?: boolean }`。[详细类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/image-viewer/type.ts) | N -index | Number | - | 当前预览图片所在的下标 | N -defaultIndex | Number | - | 当前预览图片所在的下标。非受控属性 | N +images | Array | [] | 图片数组。`mainImage` 表示主图,必传;`thumbnail` 表示缩略图,如果不存在,则使用主图显示;`download` 是否允许下载图片,默认允许下载。示例: `['img_url_1', 'img_url_2']`,`[{ thumbnail: 'small_image_url', mainImage: 'big_image_url', download: false }]`。TS 类型:`Array` `interface ImageInfo { mainImage: string \| File; thumbnail?: string \| File; download?: boolean }`。[详细类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/image-viewer/type.ts) | N +index | Number | 0 | 当前预览图片所在的下标 | N +defaultIndex | Number | 0 | 当前预览图片所在的下标。非受控属性 | N mode | String | modal | 模态预览(modal)和非模态预览(modeless)。可选项:modal/modeless | N navigationArrow | TNode | true | 切换预览图片的左图标,可自定义。TS 类型:`boolean \| TNode`。[通用类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/common.ts) | N showOverlay | Boolean | undefined | 是否显示遮罩层。`mode=modal` 时,默认显示;`mode=modeless` 时,默认不显示 | N title | TNode | - | 预览标题。TS 类型:`string \| TNode`。[通用类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/common.ts) | N -trigger | TNode | - | 触发图片预览的元素,可能是一个预览按钮,可能是一张缩略图,完全自定义。TS 类型:`string \| TNode<{ open: () => void }>`。[通用类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/common.ts) | N +trigger | TNode | - | 触发图片预览的元素,可能是一个预览按钮,可能是一张缩略图,完全自定义。TS 类型:`TNode \| TNode<{ open: () => void }>`。[通用类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/common.ts) | N viewerScale | Object | - | 限制预览器缩放的最小宽度和最小高度,仅 `mode=modeless` 时有效。TS 类型:`ImageViewerScale` `interface ImageViewerScale { minWidth: number; minHeight: number }`。[详细类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/image-viewer/type.ts) | N visible | Boolean | false | 隐藏/显示预览 | N defaultVisible | Boolean | false | 隐藏/显示预览。非受控属性 | N zIndex | Number | - | 层级,默认为 2000 | N onClose | Function | | TS 类型:`(context: { trigger: 'close-btn' \| 'overlay' \| 'esc'; e: MouseEvent \| KeyboardEvent }) => void`
关闭时触发,事件参数包含触发关闭的来源:关闭按钮、遮罩层、ESC 键 | N -onIndexChange | Function | | TS 类型:`(index: number, context: { trigger: 'prev' \| 'next' }) => void`
预览图片切换时触发,`context.prev` 切换到上一张图片,`context.next` 切换到下一张图片 | N +onIndexChange | Function | | TS 类型:`(index: number, context: { trigger: 'prev' \| 'next' \| 'current' }) => void`
预览图片切换时触发,`context.prev` 切换到上一张图片,`context.next` 切换到下一张图片 | N diff --git a/src/image-viewer/type.ts b/src/image-viewer/type.ts index 72ee218898..790113d7c1 100644 --- a/src/image-viewer/type.ts +++ b/src/image-viewer/type.ts @@ -29,13 +29,15 @@ export interface TdImageViewerProps { * 图片数组。`mainImage` 表示主图,必传;`thumbnail` 表示缩略图,如果不存在,则使用主图显示;`download` 是否允许下载图片,默认允许下载。示例: `['img_url_1', 'img_url_2']`,`[{ thumbnail: 'small_image_url', mainImage: 'big_image_url', download: false }]` * @default [] */ - images?: Array; + images?: Array; /** * 当前预览图片所在的下标 + * @default 0 */ index?: number; /** * 当前预览图片所在的下标,非受控属性 + * @default 0 */ defaultIndex?: number; /** @@ -45,6 +47,7 @@ export interface TdImageViewerProps { mode?: 'modal' | 'modeless'; /** * 切换预览图片的左图标,可自定义 + * @default true */ navigationArrow?: TNode; /** @@ -58,7 +61,7 @@ export interface TdImageViewerProps { /** * 触发图片预览的元素,可能是一个预览按钮,可能是一张缩略图,完全自定义 */ - trigger?: TNode | TNode<{ onOpen: () => void }>; + trigger?: TNode | TNode<{ open: () => void }>; /** * 限制预览器缩放的最小宽度和最小高度,仅 `mode=modeless` 时有效 */ @@ -84,7 +87,7 @@ export interface TdImageViewerProps { /** * 预览图片切换时触发,`context.prev` 切换到上一张图片,`context.next` 切换到下一张图片 */ - onIndexChange?: (index: number, context: { trigger: 'prev' | 'next' }) => void; + onIndexChange?: (index: number, context: { trigger: 'prev' | 'next' | 'current' }) => void; } export interface ImageScale { @@ -94,8 +97,8 @@ export interface ImageScale { } export interface ImageInfo { - mainImage: string; - thumbnail?: string; + mainImage: string | File; + thumbnail?: string | File; download?: boolean; } diff --git a/src/image/Image.tsx b/src/image/Image.tsx index fc174362e8..754d68b1e9 100644 --- a/src/image/Image.tsx +++ b/src/image/Image.tsx @@ -1,4 +1,4 @@ -import React, { Fragment, useEffect, useRef, useState, SyntheticEvent, useMemo } from 'react'; +import React, { Fragment, useEffect, useRef, useState, SyntheticEvent, MouseEvent } from 'react'; import classNames from 'classnames'; import isFunction from 'lodash/isFunction'; import { ImageErrorIcon as TdImageErrorIcon, ImageIcon as TdImageIcon } from 'tdesign-icons-react'; @@ -11,8 +11,14 @@ import Space from '../space'; import useGlobalIcon from '../hooks/useGlobalIcon'; import { StyledProps } from '../common'; import useDefaultProps from '../hooks/useDefaultProps'; +import useImagePreviewUrl from '../hooks/useImagePreviewUrl'; -export type ImageProps = TdImageProps & StyledProps; +export type ImageProps = TdImageProps & + StyledProps & { + onClick?: (e: MouseEvent) => void; + onMouseDown?: (e: MouseEvent) => void; + draggable?: boolean; + }; const InternalImage: React.ForwardRefRenderFunction = (originalProps, ref) => { const props = useDefaultProps(originalProps, imageDefaultProps); @@ -32,6 +38,7 @@ const InternalImage: React.ForwardRefRenderFunction gallery, overlayContent, srcset, + fallback, onLoad, onError, ...rest @@ -47,11 +54,16 @@ const InternalImage: React.ForwardRefRenderFunction React.useImperativeHandle(ref, () => imageRef.current); - // replace image url - const imageSrc = useMemo( - () => (isFunction(local.replaceImageSrc) ? local.replaceImageSrc(props) : src), - [src, local, props], - ); + const [imageSrc, setImageSrc] = useState(src); + + useEffect(() => { + const tmpUrl = isFunction(local.replaceImageSrc) ? local.replaceImageSrc(props) : src; + if (tmpUrl === imageSrc && imageSrc) return; + setImageSrc(tmpUrl); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [src, local, props]); + + const { previewUrl } = useImagePreviewUrl(imageSrc); const [shouldLoad, setShouldLoad] = useState(!lazy); const handleLoadImage = () => { @@ -83,9 +95,20 @@ const InternalImage: React.ForwardRefRenderFunction const [hasError, setHasError] = useState(false); const handleError = (e: SyntheticEvent) => { setHasError(true); + if (fallback) { + setImageSrc(fallback); + setHasError(false); + } onError?.({ e }); }; + useEffect(() => { + if (hasError && previewUrl) { + setHasError(false); + } + // eslint-disable-next-line + }, [previewUrl]); + const hasMouseEvent = overlayTrigger === 'hover'; const [shouldShowOverlay, setShouldShowOverlay] = useState(!hasMouseEvent); const handleToggleOverlay = (overlay: boolean) => { @@ -121,26 +144,29 @@ const InternalImage: React.ForwardRefRenderFunction return
; }; - const renderImage = (url: string) => ( - {alt} - ); + const renderImage = () => { + const url = typeof imageSrc === 'string' ? imageSrc : previewUrl; + return ( + {alt} + ); + }; const renderImageSrcset = () => ( {Object.entries(props.srcset).map(([type, url]) => ( ))} - {props.src && renderImage(props.src)} + {props.src && renderImage()} ); @@ -169,13 +195,14 @@ const InternalImage: React.ForwardRefRenderFunction {!(hasError || !shouldLoad) && ( - {srcset && Object.keys(srcset).length ? renderImageSrcset() : renderImage(imageSrc)} + {srcset && Object.keys(srcset).length ? renderImageSrcset() : renderImage()} {!(hasError || !shouldLoad) && !isLoaded && (
{loading || ( - {t(local.loadingText)} + {/* support loading = '' to hide loading text */} + {typeof loading === 'string' ? loading : t(local.loadingText)} )}
@@ -188,7 +215,7 @@ const InternalImage: React.ForwardRefRenderFunction {error || ( - {t(local.errorText)} + {typeof error === 'string' ? error : t(local.errorText)} )}
diff --git a/src/image/__tests__/__snapshots__/vitest-image.test.jsx.snap b/src/image/__tests__/__snapshots__/vitest-image.test.jsx.snap index 3a49a94752..27eda6f09d 100644 --- a/src/image/__tests__/__snapshots__/vitest-image.test.jsx.snap +++ b/src/image/__tests__/__snapshots__/vitest-image.test.jsx.snap @@ -3,30 +3,35 @@ exports[`Image Component > props.fit is equal to contain 1`] = ` `; exports[`Image Component > props.fit is equal to cover 1`] = ` `; exports[`Image Component > props.fit is equal to fill 1`] = ` `; exports[`Image Component > props.fit is equal to none 1`] = ` `; exports[`Image Component > props.fit is equal to scale-down 1`] = ` `; @@ -37,6 +42,7 @@ exports[`Image Component > props.loading works fine 1`] = ` >
props.overlayContent works fine 1`] = ` >
props.placeholder works fine 1`] = `
{ + const timer = setTimeout(() => { + setSrc('https://tdesign.gtimg.com/demo/demo-image-1.png'); + }, 100); + + return () => { + clearTimeout(timer); + }; + }, []); + return ( { ['fill', 'contain', 'cover', 'none', 'scale-down'].map(item => ( {item} diff --git a/src/image/_example/placeholder.jsx b/src/image/_example/placeholder.jsx index 7983a1486f..ec13aa670f 100644 --- a/src/image/_example/placeholder.jsx +++ b/src/image/_example/placeholder.jsx @@ -46,7 +46,7 @@ export default function PlaceholderImage() { 默认错误 { @@ -59,7 +59,7 @@ export default function PlaceholderImage() { 自定义错误 - } /> + } />
diff --git a/src/image/image.en-US.md b/src/image/image.en-US.md index 47417d94b5..0639351646 100644 --- a/src/image/image.en-US.md +++ b/src/image/image.en-US.md @@ -9,16 +9,18 @@ className | String | - | 类名 | N style | Object | - | 样式,Typescript:`React.CSSProperties` | N alt | String | - | \- | N error | TNode | - | Typescript:`string \| TNode`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/src/common.ts) | N -fit | String | fill | options:contain/cover/fill/none/scale-down | N +fallback | String | - | display `fallback` image on `src` loading failed. you can also use `error` to define more complex error content | N +fit | String | fill | options: contain/cover/fill/none/scale-down | N gallery | Boolean | false | \- | N lazy | Boolean | false | \- | N loading | TNode | - | Typescript:`string \| TNode`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/src/common.ts) | N overlayContent | TNode | - | overlay on the top of image。Typescript:`string \| TNode`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/src/common.ts) | N -overlayTrigger | String | always | options:always/hover | N +overlayTrigger | String | always | options: always/hover | N placeholder | TNode | - | Typescript:`string \| TNode`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/src/common.ts) | N position | String | center | \- | N -shape | String | square | options:circle/round/square | N -src | String | - | \- | N -srcset | Object | - | for `.avif` and `.webp` image url。Typescript:`ImageSrcset` `interface ImageSrcset { 'image/avif': string; 'image/webp': string; }`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/src/image/type.ts) | N +referrerpolicy | String | - | attribute of ``, [MDN Definition](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy)。options: no-referrer/no-referrer-when-downgrade/origin/origin-when-cross-origin/same-origin/strict-origin/strict-origin-when-cross-origin/unsafe-url | N +shape | String | square | options: circle/round/square | N +src | String / Object | - | src attribute of ``. image File can also be loaded。Typescript:`string \| File` | N +srcset | Object | - | for `.avif` and `.webp` image url, load `srcset` before `src`。Typescript:`ImageSrcset` `interface ImageSrcset { 'image/avif': string; 'image/webp': string; }`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/src/image/type.ts) | N onError | Function | | Typescript:`(context: { e: ImageEvent }) => void`
trigger on image load failed | N onLoad | Function | | Typescript:`(context: { e: ImageEvent }) => void`
trigger on image loaded | N diff --git a/src/image/image.md b/src/image/image.md index 34b46aae03..25c004e456 100644 --- a/src/image/image.md +++ b/src/image/image.md @@ -9,6 +9,7 @@ className | String | - | 类名 | N style | Object | - | 样式,TS 类型:`React.CSSProperties` | N alt | String | - | 图片描述 | N error | TNode | - | 自定义图片加载失败状态下的显示内容。TS 类型:`string \| TNode`。[通用类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/common.ts) | N +fallback | String | - | 图片加载失败时,显示当前链接设置的图片地址。如果要使用组件图标或完全自定义加载失败时显示的内容,请更为使用 `error` | N fit | String | fill | 图片填充模式。可选项:contain/cover/fill/none/scale-down | N gallery | Boolean | false | 是否展示为图集样式 | N lazy | Boolean | false | 是否开启图片懒加载 | N @@ -17,8 +18,9 @@ overlayContent | TNode | - | 图片上方的浮层内容。TS 类型:`string \ overlayTrigger | String | always | 浮层 `overlayContent` 出现的时机。可选项:always/hover | N placeholder | TNode | - | 占位元素,展示层级低于 `loading` `error` 和图片本身,值类型为字符串时表示占位图片地址。TS 类型:`string \| TNode`。[通用类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/common.ts) | N position | String | center | 等同于原生的 object-position 属性,可选值为 top right bottom left 或 string,可以自定义任何单位,px 或者 百分比 | N +referrerpolicy | String | - | `` 标签的原生属性,[MDN 定义](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy)。可选项:no-referrer/no-referrer-when-downgrade/origin/origin-when-cross-origin/same-origin/strict-origin/strict-origin-when-cross-origin/unsafe-url | N shape | String | square | 图片圆角类型。可选项:circle/round/square | N -src | String | - | 图片链接 | N -srcset | Object | - | 图片地址,支持特殊格式的图片,如 `.avif` 和 `.webp`。TS 类型:`ImageSrcset` `interface ImageSrcset { 'image/avif': string; 'image/webp': string; }`。[详细类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/image/type.ts) | N +src | String / Object | - | 用于显示图片的链接或原始图片文件对象。TS 类型:`string \| File` | N +srcset | Object | - | 图片链接集合,用于支持特殊格式的图片,如 `.avif` 和 `.webp`。会优先加载 `srcset` 中的图片格式,浏览器不支持的情况下,加载 `src` 设置的图片地址。TS 类型:`ImageSrcset` `interface ImageSrcset { 'image/avif': string; 'image/webp': string; }`。[详细类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/image/type.ts) | N onError | Function | | TS 类型:`(context: { e: ImageEvent }) => void`
图片加载失败时触发 | N onLoad | Function | | TS 类型:`(context: { e: ImageEvent }) => void`
图片加载完成时触发 | N diff --git a/src/image/type.ts b/src/image/type.ts index 71988ee16f..0ba1f34a85 100644 --- a/src/image/type.ts +++ b/src/image/type.ts @@ -16,6 +16,11 @@ export interface TdImageProps { * 自定义图片加载失败状态下的显示内容 */ error?: TNode; + /** + * 图片加载失败时,显示当前链接设置的图片地址。如果要使用组件图标或完全自定义加载失败时显示的内容,请更为使用 `error` + * @default '' + */ + fallback?: string; /** * 图片填充模式 * @default fill @@ -53,18 +58,29 @@ export interface TdImageProps { * @default center */ position?: string; + /** + * `` 标签的原生属性,[MDN 定义](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy) + */ + referrerpolicy?: + | 'no-referrer' + | 'no-referrer-when-downgrade' + | 'origin' + | 'origin-when-cross-origin' + | 'same-origin' + | 'strict-origin' + | 'strict-origin-when-cross-origin' + | 'unsafe-url'; /** * 图片圆角类型 * @default square */ shape?: 'circle' | 'round' | 'square'; /** - * 图片链接 - * @default '' + * 用于显示图片的链接或原始图片文件对象 */ - src?: string; + src?: string | File; /** - * 图片地址,支持特殊格式的图片,如 `.avif` 和 `.webp` + * 图片链接集合,用于支持特殊格式的图片,如 `.avif` 和 `.webp`。会优先加载 `srcset` 中的图片格式,浏览器不支持的情况下,加载 `src` 设置的图片地址 */ srcset?: ImageSrcset; /** diff --git a/src/menu/HeadMenu.tsx b/src/menu/HeadMenu.tsx index 1078da4692..d4fa900280 100644 --- a/src/menu/HeadMenu.tsx +++ b/src/menu/HeadMenu.tsx @@ -49,8 +49,8 @@ const HeadMenu: FC = (props) => { value={currentChildListValues.includes(value.active) ? value.active : currentChildListValues[0]} onChange={value.onChange} > - {childList.map(({ props }) => ( - + {childList.map(({ props: { children, ...restProps } }) => ( + ))} diff --git a/src/menu/__tests__/menu.test.tsx b/src/menu/__tests__/menu.test.tsx index c62d82f084..8b3bb4255a 100644 --- a/src/menu/__tests__/menu.test.tsx +++ b/src/menu/__tests__/menu.test.tsx @@ -170,4 +170,22 @@ describe('Menu 组件测试', () => { fireEvent.click(getByText('列表项')); expect(ulNode.style.maxHeight).not.toBe('0'); }); + + test('menu 測試 menuItem onClick事件', () => { + const clickFn = vi.fn(); + const { getByText } = render( + + + + 仪表盘 + + + + 基础列表项 + + , + ); + fireEvent.click(getByText('仪表盘')); + expect(clickFn).toHaveBeenCalledTimes(1); + }); }); diff --git a/src/steps/StepItem.tsx b/src/steps/StepItem.tsx index 968a60dad9..cad45d4d29 100644 --- a/src/steps/StepItem.tsx +++ b/src/steps/StepItem.tsx @@ -39,11 +39,7 @@ const StepItem = (props: StepItemProps) => { return {(globalStepsConfig.errorIcon || ) as React.ReactNode}; if (status === 'finish') - return ( - - - - ); + return {(globalStepsConfig.checkIcon || ) as React.ReactNode}; return {Number(index) + 1}; } diff --git a/src/table/BaseTable.tsx b/src/table/BaseTable.tsx index 65d84cacbc..95b6fe7173 100644 --- a/src/table/BaseTable.tsx +++ b/src/table/BaseTable.tsx @@ -1,7 +1,7 @@ import React, { useRef, useMemo, useImperativeHandle, forwardRef, useEffect, useState, WheelEvent } from 'react'; import pick from 'lodash/pick'; import classNames from 'classnames'; -import TBody, { extendTableProps } from './TBody'; +import TBody, { extendTableProps, TableBodyProps } from './TBody'; import { Affix } from '../affix'; import { ROW_LISTENERS } from './TR'; import THead from './THead'; @@ -49,8 +49,10 @@ const BaseTable = forwardRef((props, ref) => { const tableElmRef = useRef(); const bottomContentRef = useRef(); const [tableFootHeight, setTableFootHeight] = useState(0); + const allTableClasses = useClassName(); + const { classPrefix, virtualScrollClasses, tableLayoutClasses, tableBaseClass, tableColFixedClasses } = - useClassName(); + allTableClasses; // 表格基础样式类 const { tableClasses, sizeClassNames, tableContentStyles, tableElementStyles } = useStyle(props); const { isMultipleHeader, spansAndLeafNodes, thList } = useTableHeader({ columns: props.columns }); @@ -422,7 +424,7 @@ const BaseTable = forwardRef((props, ref) => { return affixedFooter; }; - const tableBodyProps = { + const tableBodyProps: TableBodyProps = { classPrefix, ellipsisOverlayClassName: props.size !== 'medium' ? sizeClassNames[props.size] : '', rowAndColFixedPosition, @@ -431,10 +433,11 @@ const BaseTable = forwardRef((props, ref) => { virtualConfig, handleRowMounted: virtualConfig.handleRowMounted, columns: spansAndLeafNodes?.leafColumns || columns, - tableElm: tableRef.current, - tableContentElm: tableContentRef.current, - tableWidth: tableWidth.current, + tableRef, + tableContentRef, + tableWidth, isWidthOverflow, + allTableClasses, rowKey: props.rowKey || 'id', scroll: props.scroll, cellEmptyContent: props.cellEmptyContent, @@ -483,15 +486,15 @@ const BaseTable = forwardRef((props, ref) => { ), // eslint-disable-next-line [ - tableBodyProps.classPrefix, + allTableClasses, tableBodyProps.ellipsisOverlayClassName, tableBodyProps.rowAndColFixedPosition, tableBodyProps.showColumnShadow, tableBodyProps.data, tableBodyProps.columns, - tableBodyProps.tableElm, - tableBodyProps.tableContentElm, - tableBodyProps.tableWidth, + tableRef, + tableContentRef, + tableWidth, isWidthOverflow, props.rowKey, props.rowClassName, diff --git a/src/table/Cell.tsx b/src/table/Cell.tsx index 980e3d3be2..eec7f5e1c1 100644 --- a/src/table/Cell.tsx +++ b/src/table/Cell.tsx @@ -1,4 +1,4 @@ -import React, { MouseEvent } from 'react'; +import React, { MouseEvent, MutableRefObject } from 'react'; import classNames from 'classnames'; import isFunction from 'lodash/isFunction'; import get from 'lodash/get'; @@ -27,7 +27,7 @@ export interface CellProps { cellSpans: RowspanColspan; cellEmptyContent: TdBaseTableProps['cellEmptyContent']; tableClassNames: TableClassName; - tableElm?: HTMLDivElement; + tableRef?: MutableRefObject; classPrefix?: string; overlayClassName?: string; pagination?: PaginationProps; @@ -95,7 +95,7 @@ function renderEllipsisCell(cellParams: BaseTableCellParams, param } const Cell = (props: CellProps) => { - const { cellParams, tableClassNames, tableElm, columnLength, classPrefix, overlayClassName, pagination } = props; + const { cellParams, tableClassNames, tableRef, columnLength, classPrefix, overlayClassName, pagination } = props; const { col, colIndex, rowIndex } = cellParams; const { cellSpans, dataLength, rowAndColFixedPosition, cellEmptyContent, rowspanAndColspan, onClick } = props; const { tableColFixedClasses, tdEllipsisClass, tableBaseClass, tdAlignClasses, tableDraggableClasses } = @@ -127,7 +127,13 @@ const Cell = (props: CellProps) => { onClick={onClick} > {col.ellipsis - ? renderEllipsisCell(cellParams, { cellNode, tableElm, columnLength, classPrefix, overlayClassName }) + ? renderEllipsisCell(cellParams, { + cellNode, + tableElm: tableRef.current, + columnLength, + classPrefix, + overlayClassName, + }) : cellNode} ); diff --git a/src/table/EditableCell.tsx b/src/table/EditableCell.tsx index 9dffcbec81..062dda0d28 100644 --- a/src/table/EditableCell.tsx +++ b/src/table/EditableCell.tsx @@ -221,7 +221,7 @@ const EditableCell = (props: EditableCellProps) => { const params = { ...cellParams, value: val, - editedRow: { ...props.row, [props.col.colKey]: val }, + editedRow: set({ ...props.row }, props.col.colKey, val), }; props.onChange?.(params); props.onRuleChange?.(params); diff --git a/src/table/EnhancedTable.tsx b/src/table/EnhancedTable.tsx index fc6a85c874..135ecfedf4 100644 --- a/src/table/EnhancedTable.tsx +++ b/src/table/EnhancedTable.tsx @@ -17,7 +17,8 @@ const EnhancedTable = forwardRef((props, const primaryTableRef = useRef(); // treeInstanceFunctions 属于对外暴露的 Ref 方法 - const { store, dataSource, formatTreeColumn, swapData, ...treeInstanceFunctions } = useTreeData(props); + const { store, dataSource, formatTreeColumn, swapData, onExpandFoldIconClick, ...treeInstanceFunctions } = + useTreeData(props); const treeDataMap = store?.treeDataMap; @@ -48,7 +49,7 @@ const EnhancedTable = forwardRef((props, const onEnhancedTableRowClick: TdPrimaryTableProps['onRowClick'] = (p) => { if (props.tree?.expandTreeNodeOnClick) { - treeInstanceFunctions.toggleExpandData( + onExpandFoldIconClick( { row: p.row, rowIndex: p.index, diff --git a/src/table/TBody.tsx b/src/table/TBody.tsx index 4444de8e23..e744ed245d 100644 --- a/src/table/TBody.tsx +++ b/src/table/TBody.tsx @@ -1,11 +1,11 @@ -import React, { useMemo } from 'react'; +import React, { MutableRefObject, useMemo } from 'react'; import camelCase from 'lodash/camelCase'; import get from 'lodash/get'; import pick from 'lodash/pick'; import classNames from 'classnames'; import TR, { ROW_LISTENERS, TABLE_PROPS } from './TR'; import { useLocaleReceiver } from '../locale/LocalReceiver'; -import useClassName from './hooks/useClassName'; +import { TableClassName } from './hooks/useClassName'; import useRowspanAndColspan from './hooks/useRowspanAndColspan'; import { BaseTableProps, RowAndColFixedPosition } from './interface'; import { TdBaseTableProps } from './type'; @@ -19,13 +19,14 @@ export interface TableBodyProps extends BaseTableProps { // 固定列 left/right 具体值 rowAndColFixedPosition?: RowAndColFixedPosition; showColumnShadow?: { left: boolean; right: boolean }; - tableElm?: HTMLDivElement; - tableContentElm?: HTMLDivElement; + tableRef?: MutableRefObject; + tableContentRef?: MutableRefObject; cellEmptyContent: TdBaseTableProps['cellEmptyContent']; - tableWidth?: number; + tableWidth?: MutableRefObject; isWidthOverflow?: boolean; virtualConfig: VirtualScrollConfig; pagination?: PaginationProps; + allTableClasses?: TableClassName; handleRowMounted?: (rowData: any) => void; } @@ -58,10 +59,12 @@ export const extendTableProps = [ export default function TBody(props: TableBodyProps) { // 如果不是变量复用,没必要对每一个参数进行解构(解构过程需要单独的内存空间存储临时变量) - const { data, columns, rowKey, firstFullRow, lastFullRow, virtualConfig } = props; + const { data, columns, rowKey, firstFullRow, lastFullRow, virtualConfig, allTableClasses } = props; const [global, t] = useLocaleReceiver('table'); - const { tableFullRowClasses, tableBaseClass } = useClassName(); + const { tableFullRowClasses, tableBaseClass } = allTableClasses; const { skipSpansMap } = useRowspanAndColspan(data, columns, rowKey, props.rowspanAndColspan); + const columnLength = columns.length; + const dataLength = data?.length; const tbodyClasses = useMemo(() => [tableBaseClass.body], [tableBaseClass.body]); const hasFullRowConfig = useMemo(() => firstFullRow || lastFullRow, [firstFullRow, lastFullRow]); @@ -104,69 +107,77 @@ export default function TBody(props: TableBodyProps) { ); }; - const columnLength = columns.length; - const dataLength = data?.length; - const trNodeList = []; - - const properties = [ - 'classPrefix', - 'ellipsisOverlayClassName', - 'rowAndColFixedPosition', - 'scroll', - 'tableElm', - 'tableContentElm', - 'trs', - 'bufferSize', - 'isVirtual', - 'rowHeight', - 'scrollType', - ]; - data?.forEach((row, rowIndex) => { - const trProps = { - ...pick(props, TABLE_PROPS), - rowKey: props.rowKey || 'id', - row, - columns, - // eslint-disable-next-line - rowIndex: row.__VIRTUAL_SCROLL_INDEX || rowIndex, - dataLength, - skipSpansMap, - virtualConfig, - classPrefix: props.classPrefix, - ellipsisOverlayClassName: props.ellipsisOverlayClassName, - ...pick(props, properties), - pagination: props.pagination, - }; - if (props.onCellClick) { - trProps.onCellClick = props.onCellClick; - } - - const trNode = ( - - ); - trNodeList.push(trNode); + const firstFullRowNode = useMemo( + () => getFullRow(columnLength, 'first-full-row'), + // eslint-disable-next-line react-hooks/exhaustive-deps + [firstFullRow, columnLength, getFullRow], + ); - // 执行展开行渲染 - if (props.renderExpandedRow) { - const p = { + const lastFullRowNode = useMemo( + () => getFullRow(columnLength, 'last-full-row'), + // eslint-disable-next-line react-hooks/exhaustive-deps + [lastFullRow, columnLength, getFullRow], + ); + + const isSkipSnapsMapNotFinish = Boolean(props.rowspanAndColspan && !skipSpansMap.size); + + const getTRNodeList = () => { + if (isSkipSnapsMapNotFinish) return null; + const trNodeList = []; + const properties = [ + 'classPrefix', + 'ellipsisOverlayClassName', + 'rowAndColFixedPosition', + 'scroll', + 'tableRef', + 'tableContentRef', + 'trs', + 'bufferSize', + 'isVirtual', + 'rowHeight', + 'scrollType', + ]; + data?.forEach((row, rowIndex) => { + const trProps = { + ...pick(props, TABLE_PROPS), + rowKey: props.rowKey || 'id', row, - index: rowIndex, columns, - tableWidth: props.tableWidth, - isWidthOverflow: props.isWidthOverflow, + // eslint-disable-next-line + rowIndex: row.__VIRTUAL_SCROLL_INDEX || rowIndex, + dataLength, + skipSpansMap, + virtualConfig, + classPrefix: props.classPrefix, + ellipsisOverlayClassName: props.ellipsisOverlayClassName, + ...pick(props, properties), + pagination: props.pagination, }; - const expandedContent = props.renderExpandedRow(p); - expandedContent && trNodeList.push(expandedContent); - } - }); + if (props.onCellClick) { + trProps.onCellClick = props.onCellClick; + } + + const trNode = ( + + ); + trNodeList.push(trNode); + + // 执行展开行渲染 + if (props.renderExpandedRow) { + const p = { + row, + index: rowIndex, + columns, + tableWidth: props.tableWidth, + isWidthOverflow: props.isWidthOverflow, + }; + const expandedContent = props.renderExpandedRow(p); + expandedContent && trNodeList.push(expandedContent); + } + }); + return trNodeList; + }; - const list = ( - <> - {getFullRow(columnLength, 'first-full-row')} - {trNodeList} - {getFullRow(columnLength, 'last-full-row')} - - ); const isEmpty = !data?.length && !props.loading && !hasFullRowConfig; // 垫上隐藏的 tr 元素高度 @@ -180,6 +191,14 @@ export default function TBody(props: TableBodyProps) { } : undefined; + const list = ( + <> + {firstFullRowNode} + {getTRNodeList()} + {lastFullRowNode} + + ); + return ( {isEmpty ? renderEmpty(columns) : list} diff --git a/src/table/TR.tsx b/src/table/TR.tsx index 915ec23593..111dac27e9 100644 --- a/src/table/TR.tsx +++ b/src/table/TR.tsx @@ -1,4 +1,4 @@ -import React, { useMemo, useRef, MouseEvent, useEffect } from 'react'; +import React, { useMemo, useRef, MouseEvent, useEffect, MutableRefObject } from 'react'; import get from 'lodash/get'; import classnames from 'classnames'; import { formatRowAttributes, formatRowClassNames } from './utils'; @@ -52,8 +52,8 @@ export interface TrProps extends TrCommonProps { trs?: Map; bufferSize?: number; scroll?: InfinityScroll; - tableElm?: HTMLDivElement; - tableContentElm?: HTMLDivElement; + tableRef?: MutableRefObject; + tableContentRef?: MutableRefObject; pagination?: PaginationProps; virtualConfig?: VirtualScrollConfig; onRowMounted?: (data: any) => void; @@ -72,7 +72,7 @@ export default function TR(props: TrProps) { dataLength, fixedRows, scroll, - tableContentElm, + tableContentRef, rowAndColFixedPosition, virtualConfig, onRowMounted, @@ -102,7 +102,7 @@ export default function TR(props: TrProps) { }, [row, rowClassName, rowIndex, rowKey, trStyles?.classes]); const useLazyLoadParams = useMemo(() => ({ ...scroll, rowIndex }), [scroll, rowIndex]); - const { hasLazyLoadHolder, tRowHeight } = useLazyLoad(tableContentElm, trRef, useLazyLoadParams); + const { hasLazyLoadHolder, tRowHeight } = useLazyLoad(tableContentRef.current, trRef, useLazyLoadParams); useEffect(() => { if (virtualConfig.isVirtualScroll && trRef.current) { @@ -123,7 +123,7 @@ export default function TR(props: TrProps) { colIndex, }; let spanState = null; - if (props.skipSpansMap.size) { + if (props.skipSpansMap?.size) { const cellKey = getCellKey(row, props.rowKey, col.colKey, colIndex); spanState = props.skipSpansMap.get(cellKey) || {}; spanState?.rowspan > 1 && (cellSpans.rowspan = spanState.rowspan); @@ -149,7 +149,7 @@ export default function TR(props: TrProps) { tableClassNames={classNames} rowspanAndColspan={props.rowspanAndColspan} onClick={onClick} - tableElm={props.tableElm} + tableRef={props.tableRef} classPrefix={props.classPrefix} overlayClassName={props.ellipsisOverlayClassName} pagination={props.pagination} diff --git a/src/table/_example/editable-cell.jsx b/src/table/_example/editable-cell.jsx index e0b5f4a923..5c6d55d869 100644 --- a/src/table/_example/editable-cell.jsx +++ b/src/table/_example/editable-cell.jsx @@ -115,6 +115,7 @@ export default function EditableCellTable() { title: '申请事项', colKey: 'letters', cell: ({ row }) => row?.letters?.join('、'), + width: 280, edit: { keepEditMode: true, component: Select, diff --git a/src/table/_example/editable-row.jsx b/src/table/_example/editable-row.jsx index b4e7c578c4..b525e03a63 100644 --- a/src/table/_example/editable-row.jsx +++ b/src/table/_example/editable-row.jsx @@ -187,6 +187,7 @@ export default function EditableRowTable() { title: '申请事项', colKey: 'letters', cell: ({ row }) => row?.letters?.join('、'), + width: 200, edit: { component: Select, // props, 透传全部属性到 Select 组件 @@ -229,7 +230,6 @@ export default function EditableRowTable() { { title: '操作栏', colKey: 'operate', - width: 150, cell: ({ row }) => { const editable = editableRowKeys.includes(row.key); return ( diff --git a/src/table/hooks/tree-store.ts b/src/table/hooks/tree-store.ts index 7dd07e841a..e176ba2149 100644 --- a/src/table/hooks/tree-store.ts +++ b/src/table/hooks/tree-store.ts @@ -1,7 +1,7 @@ /* eslint-disable class-methods-use-this */ /* eslint-disable no-param-reassign */ import get from 'lodash/get'; -import { isRowSelectedDisabled } from '../utils'; +import { isRowSelectedDisabled } from '../../_common/js/table/utils'; import { PrimaryTableCol, TableRowState, TableRowValue, TableRowData } from '../type'; import log from '../../_common/js/log'; diff --git a/src/table/hooks/useClassName.ts b/src/table/hooks/useClassName.ts index 7fd82c6662..353d89d83c 100644 --- a/src/table/hooks/useClassName.ts +++ b/src/table/hooks/useClassName.ts @@ -1,187 +1,191 @@ +import { useMemo } from 'react'; import useConfig from '../../hooks/useConfig'; export default function useClassName() { const { classPrefix } = useConfig(); - const classNames = { - classPrefix, - tableBaseClass: { - table: `${classPrefix}-table`, - columnResizableTable: `${classPrefix}-table--column-resizable`, - body: `${classPrefix}-table__body`, - overflowVisible: `${classPrefix}-table--overflow-visible`, - content: `${classPrefix}-table__content`, - topContent: `${classPrefix}-table__top-content`, - bottomContent: `${classPrefix}-table__bottom-content`, - paginationWrap: `${classPrefix}-table__pagination-wrap`, - tdLastRow: `${classPrefix}-table__td-last-row`, - tdFirstCol: `${classPrefix}-table__td-first-col`, - thCellInner: `${classPrefix}-table__th-cell-inner`, - tableRowEdit: `${classPrefix}-table--row-edit`, - cellEditable: `${classPrefix}-table__cell--editable`, - cellEditWrap: `${classPrefix}-table__cell-wrap`, - bordered: `${classPrefix}-table--bordered`, - striped: `${classPrefix}-table--striped`, - hover: `${classPrefix}-table--hoverable`, - loading: `${classPrefix}-table--loading`, - rowspanAndColspan: `${classPrefix}-table--rowspan-colspan`, - empty: `${classPrefix}-table__empty`, - emptyRow: `${classPrefix}-table__empty-row`, - headerFixed: `${classPrefix}-table--header-fixed`, - columnFixed: `${classPrefix}-table--column-fixed`, - widthOverflow: `${classPrefix}-table--width-overflow`, - multipleHeader: `${classPrefix}-table--multiple-header`, - footerAffixed: `${classPrefix}-table--footer-affixed`, - horizontalBarAffixed: `${classPrefix}-table--horizontal-bar-affixed`, - affixedHeader: `${classPrefix}-table--affixed-header`, - affixedHeaderElm: `${classPrefix}-table__affixed-header-elm`, - affixedFooterElm: `${classPrefix}-table__affixed-footer-elm`, - affixedFooterWrap: `${classPrefix}-table__affixed-footer-wrap`, - // 边框模式,固定表头,横向滚动时,右侧添加边线,分隔滚动条 - scrollbarDivider: `${classPrefix}-table__scroll-bar-divider`, - // 当用户设置 height 为固定高度,为保证行元素铺满 table,则需设置 table 元素高度为 100% - fullHeight: `${classPrefix}-table--full-height`, - // 拖拽列时的标记线 - resizeLine: `${classPrefix}-table__resize-line`, - obviousScrollbar: `${classPrefix}-table__scrollbar--obvious`, - affixedHeaderWrap: `${classPrefix}-table__affixed-header-elm-wrap`, - }, - - tdAlignClasses: { - left: `${classPrefix}-align-left`, - right: `${classPrefix}-align-right`, - center: `${classPrefix}-align-center`, - }, - - tableHeaderClasses: { - header: `${classPrefix}-table__header`, - thBordered: `${classPrefix}-table__header-th--bordered`, - fixed: `${classPrefix}-table__header--fixed`, - multipleHeader: `${classPrefix}-table__header--multiple`, - }, - - tableFooterClasses: { - footer: `${classPrefix}-table__footer`, - fixed: `${classPrefix}-table__footer--fixed`, - }, - - tableAlignClasses: { - top: `${classPrefix}-vertical-align-top`, - middle: `${classPrefix}-vertical-align-middle`, - bottom: `${classPrefix}-vertical-align-bottom`, - }, - - tableRowFixedClasses: { - top: `${classPrefix}-table__row--fixed-top`, - bottom: `${classPrefix}-table__row--fixed-bottom`, - firstBottom: `${classPrefix}-table__row--fixed-bottom-first`, - withoutBorderBottom: `${classPrefix}-table__row--without-border-bottom`, - }, - - tableColFixedClasses: { - left: `${classPrefix}-table__cell--fixed-left`, - right: `${classPrefix}-table__cell--fixed-right`, - lastLeft: `${classPrefix}-table__cell--fixed-left-last`, - firstRight: `${classPrefix}-table__cell--fixed-right-first`, - leftShadow: `${classPrefix}-table__content--scrollable-to-left`, - rightShadow: `${classPrefix}-table__content--scrollable-to-right`, - }, - - tableLayoutClasses: { - auto: `${classPrefix}-table--layout-auto`, - fixed: `${classPrefix}-table--layout-fixed`, - }, - - tdEllipsisClass: `${classPrefix}-table-td--ellipsis`, - - // 行通栏,一列铺满整行 - tableFullRowClasses: { - base: `${classPrefix}-table__row--full`, - innerFullRow: `${classPrefix}-table__row-full-inner`, - innerFullElement: `${classPrefix}-table__row-full-element`, - firstFullRow: `${classPrefix}-table__first-full-row`, - lastFullRow: `${classPrefix}-table__last-full-row`, - }, - - // 展开/收起行,全部类名 - tableExpandClasses: { - iconBox: `${classPrefix}-table__expand-box`, - iconCell: `${classPrefix}-table__expandable-icon-cell`, - row: `${classPrefix}-table__expanded-row`, - rowInner: `${classPrefix}-table__expanded-row-inner`, - expanded: `${classPrefix}-table__row--expanded`, - collapsed: `${classPrefix}-table__row--collapsed`, - }, - - // 排序功能,全部类名 - tableSortClasses: { - sortable: `${classPrefix}-table__cell--sortable`, - sortColumn: `${classPrefix}-table__sort-column`, - title: `${classPrefix}-table__cell--title`, - trigger: `${classPrefix}-table__cell--sort-trigger`, - doubleIcon: `${classPrefix}-table__double-icons`, - sortIcon: `${classPrefix}-table__sort-icon`, - iconDirection: { - asc: `${classPrefix}-table-sort-asc`, - desc: `${classPrefix}-table-sort-desc`, - }, - iconActive: `${classPrefix}-table__sort-icon--active`, - iconDefault: `${classPrefix}-icon-sort--default`, - }, - - // 行选中功能,全部类名 - tableSelectedClasses: { - selected: `${classPrefix}-table__row--selected`, - disabled: `${classPrefix}-table__row--disabled`, - checkCell: `${classPrefix}-table__cell-check`, - }, - - // 过滤功能,全部类名 - tableFilterClasses: { - filterable: `${classPrefix}-table__cell--filterable`, - popup: `${classPrefix}-table__filter-pop`, - icon: `${classPrefix}-table__filter-icon`, - popupContent: `${classPrefix}-table__filter-pop-content`, - result: `${classPrefix}-table__filter-result`, - inner: `${classPrefix}-table__row-filter-inner`, - bottomButtons: `${classPrefix}-table__filter--bottom-buttons`, - contentInner: `${classPrefix}-table__filter-pop-content-inner`, - iconWrap: `${classPrefix}-table__filter-icon-wrap`, - }, - - // 通用类名 - asyncLoadingClass: `${classPrefix}-table__async-loading`, - isFocusClass: `${classPrefix}-is-focus`, - isLoadingClass: `${classPrefix}-is-loading`, - isLoadMoreClass: `${classPrefix}-is-load-more`, - - // 树形结构类名 - tableTreeClasses: { - col: `${classPrefix}-table__tree-col`, - inlineCol: `${classPrefix}-table__tree-col--inline`, - icon: `${classPrefix}-table__tree-op-icon`, - leafNode: `${classPrefix}-table__tree-leaf-node`, - }, - - // 拖拽功能类名 - tableDraggableClasses: { - rowDraggable: `${classPrefix}-table--row-draggable`, - rowHandlerDraggable: `${classPrefix}-table--row-handler-draggable`, - colDraggable: `${classPrefix}-table--col-draggable`, - handle: `${classPrefix}-table__handle-draggable`, - ghost: `${classPrefix}-table__ele--draggable-ghost`, - chosen: `${classPrefix}-table__ele--draggable-chosen`, - dragging: `${classPrefix}-table__ele--draggable-dragging`, - dragSortTh: `${classPrefix}-table__th--drag-sort`, - }, - - virtualScrollClasses: { - cursor: `${classPrefix}-table__virtual-scroll-cursor`, - header: `${classPrefix}-table__virtual-scroll-header`, - }, - - positiveRotate90: `${classPrefix}-positive-rotate-90`, - negativeRotate180: `${classPrefix}-negative-rotate-180`, - }; + const classNames = useMemo( + () => ({ + classPrefix, + tableBaseClass: { + table: `${classPrefix}-table`, + columnResizableTable: `${classPrefix}-table--column-resizable`, + body: `${classPrefix}-table__body`, + overflowVisible: `${classPrefix}-table--overflow-visible`, + content: `${classPrefix}-table__content`, + topContent: `${classPrefix}-table__top-content`, + bottomContent: `${classPrefix}-table__bottom-content`, + paginationWrap: `${classPrefix}-table__pagination-wrap`, + tdLastRow: `${classPrefix}-table__td-last-row`, + tdFirstCol: `${classPrefix}-table__td-first-col`, + thCellInner: `${classPrefix}-table__th-cell-inner`, + tableRowEdit: `${classPrefix}-table--row-edit`, + cellEditable: `${classPrefix}-table__cell--editable`, + cellEditWrap: `${classPrefix}-table__cell-wrap`, + bordered: `${classPrefix}-table--bordered`, + striped: `${classPrefix}-table--striped`, + hover: `${classPrefix}-table--hoverable`, + loading: `${classPrefix}-table--loading`, + rowspanAndColspan: `${classPrefix}-table--rowspan-colspan`, + empty: `${classPrefix}-table__empty`, + emptyRow: `${classPrefix}-table__empty-row`, + headerFixed: `${classPrefix}-table--header-fixed`, + columnFixed: `${classPrefix}-table--column-fixed`, + widthOverflow: `${classPrefix}-table--width-overflow`, + multipleHeader: `${classPrefix}-table--multiple-header`, + footerAffixed: `${classPrefix}-table--footer-affixed`, + horizontalBarAffixed: `${classPrefix}-table--horizontal-bar-affixed`, + affixedHeader: `${classPrefix}-table--affixed-header`, + affixedHeaderElm: `${classPrefix}-table__affixed-header-elm`, + affixedFooterElm: `${classPrefix}-table__affixed-footer-elm`, + affixedFooterWrap: `${classPrefix}-table__affixed-footer-wrap`, + // 边框模式,固定表头,横向滚动时,右侧添加边线,分隔滚动条 + scrollbarDivider: `${classPrefix}-table__scroll-bar-divider`, + // 当用户设置 height 为固定高度,为保证行元素铺满 table,则需设置 table 元素高度为 100% + fullHeight: `${classPrefix}-table--full-height`, + // 拖拽列时的标记线 + resizeLine: `${classPrefix}-table__resize-line`, + obviousScrollbar: `${classPrefix}-table__scrollbar--obvious`, + affixedHeaderWrap: `${classPrefix}-table__affixed-header-elm-wrap`, + }, + + tdAlignClasses: { + left: `${classPrefix}-align-left`, + right: `${classPrefix}-align-right`, + center: `${classPrefix}-align-center`, + }, + + tableHeaderClasses: { + header: `${classPrefix}-table__header`, + thBordered: `${classPrefix}-table__header-th--bordered`, + fixed: `${classPrefix}-table__header--fixed`, + multipleHeader: `${classPrefix}-table__header--multiple`, + }, + + tableFooterClasses: { + footer: `${classPrefix}-table__footer`, + fixed: `${classPrefix}-table__footer--fixed`, + }, + + tableAlignClasses: { + top: `${classPrefix}-vertical-align-top`, + middle: `${classPrefix}-vertical-align-middle`, + bottom: `${classPrefix}-vertical-align-bottom`, + }, + + tableRowFixedClasses: { + top: `${classPrefix}-table__row--fixed-top`, + bottom: `${classPrefix}-table__row--fixed-bottom`, + firstBottom: `${classPrefix}-table__row--fixed-bottom-first`, + withoutBorderBottom: `${classPrefix}-table__row--without-border-bottom`, + }, + + tableColFixedClasses: { + left: `${classPrefix}-table__cell--fixed-left`, + right: `${classPrefix}-table__cell--fixed-right`, + lastLeft: `${classPrefix}-table__cell--fixed-left-last`, + firstRight: `${classPrefix}-table__cell--fixed-right-first`, + leftShadow: `${classPrefix}-table__content--scrollable-to-left`, + rightShadow: `${classPrefix}-table__content--scrollable-to-right`, + }, + + tableLayoutClasses: { + auto: `${classPrefix}-table--layout-auto`, + fixed: `${classPrefix}-table--layout-fixed`, + }, + + tdEllipsisClass: `${classPrefix}-table-td--ellipsis`, + + // 行通栏,一列铺满整行 + tableFullRowClasses: { + base: `${classPrefix}-table__row--full`, + innerFullRow: `${classPrefix}-table__row-full-inner`, + innerFullElement: `${classPrefix}-table__row-full-element`, + firstFullRow: `${classPrefix}-table__first-full-row`, + lastFullRow: `${classPrefix}-table__last-full-row`, + }, + + // 展开/收起行,全部类名 + tableExpandClasses: { + iconBox: `${classPrefix}-table__expand-box`, + iconCell: `${classPrefix}-table__expandable-icon-cell`, + row: `${classPrefix}-table__expanded-row`, + rowInner: `${classPrefix}-table__expanded-row-inner`, + expanded: `${classPrefix}-table__row--expanded`, + collapsed: `${classPrefix}-table__row--collapsed`, + }, + + // 排序功能,全部类名 + tableSortClasses: { + sortable: `${classPrefix}-table__cell--sortable`, + sortColumn: `${classPrefix}-table__sort-column`, + title: `${classPrefix}-table__cell--title`, + trigger: `${classPrefix}-table__cell--sort-trigger`, + doubleIcon: `${classPrefix}-table__double-icons`, + sortIcon: `${classPrefix}-table__sort-icon`, + iconDirection: { + asc: `${classPrefix}-table-sort-asc`, + desc: `${classPrefix}-table-sort-desc`, + }, + iconActive: `${classPrefix}-table__sort-icon--active`, + iconDefault: `${classPrefix}-icon-sort--default`, + }, + + // 行选中功能,全部类名 + tableSelectedClasses: { + selected: `${classPrefix}-table__row--selected`, + disabled: `${classPrefix}-table__row--disabled`, + checkCell: `${classPrefix}-table__cell-check`, + }, + + // 过滤功能,全部类名 + tableFilterClasses: { + filterable: `${classPrefix}-table__cell--filterable`, + popup: `${classPrefix}-table__filter-pop`, + icon: `${classPrefix}-table__filter-icon`, + popupContent: `${classPrefix}-table__filter-pop-content`, + result: `${classPrefix}-table__filter-result`, + inner: `${classPrefix}-table__row-filter-inner`, + bottomButtons: `${classPrefix}-table__filter--bottom-buttons`, + contentInner: `${classPrefix}-table__filter-pop-content-inner`, + iconWrap: `${classPrefix}-table__filter-icon-wrap`, + }, + + // 通用类名 + asyncLoadingClass: `${classPrefix}-table__async-loading`, + isFocusClass: `${classPrefix}-is-focus`, + isLoadingClass: `${classPrefix}-is-loading`, + isLoadMoreClass: `${classPrefix}-is-load-more`, + + // 树形结构类名 + tableTreeClasses: { + col: `${classPrefix}-table__tree-col`, + inlineCol: `${classPrefix}-table__tree-col--inline`, + icon: `${classPrefix}-table__tree-op-icon`, + leafNode: `${classPrefix}-table__tree-leaf-node`, + }, + + // 拖拽功能类名 + tableDraggableClasses: { + rowDraggable: `${classPrefix}-table--row-draggable`, + rowHandlerDraggable: `${classPrefix}-table--row-handler-draggable`, + colDraggable: `${classPrefix}-table--col-draggable`, + handle: `${classPrefix}-table__handle-draggable`, + ghost: `${classPrefix}-table__ele--draggable-ghost`, + chosen: `${classPrefix}-table__ele--draggable-chosen`, + dragging: `${classPrefix}-table__ele--draggable-dragging`, + dragSortTh: `${classPrefix}-table__th--drag-sort`, + }, + + virtualScrollClasses: { + cursor: `${classPrefix}-table__virtual-scroll-cursor`, + header: `${classPrefix}-table__virtual-scroll-header`, + }, + + positiveRotate90: `${classPrefix}-positive-rotate-90`, + negativeRotate180: `${classPrefix}-negative-rotate-180`, + }), + [classPrefix], + ); return classNames; } diff --git a/src/table/hooks/useDragSort.ts b/src/table/hooks/useDragSort.ts index 789bd7afe4..42171d59ba 100644 --- a/src/table/hooks/useDragSort.ts +++ b/src/table/hooks/useDragSort.ts @@ -10,7 +10,7 @@ import useLatest from '../../_util/useLatest'; import log from '../../_common/js/log'; import swapDragArrayElement from '../../_common/js/utils/swapDragArrayElement'; import { BaseTableColumns } from '../interface'; -import { getColumnDataByKey, getColumnIndexByKey } from '../utils'; +import { getColumnDataByKey, getColumnIndexByKey } from '../../_common/js/table/utils'; export default function useDragSort( props: TdPrimaryTableProps, diff --git a/src/table/hooks/useEditableRow.ts b/src/table/hooks/useEditableRow.ts index db5ef07ae0..01687c4214 100644 --- a/src/table/hooks/useEditableRow.ts +++ b/src/table/hooks/useEditableRow.ts @@ -4,7 +4,7 @@ import isFunction from 'lodash/isFunction'; import { PrimaryTableProps } from '../interface'; import { validate } from '../../form/formModel'; import { AllValidateResult } from '../../form'; -import { getEditableKeysMap } from '../utils'; +import { getEditableKeysMap } from '../../_common/js/table/utils'; import { PrimaryTableRowEditContext, TableRowData, TableErrorListMap } from '../type'; export type ErrorListObjectType = PrimaryTableRowEditContext & { errorList: AllValidateResult[] }; @@ -14,9 +14,8 @@ export interface TablePromiseErrorData { errorMap: TableErrorListMap; } -const cellRuleMap = new Map[]>(); - export function useEditableRow(props: PrimaryTableProps) { + const cellRuleMap = useMemo(() => new Map[]>(), []); const { editableRowKeys } = props; // 校验不通过的错误信息,其中 key 值为 [rowValue, col.colKey].join('__') const [errorListMap, setErrorListMap] = useState({}); diff --git a/src/table/hooks/useFilter.tsx b/src/table/hooks/useFilter.tsx index b9ca1768ed..fad1cbc7e6 100644 --- a/src/table/hooks/useFilter.tsx +++ b/src/table/hooks/useFilter.tsx @@ -2,10 +2,11 @@ import React, { useEffect, useState, MutableRefObject } from 'react'; import isFunction from 'lodash/isFunction'; import useClassName from './useClassName'; import TButton from '../../button'; -import { TdPrimaryTableProps, PrimaryTableCol, TableRowData, FilterValue } from '../type'; +import { TdPrimaryTableProps, PrimaryTableCol, TableRowData, FilterValue, TableFilterChangeContext } from '../type'; import useControlled from '../../hooks/useControlled'; import TableFilterController from '../FilterController'; import { useLocaleReceiver } from '../../locale/LocalReceiver'; +import { getColumnsResetValue } from '../../_common/js/table/utils'; function isFilterValueExist(value: any) { const isArrayTrue = value instanceof Array && value.length; @@ -27,6 +28,7 @@ function filterEmptyData(data: FilterValue) { } export default function useFilter(props: TdPrimaryTableProps, primaryTableRef: MutableRefObject) { + const { columns } = props; const [locale, t] = useLocaleReceiver('table'); const { tableFilterClasses, isFocusClass } = useClassName(); const [isTableOverflowHidden, setIsTableOverflowHidden] = useState(); @@ -101,12 +103,16 @@ export default function useFilter(props: TdPrimaryTableProps, primaryTableRef: M }; setInnerFilterValue(filterValue); if (!column.filter.showConfirmAndReset) { - emitFilterChange(filterValue, column); + emitFilterChange(filterValue, 'filter-change', column); } } - function emitFilterChange(filterValue: FilterValue, column?: PrimaryTableCol) { - setTFilterValue(filterValue, { col: column }); + function emitFilterChange( + filterValue: FilterValue, + trigger: TableFilterChangeContext['trigger'], + column?: PrimaryTableCol, + ) { + setTFilterValue(filterValue, { col: column, trigger }); props.onChange?.({ filter: filterValue }, { trigger: 'filter' }); } @@ -122,15 +128,16 @@ export default function useFilter(props: TdPrimaryTableProps, primaryTableRef: M column.filter.resetValue || '', }; - emitFilterChange(filterValue, column); + emitFilterChange(filterValue, 'reset', column); } function onResetAll() { - emitFilterChange({}, undefined); + const resetValue: { [key: string]: any } = getColumnsResetValue(columns); + emitFilterChange(resetValue, 'clear', undefined); } function onConfirm(column: PrimaryTableCol) { - emitFilterChange(innerFilterValue, column); + emitFilterChange(innerFilterValue, 'confirm', column); } // 图标:内置图标,组件自定义图标,全局配置图标 diff --git a/src/table/hooks/useRowSelect.tsx b/src/table/hooks/useRowSelect.tsx index d03f305273..6e3a34ca2a 100644 --- a/src/table/hooks/useRowSelect.tsx +++ b/src/table/hooks/useRowSelect.tsx @@ -13,7 +13,7 @@ import { TdBaseTableProps, TdPrimaryTableProps, } from '../type'; -import { isRowSelectedDisabled } from '../utils'; +import { isRowSelectedDisabled } from '../../_common/js/table/utils'; import { TableClassName } from './useClassName'; import Checkbox from '../../checkbox'; import Radio from '../../radio'; diff --git a/src/table/hooks/useRowspanAndColspan.ts b/src/table/hooks/useRowspanAndColspan.ts index c4ff27dc39..d041772b12 100644 --- a/src/table/hooks/useRowspanAndColspan.ts +++ b/src/table/hooks/useRowspanAndColspan.ts @@ -23,10 +23,14 @@ export default function useRowspanAndColspan( rowKey: string, rowspanAndColspan: TableRowspanAndColspanFunc, ) { - const [skipSpansMap] = useState(new Map()); + const [skipSpansMap, setKipSnapsMap] = useState(new Map()); // 计算单元格是否跳过渲染 - const onTrRowspanOrColspan = (params: BaseTableCellParams, skipSpansValue: SkipSpansValue) => { + const onTrRowspanOrColspan = ( + params: BaseTableCellParams, + skipSpansValue: SkipSpansValue, + map: Map, + ) => { const { rowIndex, colIndex } = params; if (!skipSpansValue.rowspan && !skipSpansValue.colspan) return; const maxRowIndex = rowIndex + (skipSpansValue.rowspan || 1); @@ -36,22 +40,22 @@ export default function useRowspanAndColspan( if (i !== rowIndex || j !== colIndex) { if (!data[i] || !columns[j]) return; const cellKey = getCellKey(data[i], rowKey, columns[j].colKey, j); - const state = skipSpansMap.get(cellKey) || {}; + const state = map.get(cellKey) || {}; state.skipped = true; - skipSpansMap.set(cellKey, state); + map.set(cellKey, state); } } } }; // 计算单元格是否需要设置 rowspan 和 colspan - const updateSkipSpansMap = ( + const getSkipSpansMap = ( data: TableRowData[], columns: BaseTableCol[], rowspanAndColspan: TableRowspanAndColspanFunc, ) => { - skipSpansMap.clear(); if (!data || !rowspanAndColspan) return; + const map: Map = new Map(); for (let i = 0, len = data.length; i < len; i++) { const row = data[i]; for (let j = 0, colLen = columns.length; j < colLen; j++) { @@ -63,22 +67,26 @@ export default function useRowspanAndColspan( colIndex: j, }; const cellKey = getCellKey(row, rowKey, col.colKey, j); - const state = skipSpansMap.get(cellKey) || {}; + const state = map.get(cellKey) || {}; const o = rowspanAndColspan(params) || {}; if (o.rowspan || o.colspan || state.rowspan || state.colspan) { o.rowspan && (state.rowspan = o.rowspan); o.colspan && (state.colspan = o.colspan); - skipSpansMap.set(cellKey, state); + map.set(cellKey, state); } - onTrRowspanOrColspan?.(params, state); + onTrRowspanOrColspan?.(params, state, map); } } + return map; }; useEffect(() => { - updateSkipSpansMap(data, columns, rowspanAndColspan); + if (!rowspanAndColspan) return; + skipSpansMap.clear(); + const result = getSkipSpansMap(data, columns, rowspanAndColspan); + setKipSnapsMap(result); // eslint-disable-next-line react-hooks/exhaustive-deps }, [data, columns, rowspanAndColspan]); - return { skipSpansMap, updateSkipSpansMap }; + return { skipSpansMap, getSkipSpansMap }; } diff --git a/src/table/hooks/useTreeData.tsx b/src/table/hooks/useTreeData.tsx index 9e2da7292f..24b458156d 100644 --- a/src/table/hooks/useTreeData.tsx +++ b/src/table/hooks/useTreeData.tsx @@ -309,6 +309,7 @@ export default function useTreeData(props: TdEnhancedTableProps) { getTreeNode, resetData, getTreeExpandedRow, + onExpandFoldIconClick, }; } diff --git a/src/table/table.en-US.md b/src/table/table.en-US.md index 076edecc01..aa4799b372 100644 --- a/src/table/table.en-US.md +++ b/src/table/table.en-US.md @@ -142,7 +142,7 @@ onDataChange | Function | | Typescript:`(data: Array, context: TableDataCh onDisplayColumnsChange | Function | | Typescript:`(value: CheckboxGroupValue) => void`
[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/src/table/type.ts)。
`import { CheckboxGroupValue } from '@Checkbox'`
| N onDragSort | Function | | Typescript:`(context: DragSortContext) => void`
trigger on drag sort。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/src/table/type.ts)。
`interface DragSortContext { currentIndex: number; current: T; targetIndex: number; target: T; data: T[]; newData: T[]; currentData?: T[]; e: SortableEvent; sort: 'row' \| 'col' }`

`import { SortableEvent, SortableOptions } from 'sortablejs'`
| N onExpandChange | Function | | Typescript:`(expandedRowKeys: Array, options: ExpandOptions) => void`
trigger on expand row keys changing。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/src/table/type.ts)。
`interface ExpandOptions { expandedRowData: Array; currentRowData: T }`
| N -onFilterChange | Function | | Typescript:`(filterValue: FilterValue, context: { col?: PrimaryTableCol }) => void`
trigger on filter value changing | N +onFilterChange | Function | | Typescript:`(filterValue: FilterValue, context: TableFilterChangeContext) => void`
trigger on filter value changing。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/src/table/type.ts)。
`interface TableFilterChangeContext { col?: PrimaryTableCol; trigger: 'filter-change' \| 'confirm' \| 'reset' \| 'clear' }`
| N onRowEdit | Function | | Typescript:`(context: PrimaryTableRowEditContext) => void`
trigger on row data is editing。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/src/table/type.ts)。
`type PrimaryTableRowEditContext = PrimaryTableCellParams & { value: any; editedRow: T }`
| N onRowValidate | Function | | Typescript:`(context: PrimaryTableRowValidateContext) => void`
trigger after row data validated。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/src/table/type.ts)。
`type PrimaryTableRowValidateContext = { result: TableRowValidateResult[]; trigger: TableValidateTrigger }`

`type TableValidateTrigger = 'self' \| 'parent'`

`export type TableRowValidateResult = PrimaryTableCellParams & { errorList: AllValidateResult[]; value: any }`
| N onSelectChange | Function | | Typescript:`(selectedRowKeys: Array, options: SelectOptions) => void`
trigger on select changing。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/src/table/type.ts)。
`interface SelectOptions { selectedRowData: Array; type: 'uncheck' \| 'check'; currentRowKey?: string; currentRowData?: T }`
| N diff --git a/src/table/table.md b/src/table/table.md index e760c4f1bc..6ef1d1c87c 100644 --- a/src/table/table.md +++ b/src/table/table.md @@ -142,7 +142,7 @@ onDataChange | Function | | TS 类型:`(data: Array, context: TableDataCha onDisplayColumnsChange | Function | | TS 类型:`(value: CheckboxGroupValue) => void`
确认列配置时触发。[详细类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/table/type.ts)。
`import { CheckboxGroupValue } from '@Checkbox'`
| N onDragSort | Function | | TS 类型:`(context: DragSortContext) => void`
拖拽排序时触发,`data` 表示排序前的数据,`newData` 表示拖拽排序结束后的新数据,`sort=row` 表示行拖拽事件触发,`sort=col` 表示列拖拽事件触发。[详细类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/table/type.ts)。
`interface DragSortContext { currentIndex: number; current: T; targetIndex: number; target: T; data: T[]; newData: T[]; currentData?: T[]; e: SortableEvent; sort: 'row' \| 'col' }`

`import { SortableEvent, SortableOptions } from 'sortablejs'`
| N onExpandChange | Function | | TS 类型:`(expandedRowKeys: Array, options: ExpandOptions) => void`
展开行发生变化时触发,泛型 T 指表格数据类型。[详细类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/table/type.ts)。
`interface ExpandOptions { expandedRowData: Array; currentRowData: T }`
| N -onFilterChange | Function | | TS 类型:`(filterValue: FilterValue, context: { col?: PrimaryTableCol }) => void`
过滤参数发生变化时触发,泛型 T 指表格数据类型 | N +onFilterChange | Function | | TS 类型:`(filterValue: FilterValue, context: TableFilterChangeContext) => void`
过滤参数发生变化时触发,泛型 T 指表格数据类型。[详细类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/table/type.ts)。
`interface TableFilterChangeContext { col?: PrimaryTableCol; trigger: 'filter-change' \| 'confirm' \| 'reset' \| 'clear' }`
| N onRowEdit | Function | | TS 类型:`(context: PrimaryTableRowEditContext) => void`
行编辑时触发。[详细类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/table/type.ts)。
`type PrimaryTableRowEditContext = PrimaryTableCellParams & { value: any; editedRow: T }`
| N onRowValidate | Function | | TS 类型:`(context: PrimaryTableRowValidateContext) => void`
行编辑校验完成后触发,即组件实例方法 `validateRowData` 执行结束后触发。`result` 表示校验结果,`trigger=self` 表示编辑组件内部触发的校验,`trigger='parent'` 表示表格父组件触发的校验。[详细类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/table/type.ts)。
`type PrimaryTableRowValidateContext = { result: TableRowValidateResult[]; trigger: TableValidateTrigger }`

`type TableValidateTrigger = 'self' \| 'parent'`

`export type TableRowValidateResult = PrimaryTableCellParams & { errorList: AllValidateResult[]; value: any }`
| N onSelectChange | Function | | TS 类型:`(selectedRowKeys: Array, options: SelectOptions) => void`
选中行发生变化时触发,泛型 T 指表格数据类型。两个参数,第一个参数为选中行 keys,第二个参数为更多参数,具体如下:`type = uncheck` 表示当前行操作为「取消行选中」;`type = check` 表示当前行操作为「行选中」; `currentRowKey` 表示当前操作行的 rowKey 值; `currentRowData` 表示当前操作行的行数据。[详细类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/table/type.ts)。
`interface SelectOptions { selectedRowData: Array; type: 'uncheck' \| 'check'; currentRowKey?: string; currentRowData?: T }`
| N diff --git a/src/table/type.ts b/src/table/type.ts index 5a57c6e085..ad8fc49bae 100644 --- a/src/table/type.ts +++ b/src/table/type.ts @@ -558,7 +558,7 @@ export interface TdPrimaryTableProps /** * 过滤参数发生变化时触发,泛型 T 指表格数据类型 */ - onFilterChange?: (filterValue: FilterValue, context: { col?: PrimaryTableCol }) => void; + onFilterChange?: (filterValue: FilterValue, context: TableFilterChangeContext) => void; /** * 行编辑时触发 */ @@ -1096,6 +1096,11 @@ export interface ExpandOptions { currentRowData: T; } +export interface TableFilterChangeContext { + col?: PrimaryTableCol; + trigger: 'filter-change' | 'confirm' | 'reset' | 'clear'; +} + export type PrimaryTableRowEditContext = PrimaryTableCellParams & { value: any; editedRow: T }; export type PrimaryTableRowValidateContext = { result: TableRowValidateResult[]; trigger: TableValidateTrigger }; diff --git a/src/table/utils.ts b/src/table/utils.ts index 737c9fd492..76d9c8d967 100644 --- a/src/table/utils.ts +++ b/src/table/utils.ts @@ -4,7 +4,6 @@ import isObject from 'lodash/isObject'; import { BaseTableCellParams, CellData, - PrimaryTableCol, RowClassNameParams, TableColumnClassName, TableRowData, @@ -94,75 +93,8 @@ export function formatClassNames( return arr; } -export function filterDataByIds( - data: Array = [], - ids: Array = [], - byId = 'id', -): Array { - return data.filter((d: Record = {}) => ids.includes(d[byId])); -} - export const INNER_PRE_NAME = '@@inner-'; -export enum SCROLL_DIRECTION { - X = 'x', - Y = 'y', - UNKNOWN = 'unknown', -} - -let preScrollLeft: any; -let preScrollTop: any; - -export const getScrollDirection = (scrollLeft: number, scrollTop: number): SCROLL_DIRECTION => { - let direction = SCROLL_DIRECTION.UNKNOWN; - if (preScrollTop !== scrollTop) { - direction = SCROLL_DIRECTION.Y; - } else if (preScrollLeft !== scrollLeft) { - direction = SCROLL_DIRECTION.X; - } - preScrollTop = scrollTop; - preScrollLeft = scrollLeft; - return direction; -}; - -export const getRecord = (record: Record) => { - if (!record) { - return record; - } - const result = {}; - Object.keys(record).forEach((key) => { - const descriptor = Object.getOwnPropertyDescriptor(record, key); - descriptor && - Reflect.defineProperty(result, key, { - set(val) { - descriptor.set(val); - }, - get() { - console.warn('The parameter `record` will be deprecated, please use `row` instead'); - return descriptor.get(); - }, - }); - }); - return result; -}; - -export function isRowSelectedDisabled( - selectColumn: PrimaryTableCol, - row: Record, - rowIndex: number, -): boolean { - if (!selectColumn) return false; - let disabled = isFunction(selectColumn.disabled) ? selectColumn.disabled({ row, rowIndex }) : selectColumn.disabled; - if (selectColumn.checkProps) { - if (isFunction(selectColumn.checkProps)) { - disabled = disabled || selectColumn.checkProps({ row, rowIndex }).disabled; - } else if (selectColumn.checkProps === 'object') { - disabled = disabled || selectColumn.checkProps.disabled; - } - } - return !!disabled; -} - // 多级表头,列配置场景,获取 currentRow export function getCurrentRowByKey(columns: T[], key: string): T { if (!columns || !key) return; @@ -181,38 +113,3 @@ export function getAffixProps(mainAffixProps: boolean | Partial, sub if (typeof subAffixProps === 'object') return subAffixProps; return {}; } - -export function getEditableKeysMap(keys: Array, list: any[], rowKey: string) { - const map: { [key: string | number]: boolean } = {}; - for (let i = 0, len = list.length; i < len; i++) { - const rowValue = get(list[i], rowKey); - if (keys.includes(rowValue)) { - map[rowValue] = true; - } - } - return map; -} - -export function getColumnDataByKey(columns: any[], colKey: string): any { - for (let i = 0, len = columns.length; i < len; i++) { - if (columns[i].colKey === colKey) return columns[i]; - if (columns[i].children?.length) { - const t = getColumnDataByKey(columns[i].children, colKey); - if (t) return t; - } - } - return null; -} - -export function getColumnIndexByKey(columns: any[], colKey: string): number { - for (let i = 0, len = columns.length; i < len; i++) { - if (columns[i].colKey === colKey) { - return i; - } - if (columns[i].children?.length) { - const t = getColumnDataByKey(columns[i].children, colKey); - if (t) return i; - } - } - return -1; -} diff --git a/src/tabs/TabNav.tsx b/src/tabs/TabNav.tsx index 923e03745a..87e91c53cd 100644 --- a/src/tabs/TabNav.tsx +++ b/src/tabs/TabNav.tsx @@ -210,6 +210,7 @@ const TabNav: React.FC = (props) => { const handleTabItemClick = (clickItem) => { tabClick(clickItem.value); onChange(clickItem.value); + clickItem?.onClick?.(clickItem.value); }; const handleTabAdd = (e) => { diff --git a/src/tabs/Tabs.tsx b/src/tabs/Tabs.tsx index 46fff8daf4..507c834be0 100644 --- a/src/tabs/Tabs.tsx +++ b/src/tabs/Tabs.tsx @@ -93,7 +93,7 @@ const Tabs = forwardRefWithStatics( return child; } if (child.props.destroyOnHide === false) { - return {child.props.children}; + return ; } } return null; diff --git a/src/tag-input/useTagList.tsx b/src/tag-input/useTagList.tsx index d06873e0ab..37fddb031e 100644 --- a/src/tag-input/useTagList.tsx +++ b/src/tag-input/useTagList.tsx @@ -104,7 +104,7 @@ export default function useTagList(props: TagInputProps) { collapsedTags: tagValue.slice(minCollapsedNum, tagValue.length), }; const more = isFunction(collapsedItems) ? collapsedItems(params) : collapsedItems; - list.push({more ?? +{len}}); + list.push({more ?? +{len}}); } return list; }; diff --git a/src/tree/Tree.tsx b/src/tree/Tree.tsx index 83616b3fd3..b9283dceb0 100644 --- a/src/tree/Tree.tsx +++ b/src/tree/Tree.tsx @@ -1,11 +1,22 @@ -import React, { forwardRef, useState, useImperativeHandle, useMemo, RefObject, MouseEvent, useRef } from 'react'; +import React, { + forwardRef, + useState, + useImperativeHandle, + useMemo, + RefObject, + MouseEvent, + useRef, + useCallback, +} from 'react'; import { CSSTransition, TransitionGroup } from 'react-transition-group'; import classNames from 'classnames'; +import get from 'lodash/get'; import TreeNode from '../_common/js/tree/tree-node'; -import { TreeOptionData, StyledProps } from '../common'; +import { TreeOptionData, StyledProps, ComponentScrollToElementParams } from '../common'; import { TreeItemProps } from './interface'; import TreeItem from './TreeItem'; +import log from '../_common/js/log'; import useControllable from './hooks/useControllable'; import { useStore } from './hooks/useStore'; @@ -144,13 +155,31 @@ const Tree = forwardRef((props: TreeProps, ref: React.Ref } setChecked(node, !node.isChecked(), { ...ctx, trigger: 'node-click' }); }; + const handleScrollToElement = useCallback( + (params: ComponentScrollToElementParams) => { + let { index } = params; + if (!index && index !== 0) { + if (!params.key) { + log.error('Tree', 'scrollToElement: one of `index` or `key` must exist.'); + return; + } + const data = isVirtual ? visibleData : visibleNodes; + index = data?.findIndex((item) => [get(item.data, 'key'), get(item.data, 'value')].includes(params.key)); + if (index < 0) { + log.error('Tree', `${params.key} does not exist in data, check \`key\` or \`data\` please.`); + } + } + scrollToElement({ ...params, index }); + }, + [scrollToElement, isVirtual, visibleData, visibleNodes], + ); /** 对外暴露的公共方法 * */ useImperativeHandle( ref, () => ({ store, - scrollTo: scrollToElement, + scrollTo: (p: ComponentScrollToElementParams) => handleScrollToElement(p), appendTo(value, newData) { let list = []; if (Array.isArray(newData)) { @@ -218,7 +247,7 @@ const Tree = forwardRef((props: TreeProps, ref: React.Ref } }, }), - [store, setExpanded, setActived, setChecked, scrollToElement], + [store, setExpanded, setActived, setChecked, handleScrollToElement], ); /* ======== render ======= */ @@ -313,7 +342,7 @@ const Tree = forwardRef((props: TreeProps, ref: React.Ref [treeClassNames.treeCheckable]: checkable, [treeClassNames.treeFx]: transition, [treeClassNames.treeBlockNode]: expandOnClickNode, - [treeClassNames.treeVscroll]: isVirtual, + [treeClassNames.treeVscroll]: props.scroll, // 开启虚拟滚动就要有overflow 否则低于 threshold 无法正常运行 scrollto })} style={style} ref={treeRef} diff --git a/src/tree/_example/operations.jsx b/src/tree/_example/operations.jsx index 2dcd63d56f..3eec0c910a 100644 --- a/src/tree/_example/operations.jsx +++ b/src/tree/_example/operations.jsx @@ -121,6 +121,15 @@ export default () => { const renderOperations2 = (node) => ( <> + + + diff --git a/src/tree/_example/vscroll.jsx b/src/tree/_example/vscroll.jsx index 7faf3be155..55100cfb95 100644 --- a/src/tree/_example/vscroll.jsx +++ b/src/tree/_example/vscroll.jsx @@ -9,17 +9,18 @@ export default () => { useEffect(() => { const newOptions = []; - for (let i = 0; i < 3000; i++) { + for (let i = 1; i <= 3000; i++) { newOptions.push({ - label: `第${i + 1}段`, + label: `第${i}段`, value: i, + key: i, children: [ { - label: `第${i + 1}段第1个子节点`, + label: `第${i}段第1个子节点`, value: `${i}.1`, }, { - label: `第${i + 1}段第2个子节点`, + label: `第${i}段第2个子节点`, value: `${i}.2`, }, ], @@ -29,7 +30,7 @@ export default () => { }, []); const handleScroll = () => { - treeRef.current.scrollTo({ index: 100, behavior: 'smooth' }); + treeRef.current.scrollTo({ key: '3.2', behavior: 'smooth' }); }; const defaultChecked = ['1.2', '2.2']; diff --git a/src/tree/hooks/useTreeVirtualScroll.ts b/src/tree/hooks/useTreeVirtualScroll.ts index 0f340747e7..0e133e8eac 100644 --- a/src/tree/hooks/useTreeVirtualScroll.ts +++ b/src/tree/hooks/useTreeVirtualScroll.ts @@ -1,8 +1,7 @@ import { useMemo, useEffect, CSSProperties } from 'react'; import useVirtualScroll from '../../hooks/useVirtualScroll'; import TreeNode from '../../_common/js/tree/tree-node'; - -import type { TScroll } from '../../common'; +import { TScroll } from '../../common'; export default function useTreeVirtualScroll({ treeRef, diff --git a/src/tree/tree.en-US.md b/src/tree/tree.en-US.md index c9f246e116..3311101401 100644 --- a/src/tree/tree.en-US.md +++ b/src/tree/tree.en-US.md @@ -67,7 +67,7 @@ getPath | `(value: TreeNodeValue)` | `TreeNodeModel[]` | required insertAfter | `(value: TreeNodeValue, newData: T)` | \- | required insertBefore | `(value: TreeNodeValue, newData: T)` | \- | required remove | `(value: TreeNodeValue)` | \- | required -scrollTo | `(scrollToParams: ScrollToElementParams)` | \- | support scrolling to a specific node when virtual scrolling +scrollTo | `(scrollToParams: ComponentScrollToElementParams)` | \- | support scrolling to a specific node when virtual scrolling setItem | `(value: TreeNodeValue, options: TreeNodeState)` | \- | required ### TreeNodeState diff --git a/src/tree/tree.md b/src/tree/tree.md index 870f31aa5f..7050267746 100644 --- a/src/tree/tree.md +++ b/src/tree/tree.md @@ -67,7 +67,7 @@ getPath | `(value: TreeNodeValue)` | `TreeNodeModel[]` | 必需。自下而 insertAfter | `(value: TreeNodeValue, newData: T)` | \- | 必需。插入新节点到指定节点后面,泛型 `T` 表示树节点 TS 类型 insertBefore | `(value: TreeNodeValue, newData: T)` | \- | 必需。插入新节点到指定节点前面,泛型 `T` 表示树节点 TS 类型 remove | `(value: TreeNodeValue)` | \- | 必需。移除指定节点 -scrollTo | `(scrollToParams: ScrollToElementParams)` | \- | 虚拟滚动场景下 支持指定滚动到具体的节点 +scrollTo | `(scrollToParams: ComponentScrollToElementParams)` | \- | 虚拟滚动场景下 支持指定滚动到具体的节点 setItem | `(value: TreeNodeValue, options: TreeNodeState)` | \- | 必需。设置节点状态 ### TreeNodeState diff --git a/src/tree/type.ts b/src/tree/type.ts index 4813df3114..d5bc2456a5 100644 --- a/src/tree/type.ts +++ b/src/tree/type.ts @@ -5,7 +5,7 @@ * */ import { CheckboxProps } from '../checkbox'; -import { TNode, TreeOptionData, TScroll, ScrollToElementParams } from '../common'; +import { TNode, TreeOptionData, TScroll, ComponentScrollToElementParams } from '../common'; import { MouseEvent, WheelEvent, DragEvent } from 'react'; export interface TdTreeProps { @@ -277,7 +277,7 @@ export interface TreeInstanceFunctions void; + scrollTo?: (scrollToParams: ComponentScrollToElementParams) => void; /** * 设置节点状态 */ diff --git a/src/upload/__tests__/__snapshots__/vitest-upload.test.jsx.snap b/src/upload/__tests__/__snapshots__/vitest-upload.test.jsx.snap index eb03a01170..7c06c68c4c 100644 --- a/src/upload/__tests__/__snapshots__/vitest-upload.test.jsx.snap +++ b/src/upload/__tests__/__snapshots__/vitest-upload.test.jsx.snap @@ -18,9 +18,45 @@ exports[`Upload Component > props.draggable: theme=image & draggable=true, fail
- +
+ +
+
+
+ + + +
+
+ 图片加载中 +
+
+
+
props.draggable: theme=image & draggable=true, progr
- +
+ +
+
+
+ + + +
+
+ 图片加载中 +
+
+
+
props.draggable: theme=image & draggable=true, succe
- +
+ +
+
+
+ + + +
+
+ 图片加载中 +
+
+
+
props.draggable: theme=image & draggable=true, succe
- +
+ +
+
+
+ + + +
+
+ 图片加载中 +
+
+
+
props.draggable: theme=image & draggable=true, waiti
- +
+ +
+
+
+ + + +
+
+ 图片加载中 +
+
+
+
props.theme: theme=image-flow works fine 1`] = `
- +
+ +
+
+
+ + + +
+
+
+
+
@@ -732,10 +945,43 @@ exports[`Upload Component > props.theme: theme=image-flow works fine 1`] = `
- +
+ +
+
+
+ + + +
+
+
+
+
@@ -792,10 +1038,43 @@ exports[`Upload Component > props.theme: theme=image-flow works fine 1`] = `
- +
+ +
+
+
+ + + +
+
+
+
+
@@ -843,7 +1122,7 @@ exports[`Upload Component > props.theme: theme=image-flow works fine 1`] = `

- img2.txt + 待上传

  • - + 禁用状态 自动上传 + 显示文件缩略图 允许上传同名文件 @@ -50,8 +55,10 @@ export default function FileFlowList() { theme="file-flow" multiple max={10} + abridge-name={ABRIDGE_NAME} disabled={disabled} autoUpload={autoUpload} + showThumbnail={showThumbnail} uploadAllFilesInOneRequest={uploadInOneRequest} isBatchUpload={isBatchUpload} allowUploadDuplicateFile={allowUploadDuplicateFile} diff --git a/src/upload/_example/img-flow-list.jsx b/src/upload/_example/img-flow-list.jsx index 011c7e700f..89c92edfe6 100644 --- a/src/upload/_example/img-flow-list.jsx +++ b/src/upload/_example/img-flow-list.jsx @@ -1,9 +1,16 @@ import React, { useState } from 'react'; import { Upload, Space, MessagePlugin, Switch } from 'tdesign-react'; +const ABRIDGE_NAME = [4, 6]; + export default function TUploadImageFlow() { const [autoUpload, setAutoUpload] = useState(false); - const [files3, setFiles3] = useState([]); + const [files, setFiles] = useState([ + { url: 'https://tdesign.gtimg.com/demo/demo-image-1.png', status: 'success', name: 'demo-image-1.png' }, + { url: 'https://tdesign.gtimg.com/site/avatar.jpg', status: 'success', name: 'avatar.jpg' }, + ]); + // eslint-disable-next-line + const [files2, setFiles2] = useState([]); // 示例代码:自定义上传方法,一个请求上传一个文件 // eslint-disable-next-line @@ -21,15 +28,14 @@ export default function TUploadImageFlow() { // 示例代码:自定义上传方法,一个请求上传多个文件 // eslint-disable-next-line const requestMethod2 = () => { + const files = [ + { name: files2[0].name, status: 'success', url: 'https://tdesign.gtimg.com/site/avatar.jpg' }, + { name: files2[1].name, status: 'success', url: 'https://avatars.githubusercontent.com/u/11605702?v=4' }, + ]; return new Promise((resolve) => { resolve({ status: 'success', - response: { - files: [ - { name: 'avatar1.jpg', url: 'https://tdesign.gtimg.com/site/avatar.jpg' }, - { name: 'avatar2.jpg', url: 'https://avatars.githubusercontent.com/u/11605702?v=4' }, - ], - }, + response: { files }, }); }); }; @@ -59,18 +65,38 @@ export default function TUploadImageFlow() {
    + {/* */} + {/* */} + + {/* */}
    ); } diff --git a/src/upload/defaultProps.ts b/src/upload/defaultProps.ts index 05d2535ca7..3874caf673 100644 --- a/src/upload/defaultProps.ts +++ b/src/upload/defaultProps.ts @@ -14,6 +14,7 @@ export const uploadDefaultProps: TdUploadProps = { method: 'POST', multiple: false, name: 'file', + showThumbnail: false, showUploadProgress: true, theme: 'file', uploadAllFilesInOneRequest: false, diff --git a/src/upload/hooks/useUpload.ts b/src/upload/hooks/useUpload.ts index bfa8c25926..225e501ec0 100644 --- a/src/upload/hooks/useUpload.ts +++ b/src/upload/hooks/useUpload.ts @@ -9,7 +9,7 @@ import { getDisplayFiles, formatToUploadFile, } from '../../_common/js/upload/main'; -import { getFileUrlByFileRaw, getFileList } from '../../_common/js/upload/utils'; +import { getFileList } from '../../_common/js/upload/utils'; import useControlled from '../../hooks/useControlled'; import { InnerProgressContext, OnResponseErrorContext, SuccessContext } from '../../_common/js/upload/types'; import useConfig from '../../hooks/useConfig'; @@ -135,33 +135,12 @@ export default function useUpload(props: TdUploadProps) { const handleNotAutoUpload = (toFiles: UploadFile[]) => { const tmpFiles = props.multiple && !isBatchUpload ? uploadValue.concat(toFiles) : toFiles; if (!tmpFiles.length) return; - // 图片需要本地预览 - if (['image', 'image-flow'].includes(props.theme)) { - const list = tmpFiles.map( - (file) => - new Promise((resolve) => { - getFileUrlByFileRaw(file.raw).then((url) => { - resolve({ ...file, url: file.url || url }); - }); - }), - ); - Promise.all(list).then((files) => { - setUploadValue(files, { - trigger: 'add', - index: uploadValue.length, - file: toFiles[0], - files: toFiles, - }); - }); - } else { - setUploadValue(tmpFiles, { - trigger: 'add', - index: uploadValue.length, - file: toFiles[0], - files: toFiles, - }); - } - // toUploadFiles.current = []; + setUploadValue(tmpFiles, { + trigger: 'add', + index: uploadValue.length, + file: toFiles[0], + files: toFiles, + }); setToUploadFiles([]); }; diff --git a/src/upload/themes/DraggerFile.tsx b/src/upload/themes/DraggerFile.tsx index feb13614c5..bc2f8fd716 100644 --- a/src/upload/themes/DraggerFile.tsx +++ b/src/upload/themes/DraggerFile.tsx @@ -14,6 +14,7 @@ import useDrag, { UploadDragEvents } from '../hooks/useDrag'; import useGlobalIcon from '../../hooks/useGlobalIcon'; import ImageViewer from '../../image-viewer'; import { parseContentTNode } from '../../_util/parseTNode'; +import Image from '../../image'; export interface DraggerProps extends CommonDisplayFileProps { trigger?: TdUploadProps['trigger']; @@ -53,7 +54,10 @@ const DraggerFile: FC = (props) => { const url = file?.url || file?.response?.url; return (
    - {url && }>} + } + >
    ); }; diff --git a/src/upload/themes/ImageCard.tsx b/src/upload/themes/ImageCard.tsx index f27e70cf51..c31c6c7dbb 100644 --- a/src/upload/themes/ImageCard.tsx +++ b/src/upload/themes/ImageCard.tsx @@ -14,6 +14,7 @@ import { TdUploadProps, UploadFile } from '../type'; import { abridgeName } from '../../_common/js/upload/utils'; import parseTNode from '../../_util/parseTNode'; import Link from '../../link'; +import Image from '../../image'; export interface ImageCardUploadProps extends CommonDisplayFileProps { multiple: TdUploadProps['multiple']; @@ -44,19 +45,19 @@ const ImageCard = (props: ImageCardUploadProps) => { const renderMainContent = (file: UploadFile, index: number) => (
    - +
    e.stopPropagation()}> ( + trigger={({ open }) => ( { props.onPreview?.({ file, index, e }); - onOpen(); + open(); }} /> )} - images={displayFiles.map((t) => t.url)} + images={displayFiles.map((t) => t.url || t.raw)} defaultIndex={index} /> @@ -117,7 +118,7 @@ const ImageCard = (props: ImageCardUploadProps) => {
  • {file.status === 'progress' && renderProgressFile(file, loadCard)} {file.status === 'fail' && renderFailFile(file, index, loadCard)} - {!['progress', 'fail'].includes(file.status) && file.url && renderMainContent(file, index)} + {!['progress', 'fail'].includes(file.status) && renderMainContent(file, index)} {fileName && (file.url ? ( diff --git a/src/upload/themes/MultipleFlowList.tsx b/src/upload/themes/MultipleFlowList.tsx index daff406571..f7784258e8 100644 --- a/src/upload/themes/MultipleFlowList.tsx +++ b/src/upload/themes/MultipleFlowList.tsx @@ -1,4 +1,4 @@ -import React, { MouseEvent, useMemo } from 'react'; +import React, { MouseEvent, useMemo, useState } from 'react'; import classNames from 'classnames'; import { BrowseIcon as TdBrowseIcon, @@ -6,6 +6,12 @@ import { CheckCircleFilledIcon as TdCheckCircleFilledIcon, ErrorCircleFilledIcon as TdErrorCircleFilledIcon, TimeFilledIcon as TdTimeFilledIcon, + FileExcelIcon as TdFileExcelIcon, + FilePdfIcon as TdFilePdfIcon, + FileWordIcon as TdFileWordIcon, + FilePowerpointIcon as TdFilePowerpointIcon, + FileIcon as TdFileIcon, + VideoIcon as TdVideoIcon, } from 'tdesign-icons-react'; import useGlobalIcon from '../../hooks/useGlobalIcon'; import ImageViewer from '../../image-viewer'; @@ -13,10 +19,20 @@ import { CommonDisplayFileProps } from '../interface'; import TButton from '../../button'; import { UploadFile, TdUploadProps } from '../type'; import useDrag, { UploadDragEvents } from '../hooks/useDrag'; -import { abridgeName, returnFileSize } from '../../_common/js/upload/utils'; +import { + abridgeName, + returnFileSize, + IMAGE_REGEXP, + FILE_PDF_REGEXP, + FILE_EXCEL_REGEXP, + FILE_WORD_REGEXP, + FILE_PPT_REGEXP, + VIDEO_REGEXP, +} from '../../_common/js/upload/utils'; import TLoading from '../../loading'; import Link from '../../link'; import parseTNode from '../../_util/parseTNode'; +import Image from '../../image'; export interface ImageFlowListProps extends CommonDisplayFileProps { uploadFiles?: (toFiles?: UploadFile[]) => void; @@ -25,21 +41,43 @@ export interface ImageFlowListProps extends CommonDisplayFileProps { disabled?: boolean; isBatchUpload?: boolean; draggable?: boolean; + showThumbnail?: boolean; onPreview?: TdUploadProps['onPreview']; } const ImageFlowList = (props: ImageFlowListProps) => { - const { draggable = true, accept } = props; + const { draggable = true, accept, showThumbnail, onPreview } = props; // locale 已经在 useUpload 中统一处理优先级 const { locale, uploading, disabled, displayFiles, classPrefix } = props; const uploadPrefix = `${classPrefix}-upload`; - const { BrowseIcon, DeleteIcon, CheckCircleFilledIcon, ErrorCircleFilledIcon, TimeFilledIcon } = useGlobalIcon({ + const [currentPreviewFile, setCurrentPreviewFile] = useState([]); + const [previewIndex, setPreviewIndex] = useState(0); + + const { + BrowseIcon, + DeleteIcon, + CheckCircleFilledIcon, + ErrorCircleFilledIcon, + TimeFilledIcon, + FileExcelIcon, + FilePdfIcon, + FileWordIcon, + FilePowerpointIcon, + FileIcon, + VideoIcon, + } = useGlobalIcon({ BrowseIcon: TdBrowseIcon, DeleteIcon: TdDeleteIcon, CheckCircleFilledIcon: TdCheckCircleFilledIcon, ErrorCircleFilledIcon: TdErrorCircleFilledIcon, TimeFilledIcon: TdTimeFilledIcon, + FileExcelIcon: TdFileExcelIcon, + FilePdfIcon: TdFilePdfIcon, + FileWordIcon: TdFileWordIcon, + FilePowerpointIcon: TdFilePowerpointIcon, + FileIcon: TdFileIcon, + VideoIcon: TdVideoIcon, }); const drag = useDrag({ ...props.dragEvents, accept }); @@ -58,6 +96,30 @@ const ImageFlowList = (props: ImageFlowListProps) => { } : {}; + const browseIconClick = ({ + e, + index, + file, + viewFiles, + }: { + e: MouseEvent; + index: number; + file: UploadFile; + viewFiles: UploadFile[]; + }) => { + setPreviewIndex(index); + setCurrentPreviewFile(viewFiles); + onPreview?.({ file, index, e }); + }; + + const previewIndexChange = (index: number) => { + setPreviewIndex(index); + }; + + const closePreview = () => { + setCurrentPreviewFile([]); + }; + const getStatusMap = () => { const iconMap = { success: , @@ -106,25 +168,21 @@ const ImageFlowList = (props: ImageFlowListProps) => {
  • )} {(['waiting', 'success'].includes(file.status) || (!file.status && file.url)) && ( - + )}
    - {file.url && ( + {(file.url || file.raw) && ( - ( - { - props.onPreview?.({ file, index, e }); - onOpen(); - }} - /> - )} - images={displayFiles.map((t) => t.url)} - defaultIndex={index} + { + const e = event.type ? event : event.e; + browseIconClick({ + e, + index, + file, + viewFiles: displayFiles, + }); + }} /> @@ -139,7 +197,9 @@ const ImageFlowList = (props: ImageFlowListProps) => { )}
    -

    {fileName}

    +

    + {file.status === 'waiting' ? locale.progress.waitingText : fileName} +

    ); }; @@ -170,6 +230,52 @@ const ImageFlowList = (props: ImageFlowListProps) => { ); + function getFileThumbnailIcon(fileType: string) { + if (FILE_PDF_REGEXP.test(fileType)) { + return ; + } + if (FILE_EXCEL_REGEXP.test(fileType)) { + return ; + } + if (FILE_WORD_REGEXP.test(fileType)) { + return ; + } + if (FILE_PPT_REGEXP.test(fileType)) { + return ; + } + if (VIDEO_REGEXP.test(fileType)) { + return ; + } + return ; + } + + function renderFileThumbnail(file: UploadFile) { + if (!file || (!file.raw && file.url)) return null; + const fileType = file.raw.type; + const className = `${uploadPrefix}__file-thumbnail`; + if (IMAGE_REGEXP.test(fileType) && (file.url || file.raw)) { + return ( + { + e.preventDefault(); + browseIconClick({ + e, + index: 0, + file, + viewFiles: [file], + }); + }} + /> + ); + } + return
    {getFileThumbnailIcon(fileType)}
    ; + } + // batchUpload action col const renderBatchActionCol = (index: number) => // 第一行数据才需要合并单元格 @@ -217,17 +323,24 @@ const ImageFlowList = (props: ImageFlowListProps) => { ? renderBatchActionCol(index) : renderNormalActionCol(file, index); const fileName = props.abridgeName?.length ? abridgeName(file.name, ...props.abridgeName) : file.name; + const thumbnailNode = showThumbnail ? ( +
    + {renderFileThumbnail(file)} + {fileName} +
    + ) : ( + fileName + ); + const fileNameNode = file.url ? ( + + {thumbnailNode} + + ) : ( + thumbnailNode + ); return ( - - {file.url ? ( - - {fileName} - - ) : ( - fileName - )} - + {fileNameNode} {returnFileSize(file.size)} {renderStatus(file)} {disabled ? null : deleteNode} @@ -302,6 +415,14 @@ const ImageFlowList = (props: ImageFlowListProps) => {
    )} + + t.url || t.raw)} + visible={!!currentPreviewFile.length} + onClose={closePreview} + index={previewIndex} + onIndexChange={previewIndexChange} + >
    ); }; diff --git a/src/upload/type.ts b/src/upload/type.ts index b8369ac736..3d6eb2d459 100644 --- a/src/upload/type.ts +++ b/src/upload/type.ts @@ -81,7 +81,7 @@ export interface TdUploadProps { */ format?: (file: File) => UploadFile; /** - * 用于新增或修改文件上传请求参数。`action` 存在时有效。一个请求上传一个文件时,默认请求字段有 `file`;
    一个请求上传多个文件时,默认字段有 `file[0]/file[1]/file[2]/.../length`,其中 `length` 表示本次上传的文件数量。
    ⚠️非常注意,此处的 `file[0]/file[1]` 仅仅是一个字段名,并非表示 `file` 是一个数组,接口获取字段时注意区分。
    可以使用 `name` 定义 `file` 字段的别名,也可以使用 `formatRequest` 自定义任意字段 + * 用于新增或修改文件上传请求 参数。`action` 存在时有效。一个请求上传一个文件时,默认请求字段有 `file`。
    一个请求上传多个文件时,默认字段有 `file[0]/file[1]/file[2]/.../length`,其中 `length` 表示本次上传的文件数量。
    ⚠️非常注意,此处的 `file[0]/file[1]` 仅仅是一个字段名,并非表示 `file` 是一个数组,接口获取字段时注意区分。
    可以使用 `name` 定义 `file` 字段的别名。
    也可以使用 `formatRequest` 自定义任意字段,如添加一个字段 `fileList` ,存储文件数组 */ formatRequest?: (requestData: { [key: string]: any }) => { [key: string]: any }; /** @@ -138,6 +138,11 @@ export interface TdUploadProps { * 自定义上传方法。返回值 `status` 表示上传成功或失败;`error` 或 `response.error` 表示上传失败的原因;
    `response` 表示请求上传成功后的返回数据,`response.url` 表示上传成功后的图片/文件地址,`response.files` 表示一个请求上传多个文件/图片后的返回值。
    示例一:`{ status: 'fail', error: '上传失败', response }`。
    示例二:`{ status: 'success', response: { url: 'https://tdesign.gtimg.com/site/avatar.jpg' } }`。
    示例三:`{ status: 'success', files: [{ url: 'https://xxx.png', name: 'xxx.png' }]}` */ requestMethod?: (files: UploadFile | UploadFile[]) => Promise; + /** + * 是否在文件列表中显示缩略图,`theme=file-flow` 时有效 + * @default false + */ + showThumbnail?: boolean; /** * 是否显示上传进度 * @default true diff --git a/src/upload/upload.en-US.md b/src/upload/upload.en-US.md index fcfed8cf87..3ed13c1e18 100644 --- a/src/upload/upload.en-US.md +++ b/src/upload/upload.en-US.md @@ -36,6 +36,7 @@ multiple | Boolean | false | multiple files uploading | N name | String | file | field name of files in upload request data | N placeholder | String | - | placeholder | N requestMethod | Function | - | custom upload request method。Typescript:`(files: UploadFile \| UploadFile[]) => Promise` `interface RequestMethodResponse { status: 'success' \| 'fail'; error?: string; response: { url?: string; files?: UploadFile[]; [key: string]: any } }`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/src/upload/type.ts) | N +showThumbnail | Boolean | false | show thumbnail before file name, only works on `theme=file-flow` | N showUploadProgress | Boolean | true | show upload progress nodes | N sizeLimit | Number / Object | - | files size limit。Typescript:`number \| SizeLimitObj` `interface SizeLimitObj { size: number; unit: SizeUnit ; message?: string }` `type SizeUnitArray = ['B', 'KB', 'MB', 'GB']` `type SizeUnit = SizeUnitArray[number]`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/src/upload/type.ts) | N status | String | - | tips status。options: default/success/warning/error | N diff --git a/src/upload/upload.md b/src/upload/upload.md index 5520a7030a..e82ead212f 100644 --- a/src/upload/upload.md +++ b/src/upload/upload.md @@ -23,7 +23,7 @@ fileListDisplay | TElement | - | 用于完全自定义文件列表界面内容(U files | Array | [] | 已上传文件列表,同 `value`。TS 类型:`UploadFile`。TS 类型:`Array` | N defaultFiles | Array | [] | 已上传文件列表,同 `value`。TS 类型:`UploadFile`。非受控属性。TS 类型:`Array` | N format | Function | - | 转换文件 `UploadFile` 的数据结构,可新增或修改 `UploadFile` 的属性,注意不能删除 `UploadFile` 属性。`action` 存在时有效。TS 类型:`(file: File) => UploadFile` | N -formatRequest | Function | - | 用于新增或修改文件上传请求参数。`action` 存在时有效。一个请求上传一个文件时,默认请求字段有 `file`;
    一个请求上传多个文件时,默认字段有 `file[0]/file[1]/file[2]/.../length`,其中 `length` 表示本次上传的文件数量。
    ⚠️非常注意,此处的 `file[0]/file[1]` 仅仅是一个字段名,并非表示 `file` 是一个数组,接口获取字段时注意区分。
    可以使用 `name` 定义 `file` 字段的别名,也可以使用 `formatRequest` 自定义任意字段。TS 类型:`(requestData: { [key: string]: any }) => { [key: string]: any }` | N +formatRequest | Function | - | 用于新增或修改文件上传请求 参数。`action` 存在时有效。一个请求上传一个文件时,默认请求字段有 `file`。
    一个请求上传多个文件时,默认字段有 `file[0]/file[1]/file[2]/.../length`,其中 `length` 表示本次上传的文件数量。
    ⚠️非常注意,此处的 `file[0]/file[1]` 仅仅是一个字段名,并非表示 `file` 是一个数组,接口获取字段时注意区分。
    可以使用 `name` 定义 `file` 字段的别名。
    也可以使用 `formatRequest` 自定义任意字段,如添加一个字段 `fileList` ,存储文件数组。TS 类型:`(requestData: { [key: string]: any }) => { [key: string]: any }` | N formatResponse | Function | - | 用于格式化文件上传后的接口响应数据,`response` 便是接口响应的原始数据。`action` 存在时有效。
    此函数的返回值 `error` 或 `response.error` 会作为错误文本提醒,如果存在会判定为本次上传失败。
    此函数的返回值 `url` 或 `response.url` 会作为上传成功后的链接。TS 类型:`(response: any, context: FormatResponseContext) => ResponseType ` `type ResponseType = { error?: string; url?: string } & Record` `interface FormatResponseContext { file: UploadFile; currentFiles?: UploadFile[] }`。[详细类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/upload/type.ts) | N headers | Object | - | 设置上传的请求头部,`action` 存在时有效。TS 类型:`{[key: string]: string}` | N inputAttributes | Object | - | 用于添加属性到 HTML 元素 `input`。TS 类型:`CSSProperties` | N @@ -36,6 +36,7 @@ multiple | Boolean | false | 支持多文件上传 | N name | String | file | 文件上传时的名称 | N placeholder | String | - | 占位符 | N requestMethod | Function | - | 自定义上传方法。返回值 `status` 表示上传成功或失败;`error` 或 `response.error` 表示上传失败的原因;
    `response` 表示请求上传成功后的返回数据,`response.url` 表示上传成功后的图片/文件地址,`response.files` 表示一个请求上传多个文件/图片后的返回值。
    示例一:`{ status: 'fail', error: '上传失败', response }`。
    示例二:`{ status: 'success', response: { url: 'https://tdesign.gtimg.com/site/avatar.jpg' } }`。
    示例三:`{ status: 'success', files: [{ url: 'https://xxx.png', name: 'xxx.png' }]}`。TS 类型:`(files: UploadFile \| UploadFile[]) => Promise` `interface RequestMethodResponse { status: 'success' \| 'fail'; error?: string; response: { url?: string; files?: UploadFile[]; [key: string]: any } }`。[详细类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/upload/type.ts) | N +showThumbnail | Boolean | false | 是否在文件列表中显示缩略图,`theme=file-flow` 时有效 | N showUploadProgress | Boolean | true | 是否显示上传进度 | N sizeLimit | Number / Object | - | 图片文件大小限制,默认单位 KB。可选单位有:`'B' \| 'KB' \| 'MB' \| 'GB'`。示例一:`1000`。示例二:`{ size: 2, unit: 'MB', message: '图片大小不超过 {sizeLimit} MB' }`。TS 类型:`number \| SizeLimitObj` `interface SizeLimitObj { size: number; unit: SizeUnit ; message?: string }` `type SizeUnitArray = ['B', 'KB', 'MB', 'GB']` `type SizeUnit = SizeUnitArray[number]`。[详细类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/upload/type.ts) | N status | String | - | 文件上传提示文本状态。可选项:default/success/warning/error | N diff --git a/src/upload/upload.tsx b/src/upload/upload.tsx index a7045c6a43..291f609c48 100644 --- a/src/upload/upload.tsx +++ b/src/upload/upload.tsx @@ -142,6 +142,7 @@ function TdUpload(props: UploadProps, ref: uploadFiles={uploadFiles} cancelUpload={cancelUpload} onPreview={props.onPreview} + showThumbnail={props.showThumbnail} >
    {triggerElement} diff --git a/test/snap/__snapshots__/csr.test.jsx.snap b/test/snap/__snapshots__/csr.test.jsx.snap index 8f6961ea9f..96dd77b522 100644 --- a/test/snap/__snapshots__/csr.test.jsx.snap +++ b/test/snap/__snapshots__/csr.test.jsx.snap @@ -38710,11 +38710,11 @@ exports[`csr snapshot test > csr test src/card/_example/base.jsx 1`] = ` class="t-card__header-wrapper" >
    - 标题 - +
    csr test src/card/_example/base.jsx 1`] = ` class="t-card__header-wrapper" >
    - 标题 - +
    csr test src/card/_example/bordered-none.jsx 1`] = class="t-card__header-wrapper" >
    - 标题 - +
    csr test src/card/_example/bordered-none.jsx 1`] = class="t-card__header-wrapper" >
    - 标题 - +
    csr test src/card/_example/header.jsx 1`] = ` class="t-card__header-wrapper" >
    - 标题 - +
    csr test src/card/_example/header.jsx 1`] = ` class="t-card__header-wrapper" >
    - 标题 - +
    csr test src/card/_example/header-all-props.jsx 1`] class="t-card__header-wrapper" >
    - 标题 - - +
    副标题 - +

    @@ -40793,16 +40793,16 @@ exports[`csr snapshot test > csr test src/card/_example/header-all-props.jsx 1`] class="t-card__header-wrapper" >

    - 标题 - - +
    副标题 - +

    @@ -40897,11 +40897,11 @@ exports[`csr snapshot test > csr test src/card/_example/header-bordered.jsx 1`] class="t-card__header-wrapper" >

    - 标题 - +
    csr test src/card/_example/header-bordered.jsx 1`] class="t-card__header-wrapper" >
    - 标题 - +
    csr test src/card/_example/header-description.jsx 1 class="t-card__header-wrapper" >
    - 标题 - +

    @@ -41070,11 +41070,11 @@ exports[`csr snapshot test > csr test src/card/_example/header-description.jsx 1 class="t-card__header-wrapper" >

    - 标题 - +

    @@ -41218,11 +41218,11 @@ exports[`csr snapshot test > csr test src/card/_example/header-footer-actions.js

    - 标题 - +

    @@ -41423,11 +41423,11 @@ exports[`csr snapshot test > csr test src/card/_example/header-footer-actions.js

    - 标题 - +

    @@ -41640,16 +41640,16 @@ exports[`csr snapshot test > csr test src/card/_example/header-subtitle.jsx 1`] class="t-card__header-wrapper" >

    - 标题 - - +
    副标题 - +
    csr test src/card/_example/header-subtitle.jsx 1`] class="t-card__header-wrapper" >
    - 标题 - - +
    副标题 - +
    csr test src/card/_example/header-subtitle-footer-a class="t-card__header-wrapper" >
    - 标题 - - +
    副标题 - +
    csr test src/card/_example/header-subtitle-footer-a class="t-card__header-wrapper" >
    - 标题 - - +
    副标题 - +
    csr test src/form/_example/disabled.jsx 1`] = `
    - +
    + +
    +
    +
    + + + +
    +
    +
    +
    +
    @@ -93000,10 +93033,43 @@ exports[`csr snapshot test > csr test src/form/_example/disabled.jsx 1`] = `
    - +
    + +
    +
    +
    + + + +
    +
    +
    +
    +
    @@ -105752,7 +105818,7 @@ exports[`csr snapshot test > csr test src/image/_example/avif.jsx 1`] = ` />
    csr test src/image/_example/avif.jsx 1`] = ` />
    csr test src/table/_example/editable-cell.jsx 1`] = style="min-width: 80px;" /> csr test src/table/_example/editable-cell.jsx 1`] = style="min-width: 80px;" /> csr test src/table/_example/editable-row.jsx 1`] = style="width: 150px;" /> csr test src/table/_example/editable-row.jsx 1`] = style="width: 150px;" /> csr test src/tree/_example/vscroll.jsx 1`] = ` class="t-space-item" >
    Tree Empty Data @@ -285300,7 +285366,7 @@ exports[`csr snapshot test > csr test src/tree/_example/vscroll.jsx 1`] = ` class="t-space-item" >
    Tree Empty Data @@ -289223,7 +289289,7 @@ exports[`csr snapshot test > csr test src/upload/_example/file-flow-list.jsx 1`] >
    csr test src/upload/_example/file-flow-list.jsx 1`]
    +
    + +
    @@ -289447,7 +289536,7 @@ exports[`csr snapshot test > csr test src/upload/_example/file-flow-list.jsx 1`] >
    csr test src/upload/_example/file-flow-list.jsx 1`]
    +
    + +
    @@ -289950,10 +290062,43 @@ exports[`csr snapshot test > csr test src/upload/_example/image.jsx 1`] = `
    - +
    + +
    +
    +
    + + + +
    +
    +
    +
    +
    @@ -290289,10 +290434,43 @@ exports[`csr snapshot test > csr test src/upload/_example/image.jsx 1`] = `
    - +
    + +
    +
    +
    + + + +
    +
    +
    +
    +
    @@ -290538,7 +290716,7 @@ exports[`csr snapshot test > csr test src/upload/_example/img-flow-list.jsx 1`] - Upload + Continue Upload
    @@ -290551,11 +290729,196 @@ exports[`csr snapshot test > csr test src/upload/_example/img-flow-list.jsx 1`]
    -
    - Click "Upload" or Drag file to this area to upload -
    +
  • +
    +
    + +
    +
    +
    + + + +
    +
    +
    +
    +
    +
    + + + + + + + + + + + + + +
    +
    +

    + demo…-1.png +

    +
  • +
  • +
    +
    + +
    +
    +
    + + + +
    +
    +
    +
    +
    +
    + + + + + + + + + + + + + +
    +
    +

    + avatar.jpg +

    +
  • +
    csr test src/upload/_example/img-flow-list.jsx 1`] Cancel
    -
    csr test src/upload/_example/img-flow-list.jsx 1`] > Upload -
    +
    @@ -290658,7 +291020,7 @@ exports[`csr snapshot test > csr test src/upload/_example/img-flow-list.jsx 1`] - Upload + Continue Upload
    @@ -290671,11 +291033,196 @@ exports[`csr snapshot test > csr test src/upload/_example/img-flow-list.jsx 1`]
    -
    - Click "Upload" or Drag file to this area to upload -
    +
  • +
    +
    + +
    +
    +
    + + + +
    +
    +
    +
    +
    +
    + + + + + + + + + + + + + +
    +
    +

    + demo…-1.png +

    +
  • +
  • +
    +
    + +
    +
    +
    + + + +
    +
    +
    +
    +
    +
    + + + + + + + + + + + + + +
    +
    +

    + avatar.jpg +

    +
  • +
    csr test src/upload/_example/img-flow-list.jsx 1`] Cancel
    -
    csr test src/upload/_example/img-flow-list.jsx 1`] > Upload -
    +
    diff --git a/test/snap/__snapshots__/ssr.test.jsx.snap b/test/snap/__snapshots__/ssr.test.jsx.snap index fffe50ce2c..5517cab2b3 100644 --- a/test/snap/__snapshots__/ssr.test.jsx.snap +++ b/test/snap/__snapshots__/ssr.test.jsx.snap @@ -148,11 +148,11 @@ exports[`ssr snapshot test > ssr test src/calendar/_example/value.jsx 1`] = `" ssr test src/calendar/_example/week.jsx 1`] = `"
    请选择
    请选择
    隐藏周末
    01
    02
    03
    04
    05
    06
    07
    08
    09
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    01
    02
    03
    04
    "`; -exports[`ssr snapshot test > ssr test src/card/_example/base.jsx 1`] = `"
    标题
    操作
    仅有内容区域的卡片形式。卡片内容区域可以是文字、图片、表单、表格等形式信息内容。可使用大中小不同的卡片尺寸,按业务需求进行呈现。
    "`; +exports[`ssr snapshot test > ssr test src/card/_example/base.jsx 1`] = `"
    标题
    操作
    仅有内容区域的卡片形式。卡片内容区域可以是文字、图片、表单、表格等形式信息内容。可使用大中小不同的卡片尺寸,按业务需求进行呈现。
    "`; exports[`ssr snapshot test > ssr test src/card/_example/bordered.jsx 1`] = `"
    仅有内容区域的卡片形式。卡片内容区域可以是文字、图片、表单、表格等形式信息内容。可使用大中小不同的卡片尺寸,按业务需求进行呈现。
    "`; -exports[`ssr snapshot test > ssr test src/card/_example/bordered-none.jsx 1`] = `"
    标题
    仅有内容区域的卡片形式。卡片内容区域可以是文字、图片、表单、表格等形式信息内容。可使用大中小不同的卡片尺寸,按业务需求进行呈现。
    "`; +exports[`ssr snapshot test > ssr test src/card/_example/bordered-none.jsx 1`] = `"
    标题
    仅有内容区域的卡片形式。卡片内容区域可以是文字、图片、表单、表格等形式信息内容。可使用大中小不同的卡片尺寸,按业务需求进行呈现。
    "`; exports[`ssr snapshot test > ssr test src/card/_example/footer.jsx 1`] = `"
    默认标签
    \\"\\"/
    "`; @@ -162,19 +162,19 @@ exports[`ssr snapshot test > ssr test src/card/_example/footer-content.jsx 1`] = exports[`ssr snapshot test > ssr test src/card/_example/footer-content-actions.jsx 1`] = `"
    \\"\\"/
    "`; -exports[`ssr snapshot test > ssr test src/card/_example/header.jsx 1`] = `"
    标题
    卡片内容,以描述性为主,可以是文字、图片或图文组合的形式。按业务需求进行自定义组合。
    "`; +exports[`ssr snapshot test > ssr test src/card/_example/header.jsx 1`] = `"
    标题
    卡片内容,以描述性为主,可以是文字、图片或图文组合的形式。按业务需求进行自定义组合。
    "`; -exports[`ssr snapshot test > ssr test src/card/_example/header-all-props.jsx 1`] = `"
    标题副标题

    描述

    操作
    卡片内容,以描述性为主,可以是文字、图片或图文组合的形式。按业务需求进行自定义组合。
    "`; +exports[`ssr snapshot test > ssr test src/card/_example/header-all-props.jsx 1`] = `"
    标题
    副标题

    描述

    操作
    卡片内容,以描述性为主,可以是文字、图片或图文组合的形式。按业务需求进行自定义组合。
    "`; -exports[`ssr snapshot test > ssr test src/card/_example/header-bordered.jsx 1`] = `"
    标题
    卡片内容,以描述性为主,可以是文字、图片或图文组合的形式。按业务需求进行自定义组合。
    "`; +exports[`ssr snapshot test > ssr test src/card/_example/header-bordered.jsx 1`] = `"
    标题
    卡片内容,以描述性为主,可以是文字、图片或图文组合的形式。按业务需求进行自定义组合。
    "`; -exports[`ssr snapshot test > ssr test src/card/_example/header-description.jsx 1`] = `"
    标题

    描述

    操作
    卡片内容,以描述性为主,可以是文字、图片或图文组合的形式。按业务需求进行自定义组合。
    "`; +exports[`ssr snapshot test > ssr test src/card/_example/header-description.jsx 1`] = `"
    标题

    描述

    操作
    卡片内容,以描述性为主,可以是文字、图片或图文组合的形式。按业务需求进行自定义组合。
    "`; -exports[`ssr snapshot test > ssr test src/card/_example/header-footer-actions.jsx 1`] = `"
    图片加载中
    标题

    卡片内容

    \\"\\"/
    "`; +exports[`ssr snapshot test > ssr test src/card/_example/header-footer-actions.jsx 1`] = `"
    图片加载中
    标题

    卡片内容

    \\"\\"/
    "`; -exports[`ssr snapshot test > ssr test src/card/_example/header-subtitle.jsx 1`] = `"
    标题副标题
    操作
    卡片内容,以描述性为主,可以是文字、图片或图文组合的形式。按业务需求进行自定义组合。
    "`; +exports[`ssr snapshot test > ssr test src/card/_example/header-subtitle.jsx 1`] = `"
    标题
    副标题
    操作
    卡片内容,以描述性为主,可以是文字、图片或图文组合的形式。按业务需求进行自定义组合。
    "`; -exports[`ssr snapshot test > ssr test src/card/_example/header-subtitle-footer-actions.jsx 1`] = `"
    标题副标题
    \\"\\"/
    "`; +exports[`ssr snapshot test > ssr test src/card/_example/header-subtitle-footer-actions.jsx 1`] = `"
    标题
    副标题
    \\"\\"/
    "`; exports[`ssr snapshot test > ssr test src/cascader/_example/base.jsx 1`] = `"
    "`; @@ -264,7 +264,7 @@ exports[`ssr snapshot test > ssr test src/config-provider/_example/dialog.jsx 1` exports[`ssr snapshot test > ssr test src/config-provider/_example/global.jsx 1`] = `"

    使用ConfigProvider包裹业务功能的最外层组件,点击下方图标查看示例代码

    英文语言包引入路径:import enConfig from 'tdesign-react/es/locale/en_US';

    中文语言包引入路径:import zhConfig from 'tdesign-react/es/locale/zh_CN';

    日文语言包引入路径:import jpConfig from 'tdesign-react/es/locale/ja_JP';

    韩文语言包引入路径:import koConfig from 'tdesign-react/es/locale/ko_KR';

    "`; -exports[`ssr snapshot test > ssr test src/config-provider/_example/others.jsx 1`] = `"
    Feature Tag
    Feature Tag
    Feature Tag
    Feature Tag
    Tree Empty Data
    First Step
    You need to click the blue button
    Second Step
    Fill your base information into the form
    Error Step
    Something Wrong! Custom Error Icon!
    4
    Last Step
    You haven't finish this step.
    图片加载中
    "`; +exports[`ssr snapshot test > ssr test src/config-provider/_example/others.jsx 1`] = `"
    Feature Tag
    Feature Tag
    Feature Tag
    Feature Tag
    Tree Empty Data
    First Step
    You need to click the blue button
    Second Step
    Fill your base information into the form
    Error Step
    Something Wrong! Custom Error Icon!
    4
    Last Step
    You haven't finish this step.
    图片加载中
    "`; exports[`ssr snapshot test > ssr test src/config-provider/_example/pagination.jsx 1`] = `"
    Total 36 items
    please select
    • 1
    • 2
    • 3
    • 4
    jump to
    / 4
    "`; @@ -372,7 +372,7 @@ exports[`ssr snapshot test > ssr test src/form/_example/clear-validate.jsx 1`] = exports[`ssr snapshot test > ssr test src/form/_example/custom-validator.jsx 1`] = `"
    "`; -exports[`ssr snapshot test > ssr test src/form/_example/disabled.jsx 1`] = `"
    请选择单张图片文件上传
    提交
    重置
    "`; +exports[`ssr snapshot test > ssr test src/form/_example/disabled.jsx 1`] = `"
    请选择单张图片文件上传
    提交
    重置
    "`; exports[`ssr snapshot test > ssr test src/form/_example/error-message.jsx 1`] = `"
    这是用户名字段帮助说明
    一句话介绍自己
    "`; @@ -440,7 +440,7 @@ exports[`ssr snapshot test > ssr test src/image/_example/extra-always.jsx 1`] = exports[`ssr snapshot test > ssr test src/image/_example/extra-hover.jsx 1`] = `"
    图片加载中
    预览
    "`; -exports[`ssr snapshot test > ssr test src/image/_example/fill-mode.jsx 1`] = `"
    图片加载中
    fill
    图片加载中
    contain
    图片加载中
    cover
    图片加载中
    none
    图片加载中
    scale-down
    "`; +exports[`ssr snapshot test > ssr test src/image/_example/fill-mode.jsx 1`] = `"
    图片加载中
    fill
    图片加载中
    contain
    图片加载中
    cover
    图片加载中
    none
    图片加载中
    scale-down
    "`; exports[`ssr snapshot test > ssr test src/image/_example/fill-position.jsx 1`] = `"
    图片加载中
    cover center
    图片加载中
    cover left
    图片加载中
    cover right
    图片加载中
    cover top
    图片加载中
    cover bottom
    图片加载中
    contain top
    图片加载中
    contain bottom
    图片加载中
    contain center
    图片加载中
    contain left
    图片加载中
    contain right
    "`; @@ -450,7 +450,7 @@ exports[`ssr snapshot test > ssr test src/image/_example/lazy-list.jsx 1`] = `"< exports[`ssr snapshot test > ssr test src/image/_example/lazy-single.jsx 1`] = `"
    "`; -exports[`ssr snapshot test > ssr test src/image/_example/placeholder.jsx 1`] = `"

    加载中的图片

    默认占位
    图片加载中
    自定义占位

    加载失败的图片

    默认错误
    图片加载中
    自定义错误
    图片加载中
    "`; +exports[`ssr snapshot test > ssr test src/image/_example/placeholder.jsx 1`] = `"

    加载中的图片

    默认占位
    图片加载中
    自定义占位

    加载失败的图片

    默认错误
    图片加载中
    自定义错误
    图片加载中
    "`; exports[`ssr snapshot test > ssr test src/image/_example/shape.jsx 1`] = `"
    图片加载中
    square
    图片加载中
    round
    图片加载中
    circle
    "`; @@ -464,11 +464,11 @@ exports[`ssr snapshot test > ssr test src/image-viewer/_example/block.jsx 1`] = exports[`ssr snapshot test > ssr test src/image-viewer/_example/button.jsx 1`] = `""`; -exports[`ssr snapshot test > ssr test src/image-viewer/_example/error.jsx 1`] = `"
    \\"test\\"/
    图片加载中
    预览
    \\"test\\"/
    图片加载中
    预览
    \\"test\\"/
    图片加载中
    预览
    \\"test\\"/
    图片加载中
    预览
    "`; +exports[`ssr snapshot test > ssr test src/image-viewer/_example/error.jsx 1`] = `"
    \\"test\\"/
    图片加载中
    预览
    \\"test\\"/
    图片加载中
    预览
    \\"test\\"/
    图片加载中
    预览
    \\"test\\"/
    图片加载中
    预览
    "`; exports[`ssr snapshot test > ssr test src/image-viewer/_example/modeless.jsx 1`] = `"
    \\"test\\"/
    图片加载中
    预览
    "`; -exports[`ssr snapshot test > ssr test src/image-viewer/_example/multiple.jsx 1`] = `"
    \\"test\\"/
    图片加载中
    预览
    \\"test\\"/
    图片加载中
    预览
    \\"test\\"/
    图片加载中
    预览
    "`; +exports[`ssr snapshot test > ssr test src/image-viewer/_example/multiple.jsx 1`] = `"
    \\"test\\"/
    图片加载中
    预览
    \\"test\\"/
    图片加载中
    预览
    \\"test\\"/
    图片加载中
    预览
    "`; exports[`ssr snapshot test > ssr test src/input/_example/addon.jsx 1`] = `"
    http://
    http://
    .com
    "`; @@ -902,9 +902,9 @@ exports[`ssr snapshot test > ssr test src/table/_example/drag-sort.jsx 1`] = `"< exports[`ssr snapshot test > ssr test src/table/_example/drag-sort-handler.jsx 1`] = `"
    排序
    申请人
    申请状态
    签署方式
    邮箱地址
    申请时间
    贾明审批通过电子签署
    w.cezkdudy@lhll.au
    2022-01-01
    张三审批失败纸质签署
    r.nmgw@peurezgn.sl
    2022-02-01
    王芳审批过期纸质签署
    p.cumx@rampblpa.ru
    2022-03-01
    贾明审批通过电子签署
    w.cezkdudy@lhll.au
    2022-04-01
    张三审批失败纸质签署
    r.nmgw@peurezgn.sl
    2022-01-01
    "`; -exports[`ssr snapshot test > ssr test src/table/_example/editable-cell.jsx 1`] = `"
    申请人
    申请状态
    申请事项
    创建日期
    please select
    please select
    please select
    please select
    please select
    "`; +exports[`ssr snapshot test > ssr test src/table/_example/editable-cell.jsx 1`] = `"
    申请人
    申请状态
    申请事项
    创建日期
    please select
    please select
    please select
    please select
    please select
    "`; -exports[`ssr snapshot test > ssr test src/table/_example/editable-row.jsx 1`] = `"


    申请人
    申请状态
    申请事项
    创建日期
    操作栏
    please enter
    please select
    please select
    "`; +exports[`ssr snapshot test > ssr test src/table/_example/editable-row.jsx 1`] = `"


    申请人
    申请状态
    申请事项
    创建日期
    操作栏
    please enter
    please select
    please select
    "`; exports[`ssr snapshot test > ssr test src/table/_example/ellipsis.jsx 1`] = `"
    申请人
    审批状态
    签署方式(超长标题示例)
    邮箱地址
    申请事项
    审核时间
    操作
    贾明(kyrieJia)
    审批通过电子签署
    w.cezkdudy@lhll.au
    宣传物料制作费用
    2021-11-01
    张三(threeZhang)
    审批失败纸质签署
    r.nmgw@peurezgn.sl
    algolia 服务报销
    2021-12-01
    王芳(fangWang)
    审批过期纸质签署
    p.cumx@rampblpa.ru
    相关周边制作费
    2022-01-01
    贾明(kyrieJia)
    审批通过电子签署
    w.cezkdudy@lhll.au
    激励奖品快递费
    2022-02-01
    张三(threeZhang)
    审批失败纸质签署
    r.nmgw@peurezgn.sl
    宣传物料制作费用
    2021-11-01
    "`; @@ -924,7 +924,7 @@ exports[`ssr snapshot test > ssr test src/table/_example/lazy.jsx 1`] = `"
    ssr test src/table/_example/loading.jsx 1`] = `"
    集群名称
    状态
    管理员
    描述
    集群名称
    状态
    管理员
    描述
    自定义加载状态文本
    集群名称
    状态
    管理员
    描述
      渲染函数自定义加载中(可单独去除内置加载图标)
    "`; -exports[`ssr snapshot test > ssr test src/table/_example/merge-cells.jsx 1`] = `"
    申请人
    申请状态
    审批事项
    邮箱地址
    其他信息
    贾明审批通过宣传物料制作费用w.cezkdudy@lhll.au电子签署2021-11-01
    张三审批失败algolia 服务报销r.nmgw@peurezgn.sl纸质签署2021-11-01
    王芳审批过期相关周边制作费p.cumx@rampblpa.ru纸质签署2021-11-01
    贾明审批通过激励奖品快递费b.nmgw@peurezgn.sl电子签署2021-11-01
    张三审批失败宣传物料制作费用d.cumx@rampblpa.ru纸质签署2021-11-01
    王芳审批过期algolia 服务报销w.cezkdudy@lhll.au纸质签署2021-11-01
    "`; +exports[`ssr snapshot test > ssr test src/table/_example/merge-cells.jsx 1`] = `"
    申请人
    申请状态
    审批事项
    邮箱地址
    其他信息
    "`; exports[`ssr snapshot test > ssr test src/table/_example/multi-header.jsx 1`] = `"
    申请人
    申请汇总
    住宿费
    交通费
    物料费
    奖品激励费
    审批汇总
    申请时间
    申请状态
    申请渠道和金额
    审批状态
    说明
    类型
    申请耗时(天)
    审批单号
    邮箱地址
    贾明审批通过电子签署3100100100100组长审批审批单号001
    w.cezkdudy@lhll.au
    2022-01-01
    张三审批失败纸质签署2200200200200部门审批审批单号002
    r.nmgw@peurezgn.sl
    2022-02-01
    王芳审批过期纸质签署4400400400400财务审批审批单号003
    p.cumx@rampblpa.ru
    2022-03-01
    贾明审批通过电子签署1500500500500组长审批审批单号004
    w.cezkdudy@lhll.au
    2022-04-01
    张三审批失败纸质签署3100100100100部门审批审批单号005
    r.nmgw@peurezgn.sl
    2022-01-01
    王芳审批过期纸质签署2200200200200财务审批审批单号006
    p.cumx@rampblpa.ru
    2022-02-01
    贾明审批通过电子签署4400400400400组长审批审批单号007
    w.cezkdudy@lhll.au
    2022-03-01
    张三审批失败纸质签署1500500500500部门审批审批单号008
    r.nmgw@peurezgn.sl
    2022-04-01
    王芳审批过期纸质签署3100100100100财务审批审批单号009
    p.cumx@rampblpa.ru
    2022-01-01
    贾明审批通过电子签署2200200200200组长审批审批单号0010
    w.cezkdudy@lhll.au
    2022-02-01
    张三审批失败纸质签署4400400400400部门审批审批单号0011
    r.nmgw@peurezgn.sl
    2022-03-01
    王芳审批过期纸质签署1500500500500财务审批审批单号0012
    p.cumx@rampblpa.ru
    2022-04-01
    贾明审批通过电子签署3100100100100组长审批审批单号0013
    w.cezkdudy@lhll.au
    2022-01-01
    张三审批失败纸质签署2200200200200部门审批审批单号0014
    r.nmgw@peurezgn.sl
    2022-02-01
    王芳审批过期纸质签署4400400400400财务审批审批单号0015
    p.cumx@rampblpa.ru
    2022-03-01
    贾明审批通过电子签署1500500500500组长审批审批单号0016
    w.cezkdudy@lhll.au
    2022-04-01
    张三审批失败纸质签署3100100100100部门审批审批单号0017
    r.nmgw@peurezgn.sl
    2022-01-01
    王芳审批过期纸质签署2200200200200财务审批审批单号0018
    p.cumx@rampblpa.ru
    2022-02-01
    贾明审批通过电子签署4400400400400组长审批审批单号0019
    w.cezkdudy@lhll.au
    2022-03-01
    张三审批失败纸质签署1500500500500部门审批审批单号0020
    r.nmgw@peurezgn.sl
    2022-04-01
    "`; @@ -1100,7 +1100,7 @@ exports[`ssr snapshot test > ssr test src/tree/_example/state.jsx 1`] = `"
    ssr test src/tree/_example/sync.jsx 1`] = `"
    checked:
    expanded:
    actived:
    Tree Empty Data
    "`; -exports[`ssr snapshot test > ssr test src/tree/_example/vscroll.jsx 1`] = `"
    Tree Empty Data
    "`; +exports[`ssr snapshot test > ssr test src/tree/_example/vscroll.jsx 1`] = `"
    Tree Empty Data
    "`; exports[`ssr snapshot test > ssr test src/tree-select/_example/base.jsx 1`] = `"
    "`; @@ -1128,11 +1128,11 @@ exports[`ssr snapshot test > ssr test src/upload/_example/custom-drag.jsx 1`] = exports[`ssr snapshot test > ssr test src/upload/_example/draggable.jsx 1`] = `"
    是否自动上传:

    Upload  /  Drag file to this area to upload
    默认文件
    size1.0 KBdate2022-09-25
    "`; -exports[`ssr snapshot test > ssr test src/upload/_example/file-flow-list.jsx 1`] = `"

    支持批量上传文件,文件格式不限,最多只能上传 10 份文件
    Click "Upload" or Drag file to this area to upload
    Cancel
    Upload
    "`; +exports[`ssr snapshot test > ssr test src/upload/_example/file-flow-list.jsx 1`] = `"

    支持批量上传文件,文件格式不限,最多只能上传 10 份文件
    Click "Upload" or Drag file to this area to upload
    Cancel
    Upload
    "`; -exports[`ssr snapshot test > ssr test src/upload/_example/image.jsx 1`] = `"

    • 请选择图片

    请选择单张图片文件上传(上传成功状态演示)
    • Click to upload

    单张图片文件上传(上传失败状态演示)
    • Click to upload

    允许选择多张图片文件上传,最多只能上传 3 张图片
    "`; +exports[`ssr snapshot test > ssr test src/upload/_example/image.jsx 1`] = `"

    • 请选择图片

    请选择单张图片文件上传(上传成功状态演示)
    • Click to upload

    单张图片文件上传(上传失败状态演示)
    • Click to upload

    允许选择多张图片文件上传,最多只能上传 3 张图片
    "`; -exports[`ssr snapshot test > ssr test src/upload/_example/img-flow-list.jsx 1`] = `"
    是否自动上传:

    支持批量上传图片文件
    Click "Upload" or Drag file to this area to upload
    Cancel
    Upload
    "`; +exports[`ssr snapshot test > ssr test src/upload/_example/img-flow-list.jsx 1`] = `"
    是否自动上传:

    支持批量上传图片文件
    • demo…-1.png

    • avatar.jpg

    Cancel
    "`; exports[`ssr snapshot test > ssr test src/upload/_example/request-method.jsx 1`] = `"
    自定义上传方法需要返回成功或失败信息
    "`;