Browse Source

refactor: extract duplicated components to shared locations (#11692)

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
pull/11695/head
afc163 1 month ago
committed by GitHub
parent
commit
6690640635
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 0
      src/components/ArticleListContent/index.style.ts
  2. 14
      src/components/ArticleListContent/index.tsx
  3. 0
      src/components/AvatarList/index.style.ts
  4. 6
      src/components/AvatarList/index.tsx
  5. 0
      src/components/StandardFormRow/index.style.ts
  6. 0
      src/components/StandardFormRow/index.tsx
  7. 0
      src/components/TagSelect/index.style.ts
  8. 0
      src/components/TagSelect/index.tsx
  9. 8
      src/components/index.ts
  10. 29
      src/pages/account/center/components/ArticleListContent/index.tsx
  11. 2
      src/pages/account/center/components/Articles/index.tsx
  12. 2
      src/pages/account/center/components/Projects/index.tsx
  13. 60
      src/pages/list/search/applications/components/StandardFormRow/index.style.ts
  14. 38
      src/pages/list/search/applications/components/StandardFormRow/index.tsx
  15. 3
      src/pages/list/search/applications/index.tsx
  16. 29
      src/pages/list/search/articles/components/ArticleListContent/index.style.ts
  17. 35
      src/pages/list/search/articles/components/TagSelect/index.style.ts
  18. 160
      src/pages/list/search/articles/components/TagSelect/index.tsx
  19. 4
      src/pages/list/search/articles/index.tsx
  20. 41
      src/pages/list/search/projects/components/AvatarList/index.style.ts
  21. 91
      src/pages/list/search/projects/components/AvatarList/index.tsx
  22. 62
      src/pages/list/search/projects/components/StandardFormRow/index.style.ts
  23. 38
      src/pages/list/search/projects/components/StandardFormRow/index.tsx
  24. 35
      src/pages/list/search/projects/components/TagSelect/index.style.ts
  25. 160
      src/pages/list/search/projects/components/TagSelect/index.tsx
  26. 4
      src/pages/list/search/projects/index.tsx

0
src/pages/account/center/components/ArticleListContent/index.style.ts → src/components/ArticleListContent/index.style.ts

14
src/pages/list/search/articles/components/ArticleListContent/index.tsx → src/components/ArticleListContent/index.tsx

@ -3,15 +3,16 @@ import dayjs from 'dayjs';
import React from 'react';
import useStyles from './index.style';
type ArticleListContentProps = {
export type ArticleListContentProps = {
data: {
content: React.ReactNode;
updatedAt: number;
avatar: string;
owner: string;
href: string;
content?: React.ReactNode;
updatedAt?: number;
avatar?: string;
owner?: string;
href?: string;
};
};
const ArticleListContent: React.FC<ArticleListContentProps> = ({
data: { content, updatedAt, avatar, owner, href },
}) => {
@ -27,4 +28,5 @@ const ArticleListContent: React.FC<ArticleListContentProps> = ({
</div>
);
};
export default ArticleListContent;

0
src/pages/account/center/components/AvatarList/index.style.ts → src/components/AvatarList/index.style.ts

6
src/pages/account/center/components/AvatarList/index.tsx → src/components/AvatarList/index.tsx

@ -2,7 +2,9 @@ import { Avatar, Tooltip } from 'antd';
import { clsx } from 'clsx';
import React from 'react';
import useStyles from './index.style';
export declare type SizeType = number | 'small' | 'default' | 'large';
export type AvatarItemProps = {
tips: React.ReactNode;
src: string;
@ -10,6 +12,7 @@ export type AvatarItemProps = {
style?: React.CSSProperties;
onClick?: () => void;
};
export type AvatarListProps = {
Item?: React.ReactElement<AvatarItemProps>;
size?: SizeType;
@ -54,6 +57,7 @@ const Item: React.FC<AvatarItemProps> = ({
</li>
);
};
const AvatarList: React.FC<AvatarListProps> & {
Item: typeof Item;
} = ({ children, size, maxLength = 5, excessItemsStyle, ...other }) => {
@ -85,5 +89,7 @@ const AvatarList: React.FC<AvatarListProps> & {
</div>
);
};
AvatarList.Item = Item;
export default AvatarList;

0
src/pages/list/search/articles/components/StandardFormRow/index.style.ts → src/components/StandardFormRow/index.style.ts

0
src/pages/list/search/articles/components/StandardFormRow/index.tsx → src/components/StandardFormRow/index.tsx

0
src/pages/list/search/applications/components/TagSelect/index.style.ts → src/components/TagSelect/index.style.ts

0
src/pages/list/search/applications/components/TagSelect/index.tsx → src/components/TagSelect/index.tsx

8
src/components/index.ts

@ -9,4 +9,12 @@ import Footer from './Footer';
import { Question, SelectLang } from './RightContent';
import { AvatarDropdown, AvatarName } from './RightContent/AvatarDropdown';
/**
*
*/
export { default as ArticleListContent } from './ArticleListContent';
export { default as AvatarList } from './AvatarList';
export { default as StandardFormRow } from './StandardFormRow';
export { default as TagSelect } from './TagSelect';
export { AvatarDropdown, AvatarName, Footer, Question, SelectLang };

29
src/pages/account/center/components/ArticleListContent/index.tsx

@ -1,29 +0,0 @@
import { Avatar } from 'antd';
import dayjs from 'dayjs';
import React from 'react';
import useStyles from './index.style';
export type ApplicationsProps = {
data: {
content?: string;
updatedAt?: any;
avatar?: string;
owner?: string;
href?: string;
};
};
const ArticleListContent: React.FC<ApplicationsProps> = ({
data: { content, updatedAt, avatar, owner, href },
}) => {
const { styles } = useStyles();
return (
<div>
<div className={styles.description}>{content}</div>
<div className={styles.extra}>
<Avatar src={avatar} size="small" />
<a href={href}>{owner}</a> <a href={href}>{href}</a>
<em>{dayjs(updatedAt).format('YYYY-MM-DD HH:mm')}</em>
</div>
</div>
);
};
export default ArticleListContent;

2
src/pages/account/center/components/Articles/index.tsx

@ -2,9 +2,9 @@ import { LikeOutlined, MessageFilled, StarTwoTone } from '@ant-design/icons';
import { useQuery } from '@tanstack/react-query';
import { Flex, List, Tag } from 'antd';
import React from 'react';
import { ArticleListContent } from '@/components';
import type { ListItemDataType } from '../../data.d';
import { queryFakeList } from '../../service';
import ArticleListContent from '../ArticleListContent';
import useStyles from './index.style';
const IconText: React.FC<{

2
src/pages/account/center/components/Projects/index.tsx

@ -3,9 +3,9 @@ import { Card, List } from 'antd';
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import React from 'react';
import { AvatarList } from '@/components';
import type { ListItemDataType } from '../../data.d';
import { queryFakeList } from '../../service';
import AvatarList from '../AvatarList';
import useStyles from './index.style';
dayjs.extend(relativeTime);

60
src/pages/list/search/applications/components/StandardFormRow/index.style.ts

@ -1,60 +0,0 @@
import { createStyles } from 'antd-style';
const useStyles = createStyles(({ token }) => {
return {
standardFormRow: {
display: 'flex',
marginBottom: '16px',
paddingBottom: '16px',
borderBottom: `1px dashed ${token.colorSplit}`,
'.ant-form-item, .ant-legacy-form-item': { marginRight: '24px' },
'.ant-form-item-label, .ant-legacy-form-item-label': {
label: {
marginRight: '0',
color: token.colorText,
},
},
'.ant-form-item-label, .ant-legacy-form-item-label, .ant-form-item-control, .ant-legacy-form-item-control':
{ padding: '0', lineHeight: '32px' },
},
label: {
flex: '0 0 auto',
marginRight: '24px',
color: token.colorTextHeading,
fontSize: token.fontSize,
textAlign: 'right',
'& > span': {
display: 'inline-block',
height: '32px',
lineHeight: '32px',
'&::after': {
content: "':'",
},
},
},
content: {
flex: '1 1 0',
'.ant-form-item, .ant-legacy-form-item': {
'&:last-child': {
marginRight: '0',
},
},
},
standardFormRowLast: {
marginBottom: '0',
paddingBottom: '0',
border: 'none',
},
standardFormRowBlock: {
'.ant-form-item, .ant-legacy-form-item, div.ant-form-item-control-wrapper, div.ant-legacy-form-item-control-wrapper':
{ display: 'block' },
},
standardFormRowGrid: {
'.ant-form-item, .ant-legacy-form-item, div.ant-form-item-control-wrapper, div.ant-legacy-form-item-control-wrapper':
{ display: 'block' },
'.ant-form-item-label, .ant-legacy-form-item-label': { float: 'left' },
},
};
});
export default useStyles;

38
src/pages/list/search/applications/components/StandardFormRow/index.tsx

@ -1,38 +0,0 @@
import { clsx } from 'clsx';
import React from 'react';
import useStyles from './index.style';
type StandardFormRowProps = {
title?: string;
last?: boolean;
block?: boolean;
grid?: boolean;
style?: React.CSSProperties;
children?: React.ReactNode;
};
const StandardFormRow: React.FC<StandardFormRowProps> = ({
title,
children,
last,
block,
grid,
...rest
}) => {
const { styles } = useStyles();
const cls = clsx(styles.standardFormRow, {
[styles.standardFormRowBlock]: block,
[styles.standardFormRowLast]: last,
[styles.standardFormRowGrid]: grid,
});
return (
<div className={cls} {...rest}>
{title && (
<div className={styles.label}>
<span>{title}</span>
</div>
)}
<div className={styles.content}>{children}</div>
</div>
);
};
export default StandardFormRow;

3
src/pages/list/search/applications/index.tsx

@ -19,9 +19,8 @@ import {
import numeral from 'numeral';
import type { FC } from 'react';
import React from 'react';
import { StandardFormRow, TagSelect } from '@/components';
import { categoryOptions } from '../../mock';
import StandardFormRow from './components/StandardFormRow';
import TagSelect from './components/TagSelect';
import type { ListItemDataType } from './data.d';
import { queryFakeList } from './service';
import useStyles from './style.style';

29
src/pages/list/search/articles/components/ArticleListContent/index.style.ts

@ -1,29 +0,0 @@
import { createStyles } from 'antd-style';
const useStyles = createStyles(({ token }) => {
return {
description: {
maxWidth: '720px',
lineHeight: '22px',
},
extra: {
marginTop: '16px',
color: token.colorTextSecondary,
lineHeight: '22px',
'& > em': {
marginLeft: '16px',
color: token.colorTextDisabled,
fontStyle: 'normal',
},
[`@media screen and (max-width: ${token.screenXS}px)`]: {
'& > em': {
display: 'block',
marginTop: '8px',
marginLeft: '0',
},
},
},
};
});
export default useStyles;

35
src/pages/list/search/articles/components/TagSelect/index.style.ts

@ -1,35 +0,0 @@
import { createStyles } from 'antd-style';
const useStyles = createStyles(({ token }) => {
return {
tagSelect: {
position: 'relative',
maxHeight: '32px',
marginLeft: '-8px',
overflow: 'hidden',
lineHeight: '32px',
transition: 'all 0.3s',
userSelect: 'none',
'.ant-tag': {
marginRight: '24px',
padding: '0 8px',
fontSize: token.fontSize,
},
},
trigger: {
position: 'absolute',
top: '0',
right: '0',
'span.anticon': { fontSize: '12px' },
},
expanded: {
maxHeight: '200px',
transition: 'all 0.3s',
},
hasExpandTag: {
paddingRight: '50px',
},
};
});
export default useStyles;

160
src/pages/list/search/articles/components/TagSelect/index.tsx

@ -1,160 +0,0 @@
import { DownOutlined, UpOutlined } from '@ant-design/icons';
import { useMergedState } from '@rc-component/util';
import { Tag } from 'antd';
import { clsx } from 'clsx';
import React, { type FC, useMemo, useState } from 'react';
import useStyles from './index.style';
const { CheckableTag } = Tag;
export interface TagSelectOptionProps {
value: string | number;
style?: React.CSSProperties;
checked?: boolean;
onChange?: (value: string | number, state: boolean) => void;
children?: React.ReactNode;
}
const TagSelectOption: React.FC<TagSelectOptionProps> & {
isTagSelectOption: boolean;
} = ({ children, checked, onChange, value }) => (
<CheckableTag
checked={!!checked}
key={value}
onChange={(state) => onChange?.(value, state)}
>
{children}
</CheckableTag>
);
TagSelectOption.isTagSelectOption = true;
type TagSelectOptionElement = React.ReactElement<
TagSelectOptionProps,
typeof TagSelectOption
>;
export interface TagSelectProps {
onChange?: (value: (string | number)[]) => void;
expandable?: boolean;
value?: (string | number)[];
defaultValue?: (string | number)[];
style?: React.CSSProperties;
hideCheckAll?: boolean;
actionsText?: {
expandText?: React.ReactNode;
collapseText?: React.ReactNode;
selectAllText?: React.ReactNode;
};
className?: string;
Option?: TagSelectOptionProps;
children?: TagSelectOptionElement | TagSelectOptionElement[];
}
const TagSelect: FC<TagSelectProps> & {
Option: typeof TagSelectOption;
} = (props) => {
const { styles } = useStyles();
const {
children,
hideCheckAll = false,
className,
style,
expandable,
actionsText = {},
} = props;
const [expand, setExpand] = useState<boolean>(false);
const [value, setValue] = useMergedState<(string | number)[]>(
props.defaultValue || [],
{
value: props.value,
defaultValue: props.defaultValue,
onChange: props.onChange,
},
);
const isTagSelectOption = (node: TagSelectOptionElement) =>
node?.type &&
(node.type.isTagSelectOption ||
node.type.displayName === 'TagSelectOption');
// Memoize all tags to avoid recalculating on every render
const allTags = useMemo(() => {
const childrenArray = React.Children.toArray(
children,
) as TagSelectOptionElement[];
return childrenArray
.filter((child) => isTagSelectOption(child))
.map((child) => child.props.value);
}, [children]);
// Use Set for O(1) lookups
const valueSet = useMemo(() => new Set(value || []), [value]);
const onSelectAll = (checked: boolean) => {
setValue(checked ? [...allTags] : []);
};
const handleTagChange = (tag: string | number, checked: boolean) => {
const checkedTags = new Set(value || []);
if (checked) {
checkedTags.add(tag);
} else {
checkedTags.delete(tag);
}
setValue([...checkedTags]);
};
const checkedAll = allTags.length === value?.length && allTags.length > 0;
const {
expandText = '展开',
collapseText = '收起',
selectAllText = '全部',
} = actionsText;
const cls = clsx(styles.tagSelect, className, {
[styles.hasExpandTag]: expandable,
[styles.expanded]: expand,
});
return (
<div className={cls} style={style}>
{hideCheckAll ? null : (
<CheckableTag
checked={checkedAll}
key="tag-select-__all__"
onChange={onSelectAll}
>
{selectAllText}
</CheckableTag>
)}
{children &&
React.Children.map(children, (child: TagSelectOptionElement) => {
if (isTagSelectOption(child)) {
return React.cloneElement(child, {
key: `tag-select-${child.props.value}`,
value: child.props.value,
checked: valueSet.has(child.props.value),
onChange: handleTagChange,
});
}
return child;
})}
{expandable && (
<a
className={styles.trigger}
onClick={() => {
setExpand(!expand);
}}
>
{expand ? (
<>
{collapseText} <UpOutlined />
</>
) : (
<>
{expandText}
<DownOutlined />
</>
)}
</a>
)}
</div>
);
};
TagSelect.Option = TagSelectOption;
export default TagSelect;

4
src/pages/list/search/articles/index.tsx

@ -9,10 +9,8 @@ import { Button, Card, Col, Flex, Form, List, Row, Select, Tag } from 'antd';
import type { DefaultOptionType } from 'antd/es/select';
import type { FC } from 'react';
import React, { useMemo, useRef } from 'react';
import { ArticleListContent, StandardFormRow, TagSelect } from '@/components';
import { categoryOptions } from '../../mock';
import ArticleListContent from './components/ArticleListContent';
import StandardFormRow from './components/StandardFormRow';
import TagSelect from './components/TagSelect';
import type { ListItemDataType } from './data.d';
import { queryFakeList } from './service';
import useStyles from './style.style';

41
src/pages/list/search/projects/components/AvatarList/index.style.ts

@ -1,41 +0,0 @@
import { createStyles } from 'antd-style';
const useStyles = createStyles(({ token }) => {
return {
avatarList: {
display: 'inline-block',
ul: { display: 'inline-block', marginLeft: '8px', fontSize: '0' },
},
avatarItem: {
display: 'inline-block',
width: token.controlHeight,
height: token.controlHeight,
marginLeft: '-8px',
fontSize: token.fontSize,
'.ant-avatar': { border: `1px solid ${token.colorBorder}` },
},
avatarItemLarge: {
width: token.controlHeightLG,
height: token.controlHeightLG,
},
avatarItemSmall: {
width: token.controlHeightSM,
height: token.controlHeightSM,
},
avatarItemMini: {
width: '20px',
height: '20px',
'.ant-avatar': {
width: '20px',
height: '20px',
lineHeight: '20px',
'.ant-avatar-string': {
fontSize: '12px',
lineHeight: '18px',
},
},
},
};
});
export default useStyles;

91
src/pages/list/search/projects/components/AvatarList/index.tsx

@ -1,91 +0,0 @@
import { Avatar, Tooltip } from 'antd';
import { clsx } from 'clsx';
import React from 'react';
import useStyles from './index.style';
export declare type SizeType = number | 'small' | 'default' | 'large';
export type AvatarItemProps = {
tips: React.ReactNode;
src: string;
size?: SizeType;
style?: React.CSSProperties;
onClick?: () => void;
};
export type AvatarListProps = {
Item?: React.ReactElement<AvatarItemProps>;
size?: SizeType;
maxLength?: number;
excessItemsStyle?: React.CSSProperties;
style?: React.CSSProperties;
children:
| React.ReactElement<AvatarItemProps>
| React.ReactElement<AvatarItemProps>[];
};
const avatarSizeToClassName = (size: SizeType | 'mini', styles: any) =>
clsx(styles.avatarItem, {
[styles.avatarItemLarge]: size === 'large',
[styles.avatarItemSmall]: size === 'small',
[styles.avatarItemMini]: size === 'mini',
});
const Item: React.FC<AvatarItemProps> = ({
src,
size,
tips,
onClick = () => {},
}) => {
const { styles } = useStyles();
const cls = avatarSizeToClassName(size || 'default', styles);
return (
<li className={cls} onClick={onClick}>
{tips ? (
<Tooltip title={tips}>
<Avatar
src={src}
size={size}
style={{
cursor: 'pointer',
}}
/>
</Tooltip>
) : (
<Avatar src={src} size={size} />
)}
</li>
);
};
const AvatarList: React.FC<AvatarListProps> & {
Item: typeof Item;
} = ({ children, size, maxLength = 5, excessItemsStyle, ...other }) => {
const { styles } = useStyles();
const numOfChildren = React.Children.count(children);
const numToShow = maxLength >= numOfChildren ? numOfChildren : maxLength;
const childrenArray = React.Children.toArray(
children,
) as React.ReactElement<AvatarItemProps>[];
const childrenWithProps = childrenArray.slice(0, numToShow).map((child) =>
React.cloneElement(child, {
size,
}),
);
if (numToShow < numOfChildren) {
const cls = avatarSizeToClassName(size || 'default', styles);
childrenWithProps.push(
<li key="exceed" className={cls}>
<Avatar
size={size}
style={excessItemsStyle}
>{`+${numOfChildren - maxLength}`}</Avatar>
</li>,
);
}
return (
<div {...other} className={styles.avatarList}>
<ul> {childrenWithProps} </ul>
</div>
);
};
AvatarList.Item = Item;
export default AvatarList;

62
src/pages/list/search/projects/components/StandardFormRow/index.style.ts

@ -1,62 +0,0 @@
import { createStyles } from 'antd-style';
const useStyles = createStyles(({ token }) => {
return {
standardFormRow: {
display: 'flex',
width: '100%',
marginBottom: '16px',
paddingBottom: '16px',
borderBottom: `1px dashed ${token.colorSplit}`,
'.ant-form-item, .ant-legacy-form-item': { marginRight: '24px' },
'.ant-form-item-label, .ant-legacy-form-item-label': {
label: {
marginRight: '0',
color: token.colorText,
},
},
'.ant-form-item-label, .ant-legacy-form-item-label, .ant-form-item-control, .ant-legacy-form-item-control':
{ padding: '0', lineHeight: '32px' },
},
label: {
flex: '0 0 auto',
marginRight: '24px',
color: token.colorTextHeading,
fontSize: token.fontSize,
textAlign: 'right',
'& > span': {
display: 'inline-block',
height: '32px',
lineHeight: '32px',
'&::after': {
content: "':'",
},
},
},
content: {
flex: '1 1 0',
'.ant-form-item, .ant-legacy-form-item': {
'&:last-child': {
display: 'block',
marginRight: '0',
},
},
},
standardFormRowLast: {
marginBottom: '0',
paddingBottom: '0',
border: 'none',
},
standardFormRowBlock: {
'.ant-form-item, .ant-legacy-form-item, div.ant-form-item-control-wrapper, div.ant-legacy-form-item-control-wrapper':
{ display: 'block' },
},
standardFormRowGrid: {
'.ant-form-item, .ant-legacy-form-item, div.ant-form-item-control-wrapper, div.ant-legacy-form-item-control-wrapper':
{ display: 'block' },
'.ant-form-item-label, .ant-legacy-form-item-label': { float: 'left' },
},
};
});
export default useStyles;

38
src/pages/list/search/projects/components/StandardFormRow/index.tsx

@ -1,38 +0,0 @@
import { clsx } from 'clsx';
import React from 'react';
import useStyles from './index.style';
type StandardFormRowProps = {
title?: string;
last?: boolean;
block?: boolean;
grid?: boolean;
children?: React.ReactNode;
style?: React.CSSProperties;
};
const StandardFormRow: React.FC<StandardFormRowProps> = ({
title,
children,
last,
block,
grid,
...rest
}) => {
const { styles } = useStyles();
const cls = clsx(styles.standardFormRow, {
[styles.standardFormRowBlock]: block,
[styles.standardFormRowLast]: last,
[styles.standardFormRowGrid]: grid,
});
return (
<div className={cls} {...rest}>
{title && (
<div className={styles.label}>
<span>{title}</span>
</div>
)}
<div className={styles.content}>{children}</div>
</div>
);
};
export default StandardFormRow;

35
src/pages/list/search/projects/components/TagSelect/index.style.ts

@ -1,35 +0,0 @@
import { createStyles } from 'antd-style';
const useStyles = createStyles(({ token }) => {
return {
tagSelect: {
position: 'relative',
maxHeight: '32px',
marginLeft: '-8px',
overflow: 'hidden',
lineHeight: '32px',
transition: 'all 0.3s',
userSelect: 'none',
'.ant-tag': {
marginRight: '24px',
padding: '0 8px',
fontSize: token.fontSize,
},
},
trigger: {
position: 'absolute',
top: '0',
right: '0',
'span.anticon': { fontSize: '12px' },
},
expanded: {
maxHeight: '200px',
transition: 'all 0.3s',
},
hasExpandTag: {
paddingRight: '50px',
},
};
});
export default useStyles;

160
src/pages/list/search/projects/components/TagSelect/index.tsx

@ -1,160 +0,0 @@
import { DownOutlined, UpOutlined } from '@ant-design/icons';
import { useMergedState } from '@rc-component/util';
import { Tag } from 'antd';
import { clsx } from 'clsx';
import React, { type FC, useMemo, useState } from 'react';
import useStyles from './index.style';
const { CheckableTag } = Tag;
export interface TagSelectOptionProps {
value: string | number;
style?: React.CSSProperties;
checked?: boolean;
onChange?: (value: string | number, state: boolean) => void;
children?: React.ReactNode;
}
const TagSelectOption: React.FC<TagSelectOptionProps> & {
isTagSelectOption: boolean;
} = ({ children, checked, onChange, value }) => (
<CheckableTag
checked={!!checked}
key={value}
onChange={(state) => onChange?.(value, state)}
>
{children}
</CheckableTag>
);
TagSelectOption.isTagSelectOption = true;
type TagSelectOptionElement = React.ReactElement<
TagSelectOptionProps,
typeof TagSelectOption
>;
export interface TagSelectProps {
onChange?: (value: (string | number)[]) => void;
expandable?: boolean;
value?: (string | number)[];
defaultValue?: (string | number)[];
style?: React.CSSProperties;
hideCheckAll?: boolean;
actionsText?: {
expandText?: React.ReactNode;
collapseText?: React.ReactNode;
selectAllText?: React.ReactNode;
};
className?: string;
Option?: TagSelectOptionProps;
children?: TagSelectOptionElement | TagSelectOptionElement[];
}
const TagSelect: FC<TagSelectProps> & {
Option: typeof TagSelectOption;
} = (props) => {
const { styles } = useStyles();
const {
children,
hideCheckAll = false,
className,
style,
expandable,
actionsText = {},
} = props;
const [expand, setExpand] = useState<boolean>(false);
const [value, setValue] = useMergedState<(string | number)[]>(
props.defaultValue || [],
{
value: props.value,
defaultValue: props.defaultValue,
onChange: props.onChange,
},
);
const isTagSelectOption = (node: TagSelectOptionElement) =>
node?.type &&
(node.type.isTagSelectOption ||
node.type.displayName === 'TagSelectOption');
// Memoize all tags to avoid recalculating on every render
const allTags = useMemo(() => {
const childrenArray = React.Children.toArray(
children,
) as TagSelectOptionElement[];
return childrenArray
.filter((child) => isTagSelectOption(child))
.map((child) => child.props.value);
}, [children]);
// Use Set for O(1) lookups
const valueSet = useMemo(() => new Set(value || []), [value]);
const onSelectAll = (checked: boolean) => {
setValue(checked ? [...allTags] : []);
};
const handleTagChange = (tag: string | number, checked: boolean) => {
const checkedTags = new Set(value || []);
if (checked) {
checkedTags.add(tag);
} else {
checkedTags.delete(tag);
}
setValue([...checkedTags]);
};
const checkedAll = allTags.length === value?.length && allTags.length > 0;
const {
expandText = '展开',
collapseText = '收起',
selectAllText = '全部',
} = actionsText;
const cls = clsx(styles.tagSelect, className, {
[styles.hasExpandTag]: expandable,
[styles.expanded]: expand,
});
return (
<div className={cls} style={style}>
{hideCheckAll ? null : (
<CheckableTag
checked={checkedAll}
key="tag-select-__all__"
onChange={onSelectAll}
>
{selectAllText}
</CheckableTag>
)}
{children &&
React.Children.map(children, (child: TagSelectOptionElement) => {
if (isTagSelectOption(child)) {
return React.cloneElement(child, {
key: `tag-select-${child.props.value}`,
value: child.props.value,
checked: valueSet.has(child.props.value),
onChange: handleTagChange,
});
}
return child;
})}
{expandable && (
<a
className={styles.trigger}
onClick={() => {
setExpand(!expand);
}}
>
{expand ? (
<>
{collapseText} <UpOutlined />
</>
) : (
<>
{expandText}
<DownOutlined />
</>
)}
</a>
)}
</div>
);
};
TagSelect.Option = TagSelectOption;
export default TagSelect;

4
src/pages/list/search/projects/index.tsx

@ -4,10 +4,8 @@ import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import type { FC } from 'react';
import { useState } from 'react';
import { AvatarList, StandardFormRow, TagSelect } from '@/components';
import { categoryOptions } from '../../mock';
import AvatarList from './components/AvatarList';
import StandardFormRow from './components/StandardFormRow';
import TagSelect from './components/TagSelect';
import type { ListItemDataType } from './data.d';
import { queryFakeList } from './service';
import useStyles from './style.style';

Loading…
Cancel
Save