New file |
| | |
| | | import * as React from 'react'; |
| | | import CopyableIcon from './CopyableIcon'; |
| | | import type { ThemeType } from './index'; |
| | | import type { CategoriesKeys } from './fields'; |
| | | import { useIntl } from '@umijs/max'; |
| | | import styles from './style.less'; |
| | | |
| | | interface CategoryProps { |
| | | title: CategoriesKeys; |
| | | icons: string[]; |
| | | theme: ThemeType; |
| | | newIcons: string[]; |
| | | onSelect: (type: string, name: string) => any; |
| | | } |
| | | |
| | | const Category: React.FC<CategoryProps> = props => { |
| | | |
| | | const { icons, title, newIcons, theme } = props; |
| | | const intl = useIntl(); |
| | | const [justCopied, setJustCopied] = React.useState<string | null>(null); |
| | | const copyId = React.useRef<NodeJS.Timeout | null>(null); |
| | | const onSelect = React.useCallback((type: string, text: string) => { |
| | | const { onSelect } = props; |
| | | if (onSelect) { |
| | | onSelect(type, text); |
| | | } |
| | | setJustCopied(type); |
| | | copyId.current = setTimeout(() => { |
| | | setJustCopied(null); |
| | | }, 2000); |
| | | }, []); |
| | | React.useEffect( |
| | | () => () => { |
| | | if (copyId.current) { |
| | | clearTimeout(copyId.current); |
| | | } |
| | | }, |
| | | [], |
| | | ); |
| | | |
| | | return ( |
| | | <div> |
| | | <h4>{intl.formatMessage({ |
| | | id: `app.docs.components.icon.category.${title}`, |
| | | defaultMessage: '信息', |
| | | })}</h4> |
| | | <ul className={styles.anticonsList}> |
| | | {icons.map(name => ( |
| | | <CopyableIcon |
| | | key={name} |
| | | name={name} |
| | | theme={theme} |
| | | isNew={newIcons.includes(name)} |
| | | justCopied={justCopied} |
| | | onSelect={onSelect} |
| | | /> |
| | | ))} |
| | | </ul> |
| | | </div> |
| | | ); |
| | | }; |
| | | |
| | | export default Category; |
New file |
| | |
| | | import * as React from 'react'; |
| | | import { Tooltip } from 'antd'; |
| | | import classNames from 'classnames'; |
| | | import * as AntdIcons from '@ant-design/icons'; |
| | | import type { ThemeType } from './index'; |
| | | import styles from './style.less'; |
| | | |
| | | const allIcons: { |
| | | [key: string]: any; |
| | | } = AntdIcons; |
| | | |
| | | export interface CopyableIconProps { |
| | | name: string; |
| | | isNew: boolean; |
| | | theme: ThemeType; |
| | | justCopied: string | null; |
| | | onSelect: (type: string, text: string) => any; |
| | | } |
| | | |
| | | const CopyableIcon: React.FC<CopyableIconProps> = ({ |
| | | name, |
| | | justCopied, |
| | | onSelect, |
| | | theme, |
| | | }) => { |
| | | const className = classNames({ |
| | | copied: justCopied === name, |
| | | [theme]: !!theme, |
| | | }); |
| | | return ( |
| | | <li className={className} |
| | | onClick={() => { |
| | | if (onSelect) { |
| | | onSelect(theme, name); |
| | | } |
| | | }}> |
| | | <Tooltip title={name}> |
| | | {React.createElement(allIcons[name], { className: styles.anticon })} |
| | | </Tooltip> |
| | | {/* <span className={styles.anticonClass}> |
| | | <Badge dot={isNew}>{name}</Badge> |
| | | </span> */} |
| | | </li> |
| | | ); |
| | | }; |
| | | |
| | | export default CopyableIcon; |
New file |
| | |
| | | import React, { useCallback, useEffect, useState } from 'react'; |
| | | import { Upload, Tooltip, Popover, Modal, Progress, Spin, Result } from 'antd'; |
| | | import * as AntdIcons from '@ant-design/icons'; |
| | | import { useIntl } from '@umijs/max'; |
| | | import './style.less'; |
| | | |
| | | const allIcons: { [key: string]: any } = AntdIcons; |
| | | |
| | | const { Dragger } = Upload; |
| | | interface AntdIconClassifier { |
| | | load: () => void; |
| | | predict: (imgEl: HTMLImageElement) => void; |
| | | } |
| | | declare global { |
| | | interface Window { |
| | | antdIconClassifier: AntdIconClassifier; |
| | | } |
| | | } |
| | | |
| | | interface PicSearcherState { |
| | | loading: boolean; |
| | | modalOpen: boolean; |
| | | popoverVisible: boolean; |
| | | icons: iconObject[]; |
| | | fileList: any[]; |
| | | error: boolean; |
| | | modelLoaded: boolean; |
| | | } |
| | | |
| | | interface iconObject { |
| | | type: string; |
| | | score: number; |
| | | } |
| | | |
| | | const PicSearcher: React.FC = () => { |
| | | const intl = useIntl(); |
| | | const {formatMessage} = intl; |
| | | const [state, setState] = useState<PicSearcherState>({ |
| | | loading: false, |
| | | modalOpen: false, |
| | | popoverVisible: false, |
| | | icons: [], |
| | | fileList: [], |
| | | error: false, |
| | | modelLoaded: false, |
| | | }); |
| | | const predict = (imgEl: HTMLImageElement) => { |
| | | try { |
| | | let icons: any[] = window.antdIconClassifier.predict(imgEl); |
| | | if (gtag && icons.length) { |
| | | gtag('event', 'icon', { |
| | | event_category: 'search-by-image', |
| | | event_label: icons[0].className, |
| | | }); |
| | | } |
| | | icons = icons.map(i => ({ score: i.score, type: i.className.replace(/\s/g, '-') })); |
| | | setState(prev => ({ ...prev, loading: false, error: false, icons })); |
| | | } catch { |
| | | setState(prev => ({ ...prev, loading: false, error: true })); |
| | | } |
| | | }; |
| | | // eslint-disable-next-line class-methods-use-this |
| | | const toImage = (url: string) => |
| | | new Promise(resolve => { |
| | | const img = new Image(); |
| | | img.setAttribute('crossOrigin', 'anonymous'); |
| | | img.src = url; |
| | | img.onload = () => { |
| | | resolve(img); |
| | | }; |
| | | }); |
| | | |
| | | const uploadFile = useCallback((file: File) => { |
| | | setState(prev => ({ ...prev, loading: true })); |
| | | const reader = new FileReader(); |
| | | reader.onload = () => { |
| | | toImage(reader.result as string).then(predict); |
| | | setState(prev => ({ |
| | | ...prev, |
| | | fileList: [{ uid: 1, name: file.name, status: 'done', url: reader.result }], |
| | | })); |
| | | }; |
| | | reader.readAsDataURL(file); |
| | | }, []); |
| | | |
| | | const onPaste = useCallback((event: ClipboardEvent) => { |
| | | const items = event.clipboardData && event.clipboardData.items; |
| | | let file = null; |
| | | if (items && items.length) { |
| | | for (let i = 0; i < items.length; i++) { |
| | | if (items[i].type.includes('image')) { |
| | | file = items[i].getAsFile(); |
| | | break; |
| | | } |
| | | } |
| | | } |
| | | if (file) { |
| | | uploadFile(file); |
| | | } |
| | | }, []); |
| | | const toggleModal = useCallback(() => { |
| | | setState(prev => ({ |
| | | ...prev, |
| | | modalOpen: !prev.modalOpen, |
| | | popoverVisible: false, |
| | | fileList: [], |
| | | icons: [], |
| | | })); |
| | | if (!localStorage.getItem('disableIconTip')) { |
| | | localStorage.setItem('disableIconTip', 'true'); |
| | | } |
| | | }, []); |
| | | |
| | | useEffect(() => { |
| | | const script = document.createElement('script'); |
| | | script.onload = async () => { |
| | | await window.antdIconClassifier.load(); |
| | | setState(prev => ({ ...prev, modelLoaded: true })); |
| | | document.addEventListener('paste', onPaste); |
| | | }; |
| | | script.src = 'https://cdn.jsdelivr.net/gh/lewis617/antd-icon-classifier@0.0/dist/main.js'; |
| | | document.head.appendChild(script); |
| | | setState(prev => ({ ...prev, popoverVisible: !localStorage.getItem('disableIconTip') })); |
| | | return () => { |
| | | document.removeEventListener('paste', onPaste); |
| | | }; |
| | | }, []); |
| | | |
| | | return ( |
| | | <div className="iconPicSearcher"> |
| | | <Popover |
| | | content={formatMessage({id: 'app.docs.components.icon.pic-searcher.intro'})} |
| | | open={state.popoverVisible} |
| | | > |
| | | <AntdIcons.CameraOutlined className="icon-pic-btn" onClick={toggleModal} /> |
| | | </Popover> |
| | | <Modal |
| | | title={intl.formatMessage({ |
| | | id: 'app.docs.components.icon.pic-searcher.title', |
| | | defaultMessage: '信息', |
| | | })} |
| | | open={state.modalOpen} |
| | | onCancel={toggleModal} |
| | | footer={null} |
| | | > |
| | | {state.modelLoaded || ( |
| | | <Spin |
| | | spinning={!state.modelLoaded} |
| | | tip={formatMessage({ |
| | | id: 'app.docs.components.icon.pic-searcher.modelloading', |
| | | |
| | | })} |
| | | > |
| | | <div style={{ height: 100 }} /> |
| | | </Spin> |
| | | )} |
| | | {state.modelLoaded && ( |
| | | <Dragger |
| | | accept="image/jpeg, image/png" |
| | | listType="picture" |
| | | customRequest={o => uploadFile(o.file as File)} |
| | | fileList={state.fileList} |
| | | showUploadList={{ showPreviewIcon: false, showRemoveIcon: false }} |
| | | > |
| | | <p className="ant-upload-drag-icon"> |
| | | <AntdIcons.InboxOutlined /> |
| | | </p> |
| | | <p className="ant-upload-text"> |
| | | {formatMessage({id: 'app.docs.components.icon.pic-searcher.upload-text'})} |
| | | </p> |
| | | <p className="ant-upload-hint"> |
| | | {formatMessage({id: 'app.docs.components.icon.pic-searcher.upload-hint'})} |
| | | </p> |
| | | </Dragger> |
| | | )} |
| | | <Spin |
| | | spinning={state.loading} |
| | | tip={formatMessage({id: 'app.docs.components.icon.pic-searcher.matching'})} |
| | | > |
| | | <div className="icon-pic-search-result"> |
| | | {state.icons.length > 0 && ( |
| | | <div className="result-tip"> |
| | | {formatMessage({id: 'app.docs.components.icon.pic-searcher.result-tip'})} |
| | | </div> |
| | | )} |
| | | <table> |
| | | {state.icons.length > 0 && ( |
| | | <thead> |
| | | <tr> |
| | | <th className="col-icon"> |
| | | {formatMessage({id: 'app.docs.components.icon.pic-searcher.th-icon'})} |
| | | </th> |
| | | <th>{formatMessage({id: 'app.docs.components.icon.pic-searcher.th-score'})}</th> |
| | | </tr> |
| | | </thead> |
| | | )} |
| | | <tbody> |
| | | {state.icons.map(icon => { |
| | | const { type } = icon; |
| | | const iconName = `${type |
| | | .split('-') |
| | | .map(str => `${str[0].toUpperCase()}${str.slice(1)}`) |
| | | .join('')}Outlined`; |
| | | return ( |
| | | <tr key={iconName}> |
| | | <td className="col-icon"> |
| | | <Tooltip title={icon.type} placement="right"> |
| | | {React.createElement(allIcons[iconName])} |
| | | </Tooltip> |
| | | </td> |
| | | <td> |
| | | <Progress percent={Math.ceil(icon.score * 100)} /> |
| | | </td> |
| | | </tr> |
| | | ); |
| | | })} |
| | | </tbody> |
| | | </table> |
| | | {state.error && ( |
| | | <Result |
| | | status="500" |
| | | title="503" |
| | | subTitle={formatMessage({id: 'app.docs.components.icon.pic-searcher.server-error'})} |
| | | /> |
| | | )} |
| | | </div> |
| | | </Spin> |
| | | </Modal> |
| | | </div> |
| | | ); |
| | | }; |
| | | |
| | | export default PicSearcher; |
New file |
| | |
| | | import * as AntdIcons from '@ant-design/icons/lib/icons'; |
| | | |
| | | const all = Object.keys(AntdIcons) |
| | | .map(n => n.replace(/(Outlined|Filled|TwoTone)$/, '')) |
| | | .filter((n, i, arr) => arr.indexOf(n) === i); |
| | | |
| | | const direction = [ |
| | | 'StepBackward', |
| | | 'StepForward', |
| | | 'FastBackward', |
| | | 'FastForward', |
| | | 'Shrink', |
| | | 'ArrowsAlt', |
| | | 'Down', |
| | | 'Up', |
| | | 'Left', |
| | | 'Right', |
| | | 'CaretUp', |
| | | 'CaretDown', |
| | | 'CaretLeft', |
| | | 'CaretRight', |
| | | 'UpCircle', |
| | | 'DownCircle', |
| | | 'LeftCircle', |
| | | 'RightCircle', |
| | | 'DoubleRight', |
| | | 'DoubleLeft', |
| | | 'VerticalLeft', |
| | | 'VerticalRight', |
| | | 'VerticalAlignTop', |
| | | 'VerticalAlignMiddle', |
| | | 'VerticalAlignBottom', |
| | | 'Forward', |
| | | 'Backward', |
| | | 'Rollback', |
| | | 'Enter', |
| | | 'Retweet', |
| | | 'Swap', |
| | | 'SwapLeft', |
| | | 'SwapRight', |
| | | 'ArrowUp', |
| | | 'ArrowDown', |
| | | 'ArrowLeft', |
| | | 'ArrowRight', |
| | | 'PlayCircle', |
| | | 'UpSquare', |
| | | 'DownSquare', |
| | | 'LeftSquare', |
| | | 'RightSquare', |
| | | 'Login', |
| | | 'Logout', |
| | | 'MenuFold', |
| | | 'MenuUnfold', |
| | | 'BorderBottom', |
| | | 'BorderHorizontal', |
| | | 'BorderInner', |
| | | 'BorderOuter', |
| | | 'BorderLeft', |
| | | 'BorderRight', |
| | | 'BorderTop', |
| | | 'BorderVerticle', |
| | | 'PicCenter', |
| | | 'PicLeft', |
| | | 'PicRight', |
| | | 'RadiusBottomleft', |
| | | 'RadiusBottomright', |
| | | 'RadiusUpleft', |
| | | 'RadiusUpright', |
| | | 'Fullscreen', |
| | | 'FullscreenExit', |
| | | ]; |
| | | |
| | | const suggestion = [ |
| | | 'Question', |
| | | 'QuestionCircle', |
| | | 'Plus', |
| | | 'PlusCircle', |
| | | 'Pause', |
| | | 'PauseCircle', |
| | | 'Minus', |
| | | 'MinusCircle', |
| | | 'PlusSquare', |
| | | 'MinusSquare', |
| | | 'Info', |
| | | 'InfoCircle', |
| | | 'Exclamation', |
| | | 'ExclamationCircle', |
| | | 'Close', |
| | | 'CloseCircle', |
| | | 'CloseSquare', |
| | | 'Check', |
| | | 'CheckCircle', |
| | | 'CheckSquare', |
| | | 'ClockCircle', |
| | | 'Warning', |
| | | 'IssuesClose', |
| | | 'Stop', |
| | | ]; |
| | | |
| | | const editor = [ |
| | | 'Edit', |
| | | 'Form', |
| | | 'Copy', |
| | | 'Scissor', |
| | | 'Delete', |
| | | 'Snippets', |
| | | 'Diff', |
| | | 'Highlight', |
| | | 'AlignCenter', |
| | | 'AlignLeft', |
| | | 'AlignRight', |
| | | 'BgColors', |
| | | 'Bold', |
| | | 'Italic', |
| | | 'Underline', |
| | | 'Strikethrough', |
| | | 'Redo', |
| | | 'Undo', |
| | | 'ZoomIn', |
| | | 'ZoomOut', |
| | | 'FontColors', |
| | | 'FontSize', |
| | | 'LineHeight', |
| | | 'Dash', |
| | | 'SmallDash', |
| | | 'SortAscending', |
| | | 'SortDescending', |
| | | 'Drag', |
| | | 'OrderedList', |
| | | 'UnorderedList', |
| | | 'RadiusSetting', |
| | | 'ColumnWidth', |
| | | 'ColumnHeight', |
| | | ]; |
| | | |
| | | const data = [ |
| | | 'AreaChart', |
| | | 'PieChart', |
| | | 'BarChart', |
| | | 'DotChart', |
| | | 'LineChart', |
| | | 'RadarChart', |
| | | 'HeatMap', |
| | | 'Fall', |
| | | 'Rise', |
| | | 'Stock', |
| | | 'BoxPlot', |
| | | 'Fund', |
| | | 'Sliders', |
| | | ]; |
| | | |
| | | const logo = [ |
| | | 'Android', |
| | | 'Apple', |
| | | 'Windows', |
| | | 'Ie', |
| | | 'Chrome', |
| | | 'Github', |
| | | 'Aliwangwang', |
| | | 'Dingding', |
| | | 'WeiboSquare', |
| | | 'WeiboCircle', |
| | | 'TaobaoCircle', |
| | | 'Html5', |
| | | 'Weibo', |
| | | 'Twitter', |
| | | 'Wechat', |
| | | 'Youtube', |
| | | 'AlipayCircle', |
| | | 'Taobao', |
| | | 'Skype', |
| | | 'Qq', |
| | | 'MediumWorkmark', |
| | | 'Gitlab', |
| | | 'Medium', |
| | | 'Linkedin', |
| | | 'GooglePlus', |
| | | 'Dropbox', |
| | | 'Facebook', |
| | | 'Codepen', |
| | | 'CodeSandbox', |
| | | 'CodeSandboxCircle', |
| | | 'Amazon', |
| | | 'Google', |
| | | 'CodepenCircle', |
| | | 'Alipay', |
| | | 'AntDesign', |
| | | 'AntCloud', |
| | | 'Aliyun', |
| | | 'Zhihu', |
| | | 'Slack', |
| | | 'SlackSquare', |
| | | 'Behance', |
| | | 'BehanceSquare', |
| | | 'Dribbble', |
| | | 'DribbbleSquare', |
| | | 'Instagram', |
| | | 'Yuque', |
| | | 'Alibaba', |
| | | 'Yahoo', |
| | | 'Reddit', |
| | | 'Sketch', |
| | | 'WhatsApp', |
| | | 'Dingtalk', |
| | | ]; |
| | | |
| | | const datum = [...direction, ...suggestion, ...editor, ...data, ...logo]; |
| | | |
| | | const other = all.filter(n => !datum.includes(n)); |
| | | |
| | | export const categories = { |
| | | direction, |
| | | suggestion, |
| | | editor, |
| | | data, |
| | | logo, |
| | | other, |
| | | }; |
| | | |
| | | export default categories; |
| | | |
| | | export type Categories = typeof categories; |
| | | export type CategoriesKeys = keyof Categories; |
New file |
| | |
| | | import * as React from 'react'; |
| | | import Icon, * as AntdIcons from '@ant-design/icons'; |
| | | import { Radio, Input, Empty } from 'antd'; |
| | | import type { RadioChangeEvent } from 'antd/es/radio/interface'; |
| | | import debounce from 'lodash/debounce'; |
| | | import Category from './Category'; |
| | | import IconPicSearcher from './IconPicSearcher'; |
| | | import { FilledIcon, OutlinedIcon, TwoToneIcon } from './themeIcons'; |
| | | import type { CategoriesKeys } from './fields'; |
| | | import { categories } from './fields'; |
| | | // import { useIntl } from '@umijs/max'; |
| | | |
| | | export enum ThemeType { |
| | | Filled = 'Filled', |
| | | Outlined = 'Outlined', |
| | | TwoTone = 'TwoTone', |
| | | } |
| | | |
| | | const allIcons: { [key: string]: any } = AntdIcons; |
| | | |
| | | interface IconSelectorProps { |
| | | //intl: any; |
| | | onSelect: any; |
| | | } |
| | | |
| | | interface IconSelectorState { |
| | | theme: ThemeType; |
| | | searchKey: string; |
| | | } |
| | | |
| | | const IconSelector: React.FC<IconSelectorProps> = (props) => { |
| | | // const intl = useIntl(); |
| | | // const { messages } = intl; |
| | | const { onSelect } = props; |
| | | const [displayState, setDisplayState] = React.useState<IconSelectorState>({ |
| | | theme: ThemeType.Outlined, |
| | | searchKey: '', |
| | | }); |
| | | |
| | | const newIconNames: string[] = []; |
| | | |
| | | const handleSearchIcon = React.useCallback( |
| | | debounce((searchKey: string) => { |
| | | setDisplayState(prevState => ({ ...prevState, searchKey })); |
| | | }), |
| | | [], |
| | | ); |
| | | |
| | | const handleChangeTheme = React.useCallback((e: RadioChangeEvent) => { |
| | | setDisplayState(prevState => ({ ...prevState, theme: e.target.value as ThemeType })); |
| | | }, []); |
| | | |
| | | const renderCategories = React.useMemo<React.ReactNode | React.ReactNode[]>(() => { |
| | | const { searchKey = '', theme } = displayState; |
| | | |
| | | const categoriesResult = Object.keys(categories) |
| | | .map((key: CategoriesKeys) => { |
| | | let iconList = categories[key]; |
| | | if (searchKey) { |
| | | const matchKey = searchKey |
| | | // eslint-disable-next-line prefer-regex-literals |
| | | .replace(new RegExp(`^<([a-zA-Z]*)\\s/>$`, 'gi'), (_, name) => name) |
| | | .replace(/(Filled|Outlined|TwoTone)$/, '') |
| | | .toLowerCase(); |
| | | iconList = iconList.filter((iconName:string) => iconName.toLowerCase().includes(matchKey)); |
| | | } |
| | | |
| | | // CopyrightCircle is same as Copyright, don't show it |
| | | iconList = iconList.filter((icon:string) => icon !== 'CopyrightCircle'); |
| | | |
| | | return { |
| | | category: key, |
| | | icons: iconList.map((iconName:string) => iconName + theme).filter((iconName:string) => allIcons[iconName]), |
| | | }; |
| | | }) |
| | | .filter(({ icons }) => !!icons.length) |
| | | .map(({ category, icons }) => ( |
| | | <Category |
| | | key={category} |
| | | title={category as CategoriesKeys} |
| | | theme={theme} |
| | | icons={icons} |
| | | newIcons={newIconNames} |
| | | onSelect={(type, name) => { |
| | | if (onSelect) { |
| | | onSelect(name, allIcons[name]); |
| | | } |
| | | }} |
| | | /> |
| | | )); |
| | | return categoriesResult.length === 0 ? <Empty style={{ margin: '2em 0' }} /> : categoriesResult; |
| | | }, [displayState.searchKey, displayState.theme]); |
| | | return ( |
| | | <> |
| | | <div style={{ display: 'flex', justifyContent: 'space-between' }}> |
| | | <Radio.Group |
| | | value={displayState.theme} |
| | | onChange={handleChangeTheme} |
| | | size="large" |
| | | optionType="button" |
| | | buttonStyle="solid" |
| | | options={[ |
| | | { |
| | | label: <Icon component={OutlinedIcon} />, |
| | | value: ThemeType.Outlined |
| | | }, |
| | | { |
| | | label: <Icon component={FilledIcon} />, |
| | | value: ThemeType.Filled |
| | | }, |
| | | { |
| | | label: <Icon component={TwoToneIcon} />, |
| | | value: ThemeType.TwoTone |
| | | }, |
| | | ]} |
| | | > |
| | | {/* <Radio.Button value={ThemeType.Outlined}> |
| | | <Icon component={OutlinedIcon} /> {messages['app.docs.components.icon.outlined']} |
| | | </Radio.Button> |
| | | <Radio.Button value={ThemeType.Filled}> |
| | | <Icon component={FilledIcon} /> {messages['app.docs.components.icon.filled']} |
| | | </Radio.Button> |
| | | <Radio.Button value={ThemeType.TwoTone}> |
| | | <Icon component={TwoToneIcon} /> {messages['app.docs.components.icon.two-tone']} |
| | | </Radio.Button> */} |
| | | </Radio.Group> |
| | | <Input.Search |
| | | // placeholder={messages['app.docs.components.icon.search.placeholder']} |
| | | style={{ margin: '0 10px', flex: 1 }} |
| | | allowClear |
| | | onChange={e => handleSearchIcon(e.currentTarget.value)} |
| | | size="large" |
| | | autoFocus |
| | | suffix={<IconPicSearcher />} |
| | | /> |
| | | </div> |
| | | {renderCategories} |
| | | </> |
| | | ); |
| | | }; |
| | | |
| | | export default IconSelector |
New file |
| | |
| | | .iconPicSearcher { |
| | | display: inline-block; |
| | | margin: 0 8px; |
| | | |
| | | .icon-pic-btn { |
| | | color: @text-color-secondary; |
| | | cursor: pointer; |
| | | transition: all 0.3s; |
| | | |
| | | &:hover { |
| | | color: @input-icon-hover-color; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .icon-pic-preview { |
| | | width: 30px; |
| | | height: 30px; |
| | | margin-top: 10px; |
| | | padding: 8px; |
| | | text-align: center; |
| | | border: 1px solid @border-color-base; |
| | | border-radius: 4px; |
| | | |
| | | > img { |
| | | max-width: 50px; |
| | | max-height: 50px; |
| | | } |
| | | } |
| | | |
| | | .icon-pic-search-result { |
| | | min-height: 50px; |
| | | padding: 0 10px; |
| | | |
| | | > .result-tip { |
| | | padding: 10px 0; |
| | | color: @text-color-secondary; |
| | | } |
| | | |
| | | > table { |
| | | width: 100%; |
| | | |
| | | .col-icon { |
| | | width: 80px; |
| | | padding: 10px 0; |
| | | |
| | | > .anticon { |
| | | font-size: 30px; |
| | | |
| | | :hover { |
| | | color: @link-hover-color; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | ul.anticonsList { |
| | | margin: 2px 0; |
| | | overflow: hidden; |
| | | direction: ltr; |
| | | list-style: none; |
| | | |
| | | li { |
| | | position: relative; |
| | | float: left; |
| | | width: 48px; |
| | | height: 48px; |
| | | margin: 3px 0; |
| | | padding: 2px 0 0; |
| | | overflow: hidden; |
| | | color: #555; |
| | | text-align: center; |
| | | list-style: none; |
| | | background-color: inherit; |
| | | border-radius: 4px; |
| | | cursor: pointer; |
| | | transition: color 0.3s ease-in-out, background-color 0.3s ease-in-out; |
| | | |
| | | .rtl & { |
| | | margin: 3px 0; |
| | | padding: 2px 0 0; |
| | | } |
| | | |
| | | .anticon { |
| | | margin: 4px 0 2px; |
| | | font-size: 24px; |
| | | transition: transform 0.3s ease-in-out; |
| | | will-change: transform; |
| | | } |
| | | |
| | | .anticonClass { |
| | | display: block; |
| | | font-family: 'Lucida Console', Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; |
| | | white-space: nowrap; |
| | | text-align: center; |
| | | transform: scale(0.83); |
| | | |
| | | .ant-badge { |
| | | transition: color 0.3s ease-in-out; |
| | | } |
| | | } |
| | | |
| | | &:hover { |
| | | color: #fff; |
| | | background-color: @primary-color; |
| | | |
| | | .anticon { |
| | | transform: scale(1.4); |
| | | } |
| | | |
| | | .ant-badge { |
| | | color: #fff; |
| | | } |
| | | } |
| | | |
| | | &.TwoTone:hover { |
| | | background-color: #8ecafe; |
| | | } |
| | | |
| | | &.copied:hover { |
| | | color: rgba(255, 255, 255, 0.2); |
| | | } |
| | | |
| | | &.copied::after { |
| | | top: -2px; |
| | | opacity: 1; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .copied-code { |
| | | padding: 2px 4px; |
| | | font-size: 12px; |
| | | background: #f5f5f5; |
| | | border-radius: 2px; |
| | | } |
New file |
| | |
| | | import * as React from 'react'; |
| | | |
| | | |
| | | export const FilledIcon: React.FC = props => { |
| | | const path = |
| | | 'M864 64H160C107 64 64 107 64 160v' + |
| | | '704c0 53 43 96 96 96h704c53 0 96-43 96-96V16' + |
| | | '0c0-53-43-96-96-96z'; |
| | | return ( |
| | | <svg {...props} viewBox="0 0 1024 1024"> |
| | | <path d={path} /> |
| | | </svg> |
| | | ); |
| | | }; |
| | | |
| | | export const OutlinedIcon: React.FC = props => { |
| | | const path = |
| | | 'M864 64H160C107 64 64 107 64 160v7' + |
| | | '04c0 53 43 96 96 96h704c53 0 96-43 96-96V160c' + |
| | | '0-53-43-96-96-96z m-12 800H172c-6.6 0-12-5.4-' + |
| | | '12-12V172c0-6.6 5.4-12 12-12h680c6.6 0 12 5.4' + |
| | | ' 12 12v680c0 6.6-5.4 12-12 12z'; |
| | | return ( |
| | | <svg {...props} viewBox="0 0 1024 1024"> |
| | | <path d={path} /> |
| | | </svg> |
| | | ); |
| | | }; |
| | | |
| | | export const TwoToneIcon: React.FC = props => { |
| | | const path = |
| | | 'M16 512c0 273.932 222.066 496 496 49' + |
| | | '6s496-222.068 496-496S785.932 16 512 16 16 238.' + |
| | | '066 16 512z m496 368V144c203.41 0 368 164.622 3' + |
| | | '68 368 0 203.41-164.622 368-368 368z'; |
| | | return ( |
| | | <svg {...props} viewBox="0 0 1024 1024"> |
| | | <path d={path} /> |
| | | </svg> |
| | | ); |
| | | }; |
| | |
| | | ProFormDateTimePicker, |
| | | ProFormTreeSelect |
| | | } from '@ant-design/pro-components'; |
| | | import { Form, Modal } from 'antd'; |
| | | import { Form, Modal, Col } from 'antd'; |
| | | import moment from 'moment'; |
| | | import Http from '@/utils/http'; |
| | | import { createIcon } from '@/utils/icon-util' |
| | | import IconSelector from '@/components/IconSelector'; |
| | | |
| | | const Edit = (props) => { |
| | | const [menuType, setMenuType] = useState(0); |
| | | const [menuIconName, setMenuIconName] = useState(); |
| | | const [iconSelectorOpen, setIconSelectorOpen] = useState(false); |
| | | |
| | | const [form] = Form.useForm(); |
| | | const { } = props; |
| | | |
| | |
| | | } |
| | | |
| | | const handleFinish = async (values) => { |
| | | props.onSubmit({ ...values }); |
| | | console.log(values); |
| | | } |
| | | |
| | | return ( |
| | |
| | | <ProFormSelect |
| | | name="type" |
| | | label="类型" |
| | | colProps={{ md: 12, xl: 12 }} |
| | | colProps={{ md: 10, xl: 610 }} |
| | | placeholder="请选择" |
| | | options={[ |
| | | { label: '菜单', value: 0 }, |
| | |
| | | colProps={{ md: 12, xl: 12 }} |
| | | placeholder="请输入" |
| | | /> |
| | | <ProFormText |
| | | <ProFormSelect |
| | | name="icon" |
| | | label="菜单图标" |
| | | hidden={menuType !== 0} |
| | | colProps={{ md: 12, xl: 12 }} |
| | | placeholder="请输入" |
| | | valueEnum={{}} |
| | | addonBefore={createIcon(menuIconName)} |
| | | fieldProps={{ |
| | | onClick: () => { |
| | | setIconSelectorOpen(true); |
| | | }, |
| | | }} |
| | | /> |
| | | </ProForm.Group> |
| | | <ProForm.Group> |
| | |
| | | /> |
| | | </ProForm.Group> |
| | | </ProForm> |
| | | </Modal> |
| | | <Modal |
| | | width={800} |
| | | open={iconSelectorOpen} |
| | | onCancel={() => { |
| | | setIconSelectorOpen(false); |
| | | }} |
| | | footer={null} |
| | | > |
| | | <IconSelector |
| | | onSelect={(name) => { |
| | | form.setFieldsValue({ icon: name }); |
| | | setMenuIconName(name); |
| | | setIconSelectorOpen(false); |
| | | }} |
| | | /> |
| | | </Modal> |
| | | </Modal > |
| | | </> |
| | | ) |
| | | } |