You can not select more than 25 topics Topics must start with a chinese character,a letter or number, can include dashes ('-') and can be up to 35 characters long.

IconPicSearcher.tsx 7.4 kB

2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  1. import KFModal from '@/components/KFModal';
  2. import * as AntdIcons from '@ant-design/icons';
  3. import { useIntl } from '@umijs/max';
  4. import { Popover, Progress, Result, Spin, Tooltip, Upload } from 'antd';
  5. import React, { useCallback, useEffect, useState } from 'react';
  6. import './style.less';
  7. const allIcons: { [key: string]: any } = AntdIcons;
  8. const { Dragger } = Upload;
  9. interface AntdIconClassifier {
  10. load: () => void;
  11. predict: (imgEl: HTMLImageElement) => void;
  12. }
  13. declare global {
  14. interface Window {
  15. antdIconClassifier: AntdIconClassifier;
  16. }
  17. }
  18. interface PicSearcherState {
  19. loading: boolean;
  20. modalOpen: boolean;
  21. popoverVisible: boolean;
  22. icons: iconObject[];
  23. fileList: any[];
  24. error: boolean;
  25. modelLoaded: boolean;
  26. }
  27. interface iconObject {
  28. type: string;
  29. score: number;
  30. }
  31. const PicSearcher: React.FC = () => {
  32. const intl = useIntl();
  33. const { formatMessage } = intl;
  34. const [state, setState] = useState<PicSearcherState>({
  35. loading: false,
  36. modalOpen: false,
  37. popoverVisible: false,
  38. icons: [],
  39. fileList: [],
  40. error: false,
  41. modelLoaded: false,
  42. });
  43. const predict = (imgEl: HTMLImageElement) => {
  44. try {
  45. let icons: any[] = window.antdIconClassifier.predict(imgEl);
  46. if (gtag && icons.length) {
  47. gtag('event', 'icon', {
  48. event_category: 'search-by-image',
  49. event_label: icons[0].className,
  50. });
  51. }
  52. icons = icons.map((i) => ({ score: i.score, type: i.className.replace(/\s/g, '-') }));
  53. setState((prev) => ({ ...prev, loading: false, error: false, icons }));
  54. } catch {
  55. setState((prev) => ({ ...prev, loading: false, error: true }));
  56. }
  57. };
  58. // eslint-disable-next-line class-methods-use-this
  59. const toImage = (url: string) =>
  60. new Promise((resolve) => {
  61. const img = new Image();
  62. img.setAttribute('crossOrigin', 'anonymous');
  63. img.src = url;
  64. img.onload = () => {
  65. resolve(img);
  66. };
  67. });
  68. const uploadFile = useCallback((file: File) => {
  69. setState((prev) => ({ ...prev, loading: true }));
  70. const reader = new FileReader();
  71. reader.onload = () => {
  72. toImage(reader.result as string).then(predict);
  73. setState((prev) => ({
  74. ...prev,
  75. fileList: [{ uid: 1, name: file.name, status: 'done', url: reader.result }],
  76. }));
  77. };
  78. reader.readAsDataURL(file);
  79. }, []);
  80. const onPaste = useCallback((event: ClipboardEvent) => {
  81. const items = event.clipboardData && event.clipboardData.items;
  82. let file = null;
  83. if (items && items.length) {
  84. for (let i = 0; i < items.length; i++) {
  85. if (items[i].type.includes('image')) {
  86. file = items[i].getAsFile();
  87. break;
  88. }
  89. }
  90. }
  91. if (file) {
  92. uploadFile(file);
  93. }
  94. }, []);
  95. const toggleModal = useCallback(() => {
  96. setState((prev) => ({
  97. ...prev,
  98. modalOpen: !prev.modalOpen,
  99. popoverVisible: false,
  100. fileList: [],
  101. icons: [],
  102. }));
  103. if (!localStorage.getItem('disableIconTip')) {
  104. localStorage.setItem('disableIconTip', 'true');
  105. }
  106. }, []);
  107. useEffect(() => {
  108. const script = document.createElement('script');
  109. script.onload = async () => {
  110. await window.antdIconClassifier.load();
  111. setState((prev) => ({ ...prev, modelLoaded: true }));
  112. document.addEventListener('paste', onPaste);
  113. };
  114. script.src = 'https://cdn.jsdelivr.net/gh/lewis617/antd-icon-classifier@0.0/dist/main.js';
  115. document.head.appendChild(script);
  116. setState((prev) => ({ ...prev, popoverVisible: !localStorage.getItem('disableIconTip') }));
  117. return () => {
  118. document.removeEventListener('paste', onPaste);
  119. };
  120. }, []);
  121. return (
  122. <div className="iconPicSearcher">
  123. <Popover
  124. content={formatMessage({ id: 'app.docs.components.icon.pic-searcher.intro' })}
  125. open={state.popoverVisible}
  126. >
  127. <AntdIcons.CameraOutlined className="icon-pic-btn" onClick={toggleModal} />
  128. </Popover>
  129. <KFModal
  130. title={intl.formatMessage({
  131. id: 'app.docs.components.icon.pic-searcher.title',
  132. defaultMessage: '信息',
  133. })}
  134. open={state.modalOpen}
  135. onCancel={toggleModal}
  136. footer={null}
  137. >
  138. {state.modelLoaded || (
  139. <Spin
  140. spinning={!state.modelLoaded}
  141. tip={formatMessage({
  142. id: 'app.docs.components.icon.pic-searcher.modelloading',
  143. })}
  144. >
  145. <div style={{ height: 100 }} />
  146. </Spin>
  147. )}
  148. {state.modelLoaded && (
  149. <Dragger
  150. accept="image/jpeg, image/png"
  151. listType="picture"
  152. customRequest={(o) => uploadFile(o.file as File)}
  153. fileList={state.fileList}
  154. showUploadList={{ showPreviewIcon: false, showRemoveIcon: false }}
  155. >
  156. <p className="ant-upload-drag-icon">
  157. <AntdIcons.InboxOutlined />
  158. </p>
  159. <p className="ant-upload-text">
  160. {formatMessage({ id: 'app.docs.components.icon.pic-searcher.upload-text' })}
  161. </p>
  162. <p className="ant-upload-hint">
  163. {formatMessage({ id: 'app.docs.components.icon.pic-searcher.upload-hint' })}
  164. </p>
  165. </Dragger>
  166. )}
  167. <Spin
  168. spinning={state.loading}
  169. tip={formatMessage({ id: 'app.docs.components.icon.pic-searcher.matching' })}
  170. >
  171. <div className="icon-pic-search-result">
  172. {state.icons.length > 0 && (
  173. <div className="result-tip">
  174. {formatMessage({ id: 'app.docs.components.icon.pic-searcher.result-tip' })}
  175. </div>
  176. )}
  177. <table>
  178. {state.icons.length > 0 && (
  179. <thead>
  180. <tr>
  181. <th className="col-icon">
  182. {formatMessage({ id: 'app.docs.components.icon.pic-searcher.th-icon' })}
  183. </th>
  184. <th>
  185. {formatMessage({ id: 'app.docs.components.icon.pic-searcher.th-score' })}
  186. </th>
  187. </tr>
  188. </thead>
  189. )}
  190. <tbody>
  191. {state.icons.map((icon) => {
  192. const { type } = icon;
  193. const iconName = `${type
  194. .split('-')
  195. .map((str) => `${str[0].toUpperCase()}${str.slice(1)}`)
  196. .join('')}Outlined`;
  197. return (
  198. <tr key={iconName}>
  199. <td className="col-icon">
  200. <Tooltip title={icon.type} placement="right">
  201. {React.createElement(allIcons[iconName])}
  202. </Tooltip>
  203. </td>
  204. <td>
  205. <Progress percent={Math.ceil(icon.score * 100)} />
  206. </td>
  207. </tr>
  208. );
  209. })}
  210. </tbody>
  211. </table>
  212. {state.error && (
  213. <Result
  214. status="500"
  215. title="503"
  216. subTitle={formatMessage({
  217. id: 'app.docs.components.icon.pic-searcher.server-error',
  218. })}
  219. />
  220. )}
  221. </div>
  222. </Spin>
  223. </KFModal>
  224. </div>
  225. );
  226. };
  227. export default PicSearcher;