dark
7 months ago
12 changed files with 570 additions and 81 deletions
-
59src/components/icon/action/style.ts
-
7src/components/icon/index.tsx
-
43src/components/icon/picker/Display.tsx
-
40src/components/icon/picker/IconList.tsx
-
51src/components/icon/picker/IconRender.tsx
-
78src/components/icon/picker/IconThumbnail.tsx
-
74src/components/icon/picker/PickerPanel.tsx
-
31src/components/icon/picker/SearchBar.tsx
-
157src/components/icon/picker/context.tsx
-
35src/components/icon/picker/icons.ts
-
56src/components/icon/picker/index.tsx
-
20src/components/icon/types.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 |
|||
}) |
@ -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 |
@ -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) |
@ -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 |
@ -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) |
@ -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 |
@ -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) |
@ -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) |
|||
} |
|||
|
|||
|
@ -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 |
@ -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> |
|||
} |
@ -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; |
Write
Preview
Loading…
Cancel
Save
Reference in new issue