Commit 6ff43820 by zhaochengxiang

全文检索

parent 618a9856
...@@ -19,6 +19,7 @@ ...@@ -19,6 +19,7 @@
"insert-css": "^2.0.0", "insert-css": "^2.0.0",
"less": "^4.1.1", "less": "^4.1.1",
"less-loader": "^8.0.0", "less-loader": "^8.0.0",
"local-storage": "^2.0.0",
"react": "^17.0.1", "react": "^17.0.1",
"react-dnd": "^14.0.2", "react-dnd": "^14.0.2",
"react-dnd-html5-backend": "^14.0.0", "react-dnd-html5-backend": "^14.0.0",
...@@ -28,6 +29,7 @@ ...@@ -28,6 +29,7 @@
"react-scripts": "4.0.3", "react-scripts": "4.0.3",
"redux": "^4.0.1", "redux": "^4.0.1",
"redux-saga": "^1.0.5", "redux-saga": "^1.0.5",
"smooth-scroll": "^16.1.3",
"web-vitals": "^1.0.1" "web-vitals": "^1.0.1"
}, },
"scripts": { "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,
}}
value={keyword}
onChange={onKeywordChange}
onFocus={onFocus}
onBlur={onBlur}
/>
</Dropdown>
);
}
export default FullSearch;
\ No newline at end of file
.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;
}
}
}
}
\ No newline at end of file
...@@ -12,6 +12,7 @@ import { routes, routeMap } from '../routes'; ...@@ -12,6 +12,7 @@ import { routes, routeMap } from '../routes';
import './index.less'; import './index.less';
import { dispatchLatest } from "../model"; import { dispatchLatest } from "../model";
import logo from "../assets/logo.png"; import logo from "../assets/logo.png";
import FullSearch from './FullSearch';
const { Header, Sider, Content } = Layout; const { Header, Sider, Content } = Layout;
const { SubMenu } = Menu; const { SubMenu } = Menu;
...@@ -87,9 +88,11 @@ export const ManageLayout = function ({ content, location }) { ...@@ -87,9 +88,11 @@ export const ManageLayout = function ({ content, location }) {
</Link> */} </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 /> <Logout />
</Header> </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;
\ No newline at end of file
...@@ -136,3 +136,13 @@ export function generateList(data,dataList){ ...@@ -136,3 +136,13 @@ export function generateList(data,dataList){
} }
return 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 { Table, Space, Button, Tooltip, Modal } from 'antd';
import { EditOutlined, ReconciliationOutlined, DeleteOutlined } from '@ant-design/icons'; import { EditOutlined, ReconciliationOutlined, DeleteOutlined } from '@ant-design/icons';
import SmoothScroll from 'smooth-scroll';
import { dispatchLatest } from '../../../../model'; import { dispatchLatest } from '../../../../model';
import { showMessage } from '../../../../util'; import { showMessage, getQueryParam } from '../../../../util';
import './ModelTable.less'; import './ModelTable.less';
const ModelTable = (props) => { const ModelTable = (props) => {
...@@ -13,6 +14,9 @@ const ModelTable = (props) => { ...@@ -13,6 +14,9 @@ const ModelTable = (props) => {
const [modal, contextHolder] = Modal.useModal(); const [modal, contextHolder] = Modal.useModal();
const anchorId = getQueryParam('id', props.location.search);
const shouldScrollRef = useRef(anchorId!==null);
useEffect(() => { useEffect(() => {
setSelectedRowKeys([]); setSelectedRowKeys([]);
...@@ -21,6 +25,21 @@ const ModelTable = (props) => { ...@@ -21,6 +25,21 @@ const ModelTable = (props) => {
//eslint-disable-next-line react-hooks/exhaustive-deps //eslint-disable-next-line react-hooks/exhaustive-deps
}, [ catalogId ]); }, [ 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 = [ const columns = [
{ {
title: '序号', title: '序号',
...@@ -122,10 +141,24 @@ const ModelTable = (props) => { ...@@ -122,10 +141,24 @@ const ModelTable = (props) => {
onChange: onSelectChange, 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 ( return (
<div className='model-table'> <div className='model-table'>
<Table <Table
loading={loading} loading={loading}
components={{
body: {
row: AnchorRow
}
}}
rowSelection={rowSelection} rowSelection={rowSelection}
columns={columns} columns={columns}
rowKey={'id'} rowKey={'id'}
......
...@@ -4,7 +4,7 @@ import { PlusOutlined, EditOutlined, SyncOutlined, DeleteOutlined } from '@ant- ...@@ -4,7 +4,7 @@ import { PlusOutlined, EditOutlined, SyncOutlined, DeleteOutlined } from '@ant-
import UpdateTreeItemModal from './UpdateTreeItemModal'; import UpdateTreeItemModal from './UpdateTreeItemModal';
import { dispatch } from '../../../../model'; import { dispatch } from '../../../../model';
import { showMessage } from '../../../../util'; import { showMessage, getQueryParam } from '../../../../util';
import './ModelTree.less'; import './ModelTree.less';
const ModelTree = (props) => { const ModelTree = (props) => {
...@@ -16,6 +16,8 @@ const ModelTree = (props) => { ...@@ -16,6 +16,8 @@ const ModelTree = (props) => {
const [ visible, setVisible ] = useState(false); const [ visible, setVisible ] = useState(false);
const [ type, setType ] = useState(null); const [ type, setType ] = useState(null);
const [ rootId, setRootId ] = useState(''); const [ rootId, setRootId ] = useState('');
const [ expandedKeys, setExpandedKeys ] = useState([]);
const [ autoExpandParent, setAutoExpandParent ] = useState(false);
const [modal, contextHolder] = Modal.useModal(); const [modal, contextHolder] = Modal.useModal();
...@@ -23,11 +25,13 @@ const ModelTree = (props) => { ...@@ -23,11 +25,13 @@ const ModelTree = (props) => {
itemRef.current = item; itemRef.current = item;
useEffect(() => { useEffect(() => {
getTreeData();
const _did = getQueryParam('did', props.location.search);
getTreeData(_did);
//eslint-disable-next-line react-hooks/exhaustive-deps //eslint-disable-next-line react-hooks/exhaustive-deps
}, []) }, [])
const getTreeData = () => { const getTreeData = (defaultSelectedId='') => {
setLoading(true); setLoading(true);
dispatch({ dispatch({
...@@ -38,6 +42,8 @@ const ModelTree = (props) => { ...@@ -38,6 +42,8 @@ const ModelTree = (props) => {
data.title = data.name||''; data.title = data.name||'';
data.children = data.subCatalogs||[]; data.children = data.subCatalogs||[];
let defaultItem = null;
function recursion(subCatalogs) { function recursion(subCatalogs) {
if ((subCatalogs||[]).length===0) return; if ((subCatalogs||[]).length===0) return;
...@@ -46,6 +52,11 @@ const ModelTree = (props) => { ...@@ -46,6 +52,11 @@ const ModelTree = (props) => {
catalog.key = catalog.id||''; catalog.key = catalog.id||'';
catalog.title = catalog.name||''; catalog.title = catalog.name||'';
catalog.children = catalog.subCatalogs||[]; catalog.children = catalog.subCatalogs||[];
if (catalog.id === defaultSelectedId) {
defaultItem = catalog;
}
recursion(catalog.subCatalogs); recursion(catalog.subCatalogs);
}) })
} }
...@@ -55,6 +66,28 @@ const ModelTree = (props) => { ...@@ -55,6 +66,28 @@ const ModelTree = (props) => {
setTreeData(data.subCatalogs||[]); setTreeData(data.subCatalogs||[]);
setRootId(data.id||''); setRootId(data.id||'');
if (defaultItem) {
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);
setItem(defaultItem);
onSelect && onSelect(defaultItem.key||'');
} else {
let firstItem = itemRef.current; let firstItem = itemRef.current;
if (firstItem === null) { if (firstItem === null) {
firstItem = (data.subCatalogs||[]).length>0?data.subCatalogs[0]: null; firstItem = (data.subCatalogs||[]).length>0?data.subCatalogs[0]: null;
...@@ -65,6 +98,8 @@ const ModelTree = (props) => { ...@@ -65,6 +98,8 @@ const ModelTree = (props) => {
itemRef.current = firstItem; itemRef.current = firstItem;
onSelect && onSelect(firstItem?(firstItem.key||''):''); onSelect && onSelect(firstItem?(firstItem.key||''):'');
}
}, },
error: () => { error: () => {
setLoading(false); setLoading(false);
...@@ -72,6 +107,37 @@ const ModelTree = (props) => { ...@@ -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) => { const onTreeSelect = (keys,data) => {
if ((keys||[]).length === 0) { if ((keys||[]).length === 0) {
...@@ -188,6 +254,9 @@ const ModelTree = (props) => { ...@@ -188,6 +254,9 @@ const ModelTree = (props) => {
> >
<Spin spinning={loading} > <Spin spinning={loading} >
<Tree <Tree
onExpand={onExpand}
expandedKeys={expandedKeys}
autoExpandParent={autoExpandParent}
showLine showLine
showIcon={false} showIcon={false}
onSelect={onTreeSelect} onSelect={onTreeSelect}
......
...@@ -215,7 +215,7 @@ class Model extends React.Component { ...@@ -215,7 +215,7 @@ class Model extends React.Component {
<Row> <Row>
<Col span={6} > <Col span={6} >
<div className='mr-4' style={{ backgroundColor: '#fff' }}> <div className='mr-4' style={{ backgroundColor: '#fff' }}>
<ModelTree onSelect={this.onTreeSelect} /> <ModelTree onSelect={this.onTreeSelect} {...this.props} />
</div> </div>
</Col> </Col>
<Col span={18}> <Col span={18}>
...@@ -270,7 +270,7 @@ class Model extends React.Component { ...@@ -270,7 +270,7 @@ class Model extends React.Component {
</Space> </Space>
</div> </div>
<div className='p-3'> <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>
</div> </div>
</Col> </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