init
Some checks failed
Node CI / build (14.x, macOS-latest) (push) Has been cancelled
Node CI / build (14.x, ubuntu-latest) (push) Has been cancelled
Node CI / build (14.x, windows-latest) (push) Has been cancelled
Node CI / build (16.x, macOS-latest) (push) Has been cancelled
Node CI / build (16.x, ubuntu-latest) (push) Has been cancelled
Node CI / build (16.x, windows-latest) (push) Has been cancelled
coverage CI / build (push) Has been cancelled
Node pnpm CI / build (16.x, macOS-latest) (push) Has been cancelled
Node pnpm CI / build (16.x, ubuntu-latest) (push) Has been cancelled
Node pnpm CI / build (16.x, windows-latest) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled

This commit is contained in:
2025-10-24 09:40:46 +08:00
commit b1781d9aef
313 changed files with 33845 additions and 0 deletions

View File

@@ -0,0 +1,63 @@
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;

View File

@@ -0,0 +1,47 @@
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;

View File

@@ -0,0 +1,233 @@
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;

View File

@@ -0,0 +1,223 @@
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;

View File

@@ -0,0 +1,142 @@
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

View File

@@ -0,0 +1,137 @@
.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;
}

View File

@@ -0,0 +1,41 @@
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>
);
};