import React, { useCallback, useEffect, useRef, useState, } from 'react'; import classNames from 'classnames'; import PinyinMatch from 'pinyin-match'; import { useHistory } from 'react-router-dom'; import Icon, { RightCircleOutlined } from '@ant-design/icons'; import Modal from '../modal'; import styes from './index.less'; const HistorySvg = () => ( <svg className="icon" viewBox="0 0 1024 1024" version="1.1" width="10" height="10" > <path d="M1016.405333 0h-351.573333a7.594667 7.594667 0 0 0-5.376 12.885333l149.077333 149.674667-244.224 249.770667H434.346667A7.594667 7.594667 0 0 0 426.666667 419.84v169.898667c0 4.266667 3.413333 7.594667 7.594666 7.594666h169.813334c4.266667 0 7.68-3.413333 7.68-7.594666V486.826667l263.253333-257.792 136.106667 135.509333A7.594667 7.594667 0 0 0 1024 359.253333V7.68A7.594667 7.594667 0 0 0 1016.405333 0z" fill="#C1C1C1" /> <path d="M979.626667 640a45.226667 45.226667 0 0 0-44.202667 46.08v249.258667H88.490667V88.576h245.418666l1.194667-0.085333A45.056 45.056 0 0 0 378.88 44.288 45.056 45.056 0 0 0 333.994667 0H7.68A7.68 7.68 0 0 0 0 7.68v1008.64c0 4.266667 3.413333 7.68 7.68 7.68h1008.64c4.266667 0 7.68-3.413333 7.68-7.68V686.08a45.226667 45.226667 0 0 0-44.373333-46.08z" fill="#C1C1C1" /> </svg> ); const DelHistory = () => ( <svg className="icon" viewBox="0 0 1024 1024" version="1.1" width="12" height="12" > <path d="M153.6 345.6a38.4 38.4 0 0 1 0-76.8h665.6a38.4 38.4 0 1 1 0 76.8H153.6z" fill="#888888" /> <path d="M345.6 307.2a38.4 38.4 0 0 1-76.8 0V204.8c0-49.4592 40.1408-89.6 89.6-89.6h256c49.4592 0 89.6 40.1408 89.6 89.6v102.4a38.4 38.4 0 1 1-76.8 0V204.8a12.8 12.8 0 0 0-12.8-12.8H358.4a12.8 12.8 0 0 0-12.8 12.8v102.4zM166.4 460.8a38.4 38.4 0 0 1 76.8 0v358.4c0 7.0656 5.7344 12.8 12.8 12.8h460.8a12.8 12.8 0 0 0 12.8-12.8V460.8a38.4 38.4 0 1 1 76.8 0v358.4A89.6 89.6 0 0 1 716.8 908.8H256A89.6 89.6 0 0 1 166.4 819.2V460.8z" fill="#888888" /> <path d="M371.2 460.8a38.4 38.4 0 0 1 76.8 0v256a38.4 38.4 0 1 1-76.8 0V460.8zM524.8 460.8a38.4 38.4 0 0 1 76.8 0v256a38.4 38.4 0 1 1-76.8 0V460.8z" fill="#888888" /> </svg> ); const HistoryHrefIcon = props => <Icon component={HistorySvg} {...props} />; const DelIcon = props => <Icon component={DelHistory} {...props} />; const unique = (arr, val) => { const res = new Map(); return arr.filter(item => !res.has(item[val]) && res.set(item[val], 1)); }; const SearchPanel = props => { const [visible, setVisible] = useState(false); // const [disabled, setDisabled] = useState(false); // const [axis, setAxis] = useState('both'); // const [currentIndex, setCurrentIndex] = useState(-1); const [eventCounts, setEventCounts] = useState(() => ['resultConsole', -1]); const featureRef = useRef(null); const consoleRef = useRef(null); let recentKeywords = props.recentKeywords.toJS ? props.recentKeywords.toJS() : props.recentKeywords; recentKeywords = Array.isArray(recentKeywords) ? recentKeywords : [recentKeywords]; let recentVisited = props.recentVisited.toJS ? props.recentVisited.toJS() : props.recentVisited; recentVisited = Array.isArray(recentVisited) ? recentVisited : [recentVisited]; let recentProducts = props.recentProducts.toJS ? props.recentProducts.toJS() : props.recentProducts; recentProducts = Array.isArray(recentProducts) ? recentProducts : [recentProducts]; const history = useHistory(); // eslint-disable-next-line consistent-return function getParents(data, key) { // eslint-disable-next-line guard-for-in,no-restricted-syntax for (const i in data) { if (data[i].key === key) { return []; } if (data[i].routes) { const ro = getParents(data[i].routes, key); if (ro !== undefined) return ro.concat(data[i]); } } } const goFeature = (path, rect, value) => { // eslint-disable-next-line no-param-reassign value = value || props.value; const findKeywordIndex = recentKeywords.length > 0 ? recentKeywords.findIndex(item => item.data === value) : -1; const findRecentVisitedIndex = recentVisited.length > 0 ? recentVisited.findIndex(item => item.data.name === rect.name) : -1; if (findKeywordIndex === -1) props.updateRecentKeywords && props.updateRecentKeywords(value); if (findRecentVisitedIndex === -1) props.updateRecentVisited && props.updateRecentVisited(rect); let parents = getParents(props.menu, rect.key); parents = parents[parents.length - 1]; const findIndex = props.menu.findIndex(k => k.name === parents.name); props.updateCurrentIndex && props.updateCurrentIndex(findIndex); props.updateOpenKeys && props.updateOpenKeys([parents.key]); props.updateComplexConfig && props.updateComplexConfig({}); props.updateComplexPathName && props.updateComplexPathName(null); if (rect && rect.routes && rect.routes.length > 0) { props.updateComplexConfig && props.updateComplexConfig(rect); props.updateComplexPathName && props.updateComplexPathName(rect.routes[0].path); history.push(rect.routes[0].path); } else { history.push(path); } props.updateSelectedKeys && props.updateSelectedKeys(rect.key); props.updatePathname && props.updatePathname(rect.key); }; const goProduct = (event, item) => { /* eslint-disable */ event.persist && event.persist(); event && event.stopPropagation(); const findRecentIndex = recentProducts.length > 0 ? recentProducts.findIndex(item => item.data.name === item.name) : -1; if (findRecentIndex === -1) props.updateRecentProduct && props.updateRecentProduct(item); const findIndex = props.menu.findIndex(k => k.name === item.name); props.updateCurrentIndex && props.updateCurrentIndex(findIndex); }; const handlerMore = event => { event.persist && event.persist(); event && event.nativeEvent.stopImmediatePropagation(); setVisible(!visible); props.onClose && props.onClose(event); }; const handlerDelHistory = () => { props.clearRecentProduct && props.clearRecentProduct(); }; const matchValue = (str, value) => { const match = PinyinMatch.match(str, value || props.value); return match; }; const transformRecentProduct = () => unique(recentProducts.map(item => item.data), 'name'); const transformWillSearch = () => { if (props.willSearch.length === 0) { return null; } return ( <div className={styes.item}> <div className={styes.title}>你是不是想搜:</div> <div className={styes.label}> {(unique(props.willSearch, 'name') || []).map(item => ( // eslint-disable-next-line react/button-has-type <button className={styes.btn} key={item.name} title={item.title} onClick={() => props.onWillSearch(item.name)} > {item.name} </button> ))} </div> </div> ); }; const transformConsole = () => { if (props.console.length === 0) { return <div className={styes.empty}>没有找到相关内容</div>; } return (props.console || []).map((item, index) => { const match = matchValue(item.name); const name = match ? ( <span> {item.name.substring(0, match[0])} <em>{item.name.substring(match[0], match[1] + 1)}</em> {item.name.substring(match[1] + 1)} </span> ) : ( item.name ); return ( <a className={styes.featureItem} key={item.name} onClick={event => goProduct(event, item)} tabIndex="1" onKeyDown={event => event.key === 'Enter' && event.keyCode === 13 && goProduct(event, item)} > <span className={styes.text}>{name}</span> <span className={styes.featIn}> <RightCircleOutlined /> </span> </a> ); }); }; const transformFeature = () => { if (props.willSearch.length === 0) { return <div className={styes.empty}>没有找到相关内容</div>; } return (unique(props.willSearch.slice(0, 20), 'path') || []).map( (item, index) => { const match = matchValue(item.name); const name = match ? ( <span> {item.name.substring(0, match[0])} <em>{item.name.substring(match[0], match[1] + 1)}</em> {item.name.substring(match[1] + 1)} </span> ) : ( item.name ); return ( <a className={styes.featureItem} key={index} onClick={() => goFeature(item.path, item)} tabIndex="1" onKeyDown={event => event.key === 'Enter' && event.keyCode === 13 && goFeature(item.path, item)} > <span className={styes.text}> {name} <HistoryHrefIcon className={styes.href} /> </span> <span className={styes.featIn}> {item.parent && item.parent.name} </span> </a> ); }, ); }; useEffect(() => { setEventCounts(['resultConsole', -1]) }, [props.value]); const RESULT_KEYS = ['resultConsole', 'resultFeature']; const getFistKey = (response, key, type) => { const result = RESULT_KEYS.filter(item => { const ret = response[item]; return !!(ret && 'success' === ret.type && ret.response.items.length > 0); }); if(0 === result.length) { return null; }; if(!key) { return "down" === type ? result[0]: type === "left" ? result[0]: type === "right" ? result[result.length - 1]:result[result.length - 1]; } const index = result.indexOf(key); return index < 0 ? null: 'down' === type ? index + 1 < result.length ? result[index + 1]: null: index - 1 >= 0 ? result[index - 1]: null; } const transfromEventHander = useCallback((children, data, type, handlerKey) => { if (children === 'none') { return } if (null === children) { const key = getFistKey(data, null, handlerKey ? handlerKey: type); if (!key) { return null; } children = key === 'resultConsole' ? consoleRef.current.children: featureRef.current.children const ret = data[key]; const currentIndex = 'up' === type ? ret.response.items.length - 1 : 0 return 'success' !== ret.type ? null : ( setEventCounts([key, currentIndex]), 'up' === type ? (props.onNavUp && (props.onNavUp(), children[currentIndex].focus())): props.onNavDown && (props.onNavDown(), children[currentIndex].focus()) ); } let eventsKey = eventCounts[0]; let eventsIndex = eventCounts[1]; let resultData = data[eventsKey]; if(resultData.response.items.length === 0) { const key = getFistKey(data, null, type); eventsKey = key; eventsIndex= -1; resultData = data[key]; children = key === 'resultConsole' ? consoleRef.current.children: featureRef.current.children } if ('success' !== resultData.type) { return null; } const index= "up" === type ? eventsIndex - 1: eventsIndex + 1; if(index >=0 && index < resultData.response.items.length) { return setEventCounts([eventsKey, index]), 'up' === type ? (props.onNavUp && (props.onNavUp(), children[index].focus())): props.onNavDown && (props.onNavDown(), children[index].focus()) } const getKey = getFistKey(data, eventsKey, type); if(!getKey) { return null; } const result = data[getKey]; if('success' !== result.type) { return null; } if('up' === type) { children = getKey === 'resultConsole' ? consoleRef.current.children: featureRef.current.children return setEventCounts([getKey, result.response.items.length - 1]), props.onNavUp && ( props.onNavUp(), children[result.response.items.length - 1].focus()) } return setEventCounts([getKey, 0]), (props.onNavDown && (props.onNavDown(), featureRef.current.children[0].focus())) }, [eventCounts, props.console, props.willSearch]) const transfomResponse = (data) => { return { query: props.value, response: { items: data, total: data.length }, type: 'success' } } let children = null; const eventHander = useCallback((event) => { const transformData = { resultConsole: transfomResponse(props.console), resultFeature: transfomResponse( unique(props.willSearch.slice(0, 20), 'path')), }; switch (event.key) { case "Escape": props.onEsc && ( event.preventDefault(), props.onEsc()) break; case "ArrowRight": event.preventDefault(); children = featureRef.current.children; transfromEventHander(null, transformData, 'down', 'right') break; case "ArrowDown": const type = eventCounts[0] children = type === 'resultFeature' ? featureRef.current.children: consoleRef.current.children; event.preventDefault(); transfromEventHander(children, transformData, 'down') break; case "ArrowLeft": event.preventDefault(); // const leftChildren = consoleRef.current.children; setEventCounts(['resultConsole', -1]) transfromEventHander(null, transformData, 'up', 'left') break; case "ArrowUp": children = eventCounts[0] === 'resultFeature' ? featureRef.current.children: consoleRef.current.children; event.preventDefault(); transfromEventHander(children, transformData, 'up') break } }, [eventCounts, props.console, props.willSearch]); useEffect(() => { props.test && document.addEventListener('keydown', eventHander); return function() { return document.removeEventListener('keydown', eventHander) } }, [props.test, eventHander, eventCounts]) return ( <div className={classNames(styes.searchPanel, props.className)} style={props.style} onMouseEnter={props.onMouseEnter} onMouseLeave={props.onMouseLeave} onFocus={props.onFocusChange} > <div className={styes.container} ref={props.wrapperRef}> {props.value === '' && ( <div className={styes.body} style={{ display: 'flex' }}> <div className={styes.flexItem}> {recentKeywords.length > 0 ? ( <> <h5 className={styes.historyTitle}> 最近搜索 <DelIcon onClick={handlerDelHistory} /> </h5> <div className={styes.label}> {(unique(recentKeywords, 'data') || []).map(item => ( // eslint-disable-next-line react/button-has-type <button className={styes.btn} title={item.data} key={item.data} onClick={() => props.onWillSearch(item.data)} > {item.data} </button> ))} </div> </> ) : null} <h5 className={styes.historyTitle} style={{ margin: recentKeywords.length > 0 ? '16px 0px 0px' : '0', }} > 最近访问的产品 </h5> {recentProducts.length > 0 ? ( (transformRecentProduct() || []).map(item => ( <a title={item.name} className={styes.historyHref} key={item.name} onClick={event => goProduct(event, item)} onKeyDown={(event => console.log(event))} > <span className={styes.title}>{item.name}</span> <span className={styes.featIn}> <RightCircleOutlined /> </span> </a> )) ) : ( <div className={styes.empty}>无最近访问的产品</div> )} </div> <div className={styes.flexItem}> <div className={styes.title}>关注更多信息</div> <div className={styes.label}> <div className={styes.empty}>没有找到相关内容</div> </div> </div> </div> )} {props.value !== '' && ( <> <div className={styes.body}> {transformWillSearch()} <div className={styes.features}> <div className={styes.flex}> <h5 className={styes.title}> 产品入口({props.console.length}) </h5> <div className={styes.feature} ref={consoleRef}>{transformConsole()}</div> </div> <div className={styes.flex}> <h5 className={styes.title}>快捷操作</h5> <div className={styes.feature} ref={featureRef}>{transformFeature()}</div> </div> </div> </div> {props.willSearch.length > 0 || props.console.length > 0 ? ( <div className={styes.footer}> <span onClick={handlerMore}>查看全部结果</span> </div> ) : null} </> )} </div> <Modal visible={visible} triggerElRef={props.target} {...props} matchValue={matchValue} goFeature={goFeature} goProduct={goProduct} /> </div> ); }; export default SearchPanel;