Browse Source

增加IconPicker组件

main
dark 5 months ago
parent
commit
36d894f228
  1. 59
      src/components/icon/action/style.ts
  2. 7
      src/components/icon/index.tsx
  3. 43
      src/components/icon/picker/Display.tsx
  4. 40
      src/components/icon/picker/IconList.tsx
  5. 51
      src/components/icon/picker/IconRender.tsx
  6. 78
      src/components/icon/picker/IconThumbnail.tsx
  7. 74
      src/components/icon/picker/PickerPanel.tsx
  8. 31
      src/components/icon/picker/SearchBar.tsx
  9. 157
      src/components/icon/picker/context.tsx
  10. 35
      src/components/icon/picker/icons.ts
  11. 56
      src/components/icon/picker/index.tsx
  12. 20
      src/components/icon/types.ts

59
src/components/icon/action/style.ts

@ -1,37 +1,40 @@
import { createStyles } from '@/theme'
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
export const useStyles = createStyles(({ token, css, cx, prefixCls }, { size, className }) => {
const prefix = `${prefixCls}-${token.proPrefix}-icon`
const sizeBoundary =
typeof size === 'number'
? css`
width: ${size}px !important;
height: ${size}px !important;
`
: ''
const prefix = `${prefixCls}-${token.proPrefix}-icon`
const sizeBoundary =
typeof size === 'number'
? css`
width: ${size}px !important;
height: ${size}px !important;
`
: ''
const button = css`
display: flex;
align-items: center;
justify-content: center;
const button = css`
display: flex;
align-items: center;
justify-content: center;
border: 1px solid ${token.colorBorder};
&:hover {
color: ${token.colorText} !important;
}
&:hover {
color: ${token.colorText} !important;
}
&:active {
scale: 0.8;
color: ${token.colorText};
}
&:active {
scale: 0.8;
color: ${token.colorText};
}
transition: color 600ms ${token.motionEaseOut}, scale 400ms ${token.motionEaseOut},
background-color 100ms ${token.motionEaseOut};
`
transition: color 600ms ${token.motionEaseOut}, scale 400ms ${token.motionEaseOut},
background-color 100ms ${token.motionEaseOut};
`
return {
container: cx(prefix, button, sizeBoundary, className),
tooltip: css`
pointer-events: none;
`,
}
return {
container: cx(prefix, button, sizeBoundary, className),
tooltip: css`
pointer-events: none;
`,
} as const
})

7
src/components/icon/index.tsx

@ -3,9 +3,16 @@ import React, { Fragment } from 'react'
import * as AntIcons from '@ant-design/icons/es/icons'
import { IconComponentProps } from '@ant-design/icons/es/components/Icon'
import IconItem from './picker/IconRender.tsx'
export function Icon(props: Partial<IIconAllProps | IconComponentProps>) {
const { type, ...other } = props
if(type && ['antd', 'park'].includes(type as string)){
return <IconItem {...props as any}/>
}
const AntIcon = AntIcons[type as keyof typeof AntIcons]
if (AntIcon) {
return <AntIcon {...other}/>

43
src/components/icon/picker/Display.tsx

@ -1,26 +1,27 @@
import { useToken } from '@/theme'
import React from 'react'
import Icon from '../index.tsx'
import { useToken, cx, css } from '@/theme'
import ActionIcon from '../action'
import IconItem from './IconRender.tsx'
import { usePickerContext } from './context.tsx'
const Display = () => {
const token = useToken()
const DefaultIcon = (
<div
className={cx(css`
width: 8px;
height: 8px;
border-radius: 50%;
background: ${token.colorFillContent};
`)}
/>
)
return (
<Icon
onClick={togglePanel}
icon={!icon ? DefaultIcon : <IconItem {...icon} />}
/>
)
const ctx = usePickerContext()
const token = useToken()
const DefaultIcon = (
<div
className={cx(css`
width: 8px;
height: 8px;
border-radius: 50%;
background: ${token.colorFillContent};
`)}
/>
)
return ( <ActionIcon
onClick={() => ctx.actions.togglePanel()}
icon={!ctx.state.icon ? DefaultIcon : <IconItem {...ctx.state.icon as any} />} />
)
}
export default Display

40
src/components/icon/picker/IconList.tsx

@ -0,0 +1,40 @@
import type { FC } from 'react'
import { memo } from 'react'
import { createStyles } from '@/theme'
import IconThumbnail from './IconThumbnail'
import { getIconName } from './icons.ts'
import { usePickerContext } from './context.tsx'
/******************************************************
*********************** Style *************************
******************************************************/
const useStyles = createStyles(
({ css }) =>
css`
display: grid;
grid-template-columns: repeat(4, 1fr);
`,
)
/******************************************************
************************* Dom *************************
******************************************************/
const IconList: FC = () => {
const { actions: { displayListSelector } } = usePickerContext()
const { styles } = useStyles()
return (
<div className={styles}>
{displayListSelector().map((icon) => (
<IconThumbnail key={getIconName(icon)} icon={icon}/>
))}
</div>
)
}
export default memo(IconList)

51
src/components/icon/picker/IconRender.tsx

@ -1,42 +1,43 @@
import type { FC } from 'react'
import { memo } from 'react'
import AntdIcons from '../contents/antdIcons'
import { customIconList, registerCustomIcon } from '../contents/customIcons'
import { AntdIcon, ParkIcon, ALL_ICON_KEYS } from './icons.ts'
import { IconType } from '@icon-park/react/es/all'
export interface IconRenderProps {
type: 'antd' | 'iconfont' | 'custom';
componentName?: string;
props?:
| {
type?: string;
}
| any;
scriptUrl?: string;
type: 'antd' | 'park';
componentName?: string;
props?: | {
type?: string;
} | any;
}
export interface IIconRender {
(props: IconRenderProps): JSX.Element;
(props: IconRenderProps): JSX.Element;
}
const Render: FC<IconRenderProps> = memo(
({ type, componentName, props, scriptUrl }) => {
switch (type) {
case 'antd':
const Icon = AntdIcons[componentName]
return <Icon {...props} />
case 'iconfont':
const Iconfont = AntdIcons.createFromIconfontCN({
scriptUrl,
})
return <Iconfont {...props} />
}
},
({ type, componentName, props }) => {
switch (type) {
case 'antd':
// eslint-disable-next-line no-case-declarations
const AIcon = AntdIcon[componentName!]
return <AIcon {...props} />
case 'park':
// eslint-disable-next-line no-case-declarations
if (ALL_ICON_KEYS.indexOf(componentName as IconType) < 0) {
return null
}
return <ParkIcon type={componentName} {...props} />
default: {
return null
}
}
},
)
const IconIRender = Render as IIconRender
IconIRender.registerCustomIcon = registerCustomIcon
export default IconIRender

78
src/components/icon/picker/IconThumbnail.tsx

@ -0,0 +1,78 @@
import type { FC } from 'react'
import { memo } from 'react'
import { Flexbox } from 'react-layout-kit'
import { createStyles } from '@/theme'
import type { IconUnit } from '../types'
import IconItem from './IconRender'
import { usePickerContext } from './context.tsx'
import { getIconName } from './icons.ts'
/******************************************************
*********************** Style *************************
******************************************************/
const useStyles = createStyles(
({ token, css }) =>
css`
height: 48px;
width: 100%;
overflow: hidden;
box-shadow: 1px 0 0 0 ${token.colorBorderSecondary}, 0 1px 0 0 ${token.colorBorderSecondary},
1px 1px 0 0 ${token.colorBorderSecondary}, 1px 0 0 0 ${token.colorBorderSecondary} inset,
0 1px 0 0 ${token.colorBorderSecondary} inset;
background: ${token.colorBgContainer};
cursor: pointer;
font-size: 18px;
color: ${token.colorTextSecondary};
&:hover {
border: 1px solid ${token.colorBorder};
box-shadow: none;
}
&:active {
z-index: 5;
border: 1px solid ${token.colorPrimary};
border-radius: 2px;
box-shadow: 0 1px 2px ${token.colorPrimary};
}
`,
)
/******************************************************
************************* Dom *************************
******************************************************/
export interface IconBlockProps {
icon: IconUnit;
IconSource?;
createFromIconfont?: any;
}
const IconThumbnail: FC<IconBlockProps> = ({ icon }) => {
const { actions: { togglePanel, selectIcon } } = usePickerContext()
const { styles, cx } = useStyles()
return (
<Flexbox
className={cx('icon-box', styles)}
title={getIconName(icon)}
align={'center'}
distribution={'center'}
onClick={() => {
selectIcon(icon)
togglePanel()
}}
>
<IconItem {...icon as any} />
</Flexbox>
)
}
export default memo(IconThumbnail)

74
src/components/icon/picker/PickerPanel.tsx

@ -0,0 +1,74 @@
import { AntDesignOutlined } from '@ant-design/icons'
import { Button, Result, Segmented } from 'antd'
import { Flexbox } from 'react-layout-kit'
import IconItem from './IconRender'
import { css, cx, useToken } from '@/theme'
import { getIconName } from './icons.ts'
import SearchBar from './SearchBar.tsx'
import IconList from './IconList.tsx'
import { usePickerContext } from './context.tsx'
const PickerPanel = () => {
const {
state: { icon, panelTabKey, },
actions: { resetIcon, changePanelTab, isEmptyIconAntdList, isEmptyIconParkList }
} = usePickerContext()
const token = useToken()
return (
<Flexbox width={300} gap={12} style={{ maxHeight: 400 }}>
{icon ? (
<Flexbox distribution={'space-between'} horizontal align={'center'}>
<Flexbox horizontal align={'center'} gap={8}>
<IconItem {...icon as any} />
<div>{getIconName(icon)}</div>
</Flexbox>
<Button size={'small'} type={'link'} onClick={resetIcon}>
</Button>
</Flexbox>
) : undefined}
<Segmented
options={[
{ label: 'Ant Design', value: 'antd', icon: <AntDesignOutlined/> },
{ label: 'Icon Park', value: 'park' },
]}
value={panelTabKey}
onChange={(key) => {
changePanelTab(key as any)
}}
block
/>
{(isEmptyIconAntdList || isEmptyIconParkList) ? (
(
<Result
status={'info'}
style={{ padding: 0, paddingTop: 8 }}
subTitle={'暂未选择图标库,请选择图标库'}
/>
)
) : (
<>
<SearchBar/>
<Flexbox
className={cx(css`
overflow-y: scroll;
border: 1px solid ${token.colorBorderSecondary};
border-left: 0;
padding-top: -1px;
`)}
>
<IconList/>
</Flexbox>
</>
)}
</Flexbox>
)
}
export default PickerPanel

31
src/components/icon/picker/SearchBar.tsx

@ -0,0 +1,31 @@
import { Input } from 'antd'
import { memo } from 'react'
import { css, cx, useToken } from '@/theme'
import { usePickerContext } from './context.tsx'
const SearchBar = () => {
const token = useToken()
const { state: { filterKeywords }, actions: { changeFilterKeywords } } = usePickerContext()
return (
<Input
placeholder={'输入图标名称进行搜索...'}
allowClear
value={filterKeywords}
onChange={(e) => {
changeFilterKeywords(e.target.value)
}}
bordered={false}
className={cx(css`
border-radius: 0;
border-bottom: 1px solid ${token.colorBorderSecondary} !important;
&:hover {
border-bottom: 1px solid ${token.colorBorder} !important;
}
`)}
/>
)
}
export default memo(SearchBar)

157
src/components/icon/picker/context.tsx

@ -0,0 +1,157 @@
import { IconUnit, ReactIcon } from '../types.ts'
import { antdIconList, parkIconList } from './icons.ts'
import { createContext, ProviderProps, useContext, useReducer, useMemo, useCallback } from 'react'
export interface State {
/**
* @title
*/
icon?: IconUnit;
/**
* @title
*/
showForm: boolean;
/**
* @title
*/
open: boolean;
/**
* @title
* @enum ['antd', 'park']
* @enumNames ['Antd', 'Park']
*/
panelTabKey: 'antd' | 'park';
/**
* @title
*/
filterKeywords?: string;
/**
* @title Antd
*/
antdIconList: ReactIcon[];
parkIconList: ReactIcon[];
// 外部状态
/**
* @title Icon
* @param icon - IconUnit
*/
onIconChange?: (icon: IconUnit) => void;
}
export type ContextValue = {
state: State;
actions: Actions;
}
export const initialState: State = {
icon: undefined,
showForm: false,
open: false,
panelTabKey: 'antd',
filterKeywords: '',
antdIconList: antdIconList,
parkIconList: parkIconList,
}
export const Context = createContext<ContextValue>({} as ContextValue)
// 定义动作类型
type Action =
| { type: 'resetIcon' }
| { type: 'togglePanel'; payload?: boolean }
| { type: 'selectIcon'; payload: IconUnit }
| { type: 'changePanelTab'; payload: 'antd' | 'park' }
| { type: 'changeFilterKeywords'; payload: string }
| { type: 'toggleForm'; payload?: boolean }
type Actions = {
resetIcon: () => void;
togglePanel: (open?: boolean) => void;
selectIcon: (icon: IconUnit) => void;
changePanelTab: (key: 'antd' | 'park') => void;
changeFilterKeywords: (keywords: string) => void;
toggleForm: (visible?: boolean) => void;
displayListSelector: () => ReactIcon[];
isEmptyIconAntdList: boolean;
isEmptyIconParkList: boolean;
}
// 创建reducer函数
const reducer = (state: State, action: Action) => {
switch (action.type) {
case 'resetIcon':
return { ...state, icon: undefined }
case 'togglePanel':
return { ...state, open: action.payload ?? !state.open }
case 'selectIcon':
return { ...state, icon: action.payload }
case 'changePanelTab':
return { ...state, panelTabKey: action.payload }
case 'changeFilterKeywords':
return { ...state, filterKeywords: action.payload }
case 'toggleForm':
return { ...state, showForm: action.payload ?? !state.showForm }
default:
return state
}
}
export const PickerContextProvider = ({ value: propValue, children }: ProviderProps<ContextValue>) => {
const [ state, dispatch ] = useReducer(reducer, {
...initialState,
...propValue
})
const resetIcon = useCallback(() => dispatch({ type: 'resetIcon' }), [])
const togglePanel = useCallback((open?: boolean) => dispatch({ type: 'togglePanel', payload: open }), [])
const selectIcon = useCallback((icon: IconUnit) => dispatch({ type: 'selectIcon', payload: icon }), [])
const changePanelTab = useCallback((key: 'antd' | 'park') => dispatch({
type: 'changePanelTab',
payload: key
}), [])
const changeFilterKeywords = useCallback((keywords: string) => dispatch({
type: 'changeFilterKeywords',
payload: keywords
}), [])
const toggleForm = useCallback((visible?: boolean) => dispatch({ type: 'toggleForm', payload: visible }), [])
const displayListSelector = useCallback(() => {
const list = state.panelTabKey === 'antd' ? state.antdIconList : state.parkIconList
const filterKeywords = state.filterKeywords!.toLowerCase()
return list.filter(icon => icon.componentName.toLowerCase().includes(filterKeywords))
}, [ state.panelTabKey, state.antdIconList, state.parkIconList, state.filterKeywords ])
const isEmptyIconAntdList = useMemo(() => !state.antdIconList.length, [ state.antdIconList ])
const isEmptyIconParkList = useMemo(() => !state.parkIconList.length, [ state.parkIconList ])
// 动作函数封装
const actions = {
resetIcon,
togglePanel,
selectIcon,
changePanelTab,
changeFilterKeywords,
toggleForm,
displayListSelector,
isEmptyIconAntdList,
isEmptyIconParkList,
}
return (
<Context.Provider value={{ state, actions }}>
{children}
</Context.Provider>
)
}
// eslint-disable-next-line react-refresh/only-export-components
export const usePickerContext = () => {
return useContext<ContextValue>(Context)
}

35
src/components/icon/picker/icons.ts

@ -0,0 +1,35 @@
import * as AntdIcon from '@ant-design/icons'
import ParkIcon , { ALL_ICON_KEYS } from '@icon-park/react/es/all'
import { IconUnit, ReactIcon } from '../types.ts'
const list = Object.keys(AntdIcon).filter(
(key) => key.endsWith('Outlined') || key.endsWith('Filled'),
)
const antdIconList: ReactIcon[] = list.map((componentName) => ({
type: 'antd',
componentName,
}))
const parkIconList: ReactIcon[] = ALL_ICON_KEYS.map((componentName) => ({
type: 'park',
componentName,
}))
export const getIconName = (icon: IconUnit) => {
switch (icon.type) {
case 'antd':
case 'park':
return icon.componentName;
case 'iconfont':
return icon.props.type;
}
};
export { antdIconList, parkIconList, ParkIcon, AntdIcon, ALL_ICON_KEYS }
export default AntdIcon

56
src/components/icon/picker/index.tsx

@ -1,12 +1,54 @@
import React from 'react'
import { Popover, PopoverProps } from 'antd'
import { FC, useEffect } from 'react'
import Display from './Display.tsx'
import PickerPanel from './PickerPanel'
import { PickerContextProvider, usePickerContext } from './context.tsx'
const Index = () => {
return (
<div>
interface PickerProps extends Partial<PopoverProps> {
value?: string,
onChange?: (value: string) => void,
}
const IconPicker: FC = (props: PickerProps) => {
const { state, actions: { selectIcon, togglePanel } } = usePickerContext()
const { value, onChange } = props
useEffect(() => {
if (onChange) {
onChange(state.icon ? `${state.icon.type}:${state.icon.componentName}` : '')
}
}, [ state.icon ])
useEffect(() => {
if (value) {
const [ type, componentName ] = value.split(':')
selectIcon({ type, componentName } as any)
}
}, [ value ])
return (
</div>
)
<Popover
placement={'bottomLeft'}
{...props}
onOpenChange={(e) => {
togglePanel(e)
}}
showArrow={false}
open={state.open}
trigger={'click'}
content={<PickerPanel/>}
>
<Display/>
</Popover>
)
}
export default Index
export default (props: PickerProps) => {
return <PickerContextProvider value={{} as any}>
<IconPicker {...props as any}/>
</PickerContextProvider>
}

20
src/components/icon/types.ts

@ -0,0 +1,20 @@
export interface ReactIcon {
type: 'antd' | 'park';
componentName: string;
props?: object;
}
export interface IconfontIcon {
type: 'iconfont';
componentName: string;
props: {
type: string;
};
scriptUrl?: string;
}
/**
*
*/
export type IconUnit = ReactIcon | IconfontIcon;
Loading…
Cancel
Save