From 36d894f228b446e78316dc98fd0a5ec8fc7aa78e Mon Sep 17 00:00:00 2001 From: dark Date: Sat, 20 Apr 2024 15:56:47 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0IconPicker=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/icon/action/style.ts | 59 +++++----- src/components/icon/index.tsx | 7 ++ src/components/icon/picker/Display.tsx | 43 ++++---- src/components/icon/picker/IconList.tsx | 40 +++++++ src/components/icon/picker/IconRender.tsx | 51 ++++----- src/components/icon/picker/IconThumbnail.tsx | 78 +++++++++++++ src/components/icon/picker/PickerPanel.tsx | 74 +++++++++++++ src/components/icon/picker/SearchBar.tsx | 31 ++++++ src/components/icon/picker/context.tsx | 157 +++++++++++++++++++++++++++ src/components/icon/picker/icons.ts | 35 ++++++ src/components/icon/picker/index.tsx | 56 ++++++++-- src/components/icon/types.ts | 20 ++++ 12 files changed, 570 insertions(+), 81 deletions(-) create mode 100644 src/components/icon/picker/IconList.tsx create mode 100644 src/components/icon/picker/IconThumbnail.tsx create mode 100644 src/components/icon/picker/PickerPanel.tsx create mode 100644 src/components/icon/picker/SearchBar.tsx create mode 100644 src/components/icon/picker/context.tsx create mode 100644 src/components/icon/picker/icons.ts create mode 100644 src/components/icon/types.ts diff --git a/src/components/icon/action/style.ts b/src/components/icon/action/style.ts index 2ea030c..69651d7 100644 --- a/src/components/icon/action/style.ts +++ b/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 }) \ No newline at end of file diff --git a/src/components/icon/index.tsx b/src/components/icon/index.tsx index 2e3abd3..3d72993 100644 --- a/src/components/icon/index.tsx +++ b/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) { const { type, ...other } = props + + if(type && ['antd', 'park'].includes(type as string)){ + return + } + const AntIcon = AntIcons[type as keyof typeof AntIcons] if (AntIcon) { return diff --git a/src/components/icon/picker/Display.tsx b/src/components/icon/picker/Display.tsx index 64bfe76..969295a 100644 --- a/src/components/icon/picker/Display.tsx +++ b/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 = ( -
- ) - return ( - } - /> - ) + const ctx = usePickerContext() + + const token = useToken() + const DefaultIcon = ( +
+ ) + + return ( ctx.actions.togglePanel()} + icon={!ctx.state.icon ? DefaultIcon : } /> + ) } export default Display \ No newline at end of file diff --git a/src/components/icon/picker/IconList.tsx b/src/components/icon/picker/IconList.tsx new file mode 100644 index 0000000..090ab9a --- /dev/null +++ b/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 ( +
+ {displayListSelector().map((icon) => ( + + ))} +
+ ) +} + +export default memo(IconList) \ No newline at end of file diff --git a/src/components/icon/picker/IconRender.tsx b/src/components/icon/picker/IconRender.tsx index 755cc3a..4e9a805 100644 --- a/src/components/icon/picker/IconRender.tsx +++ b/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 = memo( - ({ type, componentName, props, scriptUrl }) => { - switch (type) { - case 'antd': - const Icon = AntdIcons[componentName] - return - - case 'iconfont': - const Iconfont = AntdIcons.createFromIconfontCN({ - scriptUrl, - }) - return - } - }, + ({ type, componentName, props }) => { + switch (type) { + case 'antd': + // eslint-disable-next-line no-case-declarations + const AIcon = AntdIcon[componentName!] + return + + case 'park': + // eslint-disable-next-line no-case-declarations + if (ALL_ICON_KEYS.indexOf(componentName as IconType) < 0) { + return null + } + return + default: { + return null + } + } + }, ) const IconIRender = Render as IIconRender -IconIRender.registerCustomIcon = registerCustomIcon export default IconIRender \ No newline at end of file diff --git a/src/components/icon/picker/IconThumbnail.tsx b/src/components/icon/picker/IconThumbnail.tsx new file mode 100644 index 0000000..5773e50 --- /dev/null +++ b/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 = ({ icon }) => { + + const { actions: { togglePanel, selectIcon } } = usePickerContext() + + + const { styles, cx } = useStyles() + return ( + { + selectIcon(icon) + togglePanel() + }} + > + + + ) +} + +export default memo(IconThumbnail) \ No newline at end of file diff --git a/src/components/icon/picker/PickerPanel.tsx b/src/components/icon/picker/PickerPanel.tsx new file mode 100644 index 0000000..bcf2a18 --- /dev/null +++ b/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 ( + + {icon ? ( + + + +
{getIconName(icon)}
+
+ +
+ ) : undefined} + }, + { label: 'Icon Park', value: 'park' }, + ]} + value={panelTabKey} + onChange={(key) => { + changePanelTab(key as any) + }} + block + /> + + {(isEmptyIconAntdList || isEmptyIconParkList) ? ( + ( + + ) + ) : ( + <> + + + + + + )} +
+ ) +} + +export default PickerPanel \ No newline at end of file diff --git a/src/components/icon/picker/SearchBar.tsx b/src/components/icon/picker/SearchBar.tsx new file mode 100644 index 0000000..1abfde0 --- /dev/null +++ b/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 ( + { + 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) \ No newline at end of file diff --git a/src/components/icon/picker/context.tsx b/src/components/icon/picker/context.tsx new file mode 100644 index 0000000..fc6ca7f --- /dev/null +++ b/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({} 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) => { + + 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 ( + + {children} + + ) +} + +// eslint-disable-next-line react-refresh/only-export-components +export const usePickerContext = () => { + return useContext(Context) +} + + diff --git a/src/components/icon/picker/icons.ts b/src/components/icon/picker/icons.ts new file mode 100644 index 0000000..d3311ee --- /dev/null +++ b/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 \ No newline at end of file diff --git a/src/components/icon/picker/index.tsx b/src/components/icon/picker/index.tsx index cff2cf8..7975428 100644 --- a/src/components/icon/picker/index.tsx +++ b/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 ( -
+interface PickerProps extends Partial { + 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 ( -
- ) + { + togglePanel(e) + }} + showArrow={false} + open={state.open} + trigger={'click'} + content={} + > + + + ) } -export default Index \ No newline at end of file +export default (props: PickerProps) => { + return + + +} \ No newline at end of file diff --git a/src/components/icon/types.ts b/src/components/icon/types.ts new file mode 100644 index 0000000..d23e94d --- /dev/null +++ b/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; \ No newline at end of file