Commit 2d61d825 by zhaochengxiang

全文检索

parent 620528a1
......@@ -11,6 +11,7 @@
"@testing-library/user-event": "^12.1.10",
"antd": "^4.14.0",
"axios": "^0.19.0",
"local-storage": "^2.0.0",
"copy-to-clipboard": "^3.3.1",
"core-js": "^3.4.2",
"craco-less": "^1.17.1",
......@@ -28,6 +29,7 @@
"react-scripts": "4.0.3",
"redux": "^4.0.1",
"redux-saga": "^1.0.5",
"smooth-scroll": "^16.1.3",
"web-vitals": "^1.0.1"
},
"scripts": {
......
import React, { useState, useEffect } from 'react';
import { Dropdown, Input, Tabs, List, Spin, Empty, Avatar, Typography } from 'antd';
import { SearchOutlined } from '@ant-design/icons';
import LocalStorage from 'local-storage';
import useDebounce from './useDebounce';
import './FullSearch.less';
const { TabPane } = Tabs;
const recordsKey = 'records';
const keywordKey = 'keyword';
const modes = [
{
title: '数据源',
shortTitle: '源',
key: 'data-source',
},
{
title: '元数据',
shortTitle: '元',
key: 'data-meta',
},
{
title: '数据标准',
shortTitle: '标',
key: 'data-standard',
},
{
title: '数据模型',
shortTitle: '模',
key: 'data-model',
},
{
title: '数据资产',
shortTitle: '资',
key: 'data-asset',
},
]
const list = [
{
title: 'test',
did: '609a4eaf9e14e664f6fee2ee',
id: '60b9978f9e14e64eb2ec5384',
desc: '模型描述'
},
{
title: 'test9',
did: '608b99d19e14e66dea274bbe',
id: '60b83b7f9e14e64eb2ec5382',
desc: '模型描述'
},
{
title: 'test',
did: '609a4eaf9e14e664f6fee2ee',
id: '60b9978f9e14e64eb2ec5384',
desc: '模型描述'
},
{
title: 'test9',
did: '608b99d19e14e66dea274bbe',
id: '60b83b7f9e14e64eb2ec5382',
desc: '模型描述'
},
{
title: 'test',
did: '609a4eaf9e14e664f6fee2ee',
id: '60b9978f9e14e64eb2ec5384',
desc: '模型描述'
},
{
title: 'test9',
did: '608b99d19e14e66dea274bbe',
id: '60b83b7f9e14e64eb2ec5382',
desc: '模型描述'
},
{
title: 'test',
did: '609a4eaf9e14e664f6fee2ee',
id: '60b9978f9e14e64eb2ec5384',
desc: '模型描述'
},
{
title: 'test9',
did: '608b99d19e14e66dea274bbe',
id: '60b83b7f9e14e64eb2ec5382',
desc: '模型描述'
},
{
title: 'test',
did: '609a4eaf9e14e664f6fee2ee',
id: '60b9978f9e14e64eb2ec5384',
desc: '模型描述'
},
{
title: 'test9',
did: '608b99d19e14e66dea274bbe',
id: '60b83b7f9e14e64eb2ec5382',
desc: '模型描述'
},
{
title: 'test',
did: '609a4eaf9e14e664f6fee2ee',
id: '60b9978f9e14e64eb2ec5384',
desc: '模型描述'
},
{
title: 'test9',
did: '608b99d19e14e66dea274bbe',
id: '60b83b7f9e14e64eb2ec5382',
desc: '模型描述'
},
{
title: 'test',
did: '609a4eaf9e14e664f6fee2ee',
id: '60b9978f9e14e64eb2ec5384',
desc: '模型描述'
},
{
title: 'test9',
did: '608b99d19e14e66dea274bbe',
id: '60b83b7f9e14e64eb2ec5382',
desc: '模型描述'
},
]
const saveRecord = (item) => {
let records = getAllRecords();
const index = records.findIndex((record) => record.id === item.id);
if (index !== -1) {
records.splice(index, 1);
}
records = [item, ...records];
//最多只保存十条
LocalStorage.set(recordsKey, JSON.stringify(records.slice(0, 10)));
}
const getAllRecords = () => {
return (JSON.parse(LocalStorage.get(recordsKey)) || []);
}
const clearAllRecords = () => {
LocalStorage.remove(recordsKey);
}
const FullSearchEmpty = (props) => {
const [reports, setReports] = useState([]);
useEffect(() => {
setReports(getAllRecords);
}, [])
const onPreventMouseDown = (event) => {
event.preventDefault();
event.stopPropagation();
}
const onItemClick = (item) => {
saveRecord(item);
window.location.href=`data-model?did=${item.did}&id=${item.id}`;
}
const clear = () => {
clearAllRecords();
setReports([]);
}
return (
<div className='full-search-empty' onMouseDown={onPreventMouseDown}>
{
(reports||[]).length===0 ? (
<div className='no-record-content'>
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="输入关键字开始搜索" />
</div>
) : (
<div style={{ padding: 10 }}>
<div className='header'>
<span className='recent'>最近搜索</span>
<span className='clear' onClick={clear}>清空</span>
</div>
<div className='content'>
{
(reports||[]).map((report, index) => {
return (
<div className='tag' key={index} onClick={() => { onItemClick(report); }}>
<div style={{ width: 24, height: 24, marginRight: 5, marginBottom: 2 }}>
<Avatar shape="square" size="small" title={report.mode?(report.mode.title||''):''}>{report.mode?(report.mode.shortTitle||''):''}</Avatar>
</div>
<Typography.Paragraph
title={report.title||''}
ellipsis
>
{report.title||''}
</Typography.Paragraph>
</div>
);
})
}
</div>
</div>
)
}
</div>
);
}
const FullSearchContent = (props) => {
const { keyword } = props;
const [currentModeKey, setCurrentModeKey] = useState('data-source');
const [loading, setLoading] = useState(false);
const debouncedKeyword = useDebounce(keyword, 300)
useEffect(() => {
setLoading(true);
setTimeout(() => {
setLoading(false);
}, 1000)
}, [debouncedKeyword])
const onPreventMouseDown = (event) => {
event.preventDefault();
event.stopPropagation();
}
const onModeChange = (key) => {
setCurrentModeKey(key);
setLoading(true);
setTimeout(() => {
setLoading(false);
}, 1000)
}
const onItemClick = (item) => {
const index = modes.findIndex((mode) => mode.key === currentModeKey);
saveRecord({...item, mode: modes[index]});
window.location.href=`data-model?did=${item.did}&id=${item.id}`;
}
return (
<div className='full-search-content' onMouseDown={onPreventMouseDown}>
<Tabs value={currentModeKey} onChange={onModeChange}>
{
modes && modes.map(mode => {
return (
<TabPane tab={mode.title} key={mode.key}>
<Spin spinning={loading}>
<List
dataSource={list}
renderItem={(item, index) => {
return (
<>
<div className='list-item' onClick={() => { onItemClick(item); }}>
<div>{item.title||''}</div>
<div className='desc'>{item.desc||''}</div>
</div>
{
(index===list.length-1) && <div style={{ display: "flex", height: 44, fontSize: 12, alignItems: 'center', justifyContent: 'center', color: '#959899' }}>
已展示全部结果
</div>
}
</>
);
}}
/>
</Spin>
</TabPane>
);
})
}
</Tabs>
</div>
);
}
const FullSearch = (props) => {
const [focused, setFocused] = useState(false);
const [keyword, setKeyword] = useState(LocalStorage.get(keywordKey)||'');
const onKeywordChange = (e) => {
setKeyword(e.target.value||'');
LocalStorage.set(keywordKey, e.target.value);
}
const onFocus = () => {
triggerFocus(true);
}
const onBlur = () => {
triggerFocus(false);
}
const triggerFocus = (focus) => {
setFocused(focus);
}
return (
<Dropdown
overlay={
(keyword||'') === '' ? <FullSearchEmpty /> : <FullSearchContent keyword={keyword} />
}
visible={focused}
>
<Input
prefix={<SearchOutlined style={{ color: '#ced4d9' }} />}
placeholder='在数据治理系统中搜索'
style={{
width: focused ? 390 : 200,
}}
allowClear
value={keyword}
onChange={onKeywordChange}
onFocus={onFocus}
onBlur={onBlur}
/>
</Dropdown>
);
}
export default FullSearch;
.full-search-content {
width: 390px;
height: 540px;
background-color: #fff;
border-radius: 2px;
box-shadow: 0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 9px 28px 8px rgba(0, 0, 0, 0.05);
box-shadow: 0 0 8px rgba(0, 0, 0, 0.15) \9;
.yy-tabs-nav {
padding: 0 10px !important;
margin-bottom: 0 !important;
}
.yy-tabs-content-holder {
height: 494px;
overflow: auto !important;
}
.list-item {
cursor: pointer;
padding: 10px;
border-bottom: 1px solid #f0f0f0;
&:hover {
background-color: #e7f7ff;
}
.desc {
color: #959899;
}
}
}
.full-search-empty {
width: 390px;
height: 540px;
background-color: #fff;
border-radius: 2px;
box-shadow: 0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 9px 28px 8px rgba(0, 0, 0, 0.05);
box-shadow: 0 0 8px rgba(0, 0, 0, 0.15) \9;
.no-record-content {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
.recent {
font-size: 16px;
}
.clear {
cursor: pointer;
font-size: 12px;
color: #a2a3a4;
&:hover {
color: #2593fc;
}
}
}
.content {
overflow: auto;
display: flex;
align-items: center;
flex-wrap: wrap;
border-radius: 2px;
.tag {
display: flex;
align-items: center;
cursor: pointer;
font-size: 16px;
padding: 5px 7px;
background-color: #eaeced;
margin-bottom: 5px;
margin-right: 5px;
min-width: 0;
&:hover {
background-color: #e0e2e4;
}
.title {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
word-break: break-all;
}
}
}
}
......@@ -12,6 +12,7 @@ import { routes, routeMap } from '../routes';
import './index.less';
import { dispatchLatest } from "../model";
import logo from "../assets/logo.png";
import FullSearch from './FullSearch';
const { Header, Sider, Content } = Layout;
const { SubMenu } = Menu;
......@@ -87,9 +88,11 @@ export const ManageLayout = function ({ content, location }) {
</Link> */}
{
collapsed ? <MenuUnfoldOutlined style={{ marginLeft: '16px' }} onClick={() => toggle(!collapsed)} /> : <MenuFoldOutlined style={{ marginLeft: '16px' }} onClick={() => toggle(!collapsed)} />
collapsed ? <MenuUnfoldOutlined style={{ marginLeft: '16px', marginRight: '100px' }} onClick={() => toggle(!collapsed)} /> : <MenuFoldOutlined style={{ marginLeft: '16px', marginRight: '185px' }} onClick={() => toggle(!collapsed)} />
}
<FullSearch />
<Logout />
</Header>
......
import { useState, useEffect } from 'react';
function useDebounce(value, delay = 300) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = window.setTimeout(() => {
setDebouncedValue(value);
}, delay)
return () => {
clearTimeout(handler);
}
}, [value, delay])
return debouncedValue;
}
export default useDebounce;
......@@ -136,3 +136,13 @@ export function generateList(data,dataList){
}
return dataList
};
export const getQueryParam = (param, url) => {
//eslint-disable-next-line
const name = param.replace(/[\[\]]/g, '\\$&')
const regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)')
const results = regex.exec(url)
if (!results) return null
if (!results[2]) return ''
return decodeURIComponent(results[2].replace(/\+/g, ' '))
}
import React, { useState, useEffect } from "react";
import React, { useState, useEffect, useRef } from "react";
import { Table, Space, Button, Tooltip, Modal } from 'antd';
import { EditOutlined, ReconciliationOutlined, DeleteOutlined } from '@ant-design/icons';
import SmoothScroll from 'smooth-scroll';
import { dispatchLatest } from '../../../../model';
import { showMessage } from '../../../../util';
import { showMessage, getQueryParam } from '../../../../util';
import './ModelTable.less';
const ModelTable = (props) => {
......@@ -13,6 +14,9 @@ const ModelTable = (props) => {
const [modal, contextHolder] = Modal.useModal();
const anchorId = getQueryParam('id', props.location.search);
const shouldScrollRef = useRef(anchorId!==null);
useEffect(() => {
setSelectedRowKeys([]);
......@@ -21,6 +25,21 @@ const ModelTable = (props) => {
//eslint-disable-next-line react-hooks/exhaustive-deps
}, [ catalogId ]);
useEffect(() => {
if (shouldScrollRef.current) {
SmoothScroll('a[href*="#"]');
const _id = getQueryParam('id', props.location.search);
var scroll = new SmoothScroll();
var anchor = document.querySelector(`#data-model-${_id}`);
if (anchor) {
scroll.animateScroll(anchor);
shouldScrollRef.current = false;
}
}
})
const columns = [
{
title: '序号',
......@@ -122,10 +141,24 @@ const ModelTable = (props) => {
onChange: onSelectChange,
};
const AnchorRow = (props) => {
const id = props['data-row-key']||'';
return (
<tr id={`data-model-${id}`} {...props} style={{ backgroundColor: (id===anchorId)?'#e7f7ff':'transparent' }} />
);
}
return (
<div className='model-table'>
<Table
loading={loading}
components={{
body: {
row: AnchorRow
}
}}
rowSelection={rowSelection}
columns={columns}
rowKey={'id'}
......
......@@ -4,7 +4,7 @@ import { PlusOutlined, EditOutlined, SyncOutlined, DeleteOutlined } from '@ant-
import UpdateTreeItemModal from './UpdateTreeItemModal';
import { dispatch } from '../../../../model';
import { showMessage } from '../../../../util';
import { showMessage, getQueryParam } from '../../../../util';
import './ModelTree.less';
const ModelTree = (props) => {
......@@ -16,6 +16,8 @@ const ModelTree = (props) => {
const [ visible, setVisible ] = useState(false);
const [ type, setType ] = useState(null);
const [ rootId, setRootId ] = useState('');
const [ expandedKeys, setExpandedKeys ] = useState([]);
const [ autoExpandParent, setAutoExpandParent ] = useState(false);
const [modal, contextHolder] = Modal.useModal();
......@@ -23,11 +25,13 @@ const ModelTree = (props) => {
itemRef.current = item;
useEffect(() => {
getTreeData();
const _did = getQueryParam('did', props.location.search);
getTreeData(_did);
//eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
const getTreeData = () => {
const getTreeData = (defaultSelectedId='') => {
setLoading(true);
dispatch({
......@@ -38,6 +42,8 @@ const ModelTree = (props) => {
data.title = data.name||'';
data.children = data.subCatalogs||[];
let defaultItem = null;
function recursion(subCatalogs) {
if ((subCatalogs||[]).length===0) return;
......@@ -46,6 +52,11 @@ const ModelTree = (props) => {
catalog.key = catalog.id||'';
catalog.title = catalog.name||'';
catalog.children = catalog.subCatalogs||[];
if (catalog.id === defaultSelectedId) {
defaultItem = catalog;
}
recursion(catalog.subCatalogs);
})
}
......@@ -55,16 +66,40 @@ const ModelTree = (props) => {
setTreeData(data.subCatalogs||[]);
setRootId(data.id||'');
let firstItem = itemRef.current;
if (firstItem === null) {
firstItem = (data.subCatalogs||[]).length>0?data.subCatalogs[0]: null;
if (defaultItem) {
setItem(firstItem);
}
const _dataList = [];
generateList(data.subCatalogs||[], _dataList);
const expandedKeys = _dataList
.map(item => {
if (item.key.indexOf(defaultSelectedId) > -1) {
return getParentKey(item.key, data.subCatalogs||[]);
}
return null;
})
.filter((item, i, self) => item && self.indexOf(item) === i);
setExpandedKeys(expandedKeys);
setAutoExpandParent(true);
itemRef.current = firstItem;
setItem(defaultItem);
onSelect && onSelect(defaultItem.key||'');
} else {
onSelect && onSelect(firstItem?(firstItem.key||''):'');
let firstItem = itemRef.current;
if (firstItem === null) {
firstItem = (data.subCatalogs||[]).length>0?data.subCatalogs[0]: null;
setItem(firstItem);
}
itemRef.current = firstItem;
onSelect && onSelect(firstItem?(firstItem.key||''):'');
}
},
error: () => {
setLoading(false);
......@@ -72,6 +107,37 @@ const ModelTree = (props) => {
})
}
const generateList = (treeData, list) => {
for (let i = 0; i < treeData.length; i++) {
const node = treeData[i];
const { id, name } = node;
list.push({ key: id , title: name });
if (node.children) {
generateList(node.children, list);
}
}
};
const getParentKey = (key, tree) => {
let parentKey;
for (let i = 0; i < tree.length; i++) {
const node = tree[i];
if (node.children) {
if (node.children.some(item => item.id === key)) {
parentKey = node.id;
} else if (getParentKey(key, node.children)) {
parentKey = getParentKey(key, node.children);
}
}
}
return parentKey;
};
const onExpand = (expandedKeys) => {
setExpandedKeys(expandedKeys);
setAutoExpandParent(false);
};
const onTreeSelect = (keys,data) => {
if ((keys||[]).length === 0) {
......@@ -188,6 +254,9 @@ const ModelTree = (props) => {
>
<Spin spinning={loading} >
<Tree
onExpand={onExpand}
expandedKeys={expandedKeys}
autoExpandParent={autoExpandParent}
showLine
showIcon={false}
onSelect={onTreeSelect}
......
......@@ -215,7 +215,7 @@ class Model extends React.Component {
<Row>
<Col span={6} >
<div className='mr-4' style={{ backgroundColor: '#fff' }}>
<ModelTree onSelect={this.onTreeSelect} />
<ModelTree onSelect={this.onTreeSelect} {...this.props} />
</div>
</Col>
<Col span={18}>
......@@ -270,7 +270,7 @@ class Model extends React.Component {
</Space>
</div>
<div className='p-3'>
<ModelTable loading={loadingTableData} catalogId={catalogId} data={filterTableData} onChange={this.onTableChange} onSelect={this.onTableSelect} onItemAction={this.onTableItemAction} />
<ModelTable loading={loadingTableData} catalogId={catalogId} data={filterTableData} onChange={this.onTableChange} onSelect={this.onTableSelect} onItemAction={this.onTableItemAction} {...this.props} />
</div>
</div>
</Col>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment