dark
7 months ago
12 changed files with 570 additions and 81 deletions
-
5src/components/icon/action/style.ts
-
7src/components/icon/index.tsx
-
19src/components/icon/picker/Display.tsx
-
40src/components/icon/picker/IconList.tsx
-
37src/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
-
52src/components/icon/picker/index.tsx
-
20src/components/icon/types.ts
@ -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) |
@ -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 = () => { |
|
||||
|
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 ( |
return ( |
||||
<div> |
|
||||
|
|
||||
</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