mirror of
https://github.com/alibaba/lowcode-engine.git
synced 2025-12-12 11:20:11 +00:00
feat: 大纲树支持节点过滤
This commit is contained in:
parent
670eeb9fe2
commit
f30db20606
@ -1,12 +1,48 @@
|
|||||||
import { Component, isValidElement } from 'react';
|
import { Component, isValidElement, ReactNode } from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { createIcon } from '@alilc/lowcode-utils';
|
import { createIcon } from '@alilc/lowcode-utils';
|
||||||
import { TitleContent, isI18nData } from '@alilc/lowcode-types';
|
import { TitleContent, isI18nData, I18nData } from '@alilc/lowcode-types';
|
||||||
import { intl } from '../../intl';
|
import { intl } from '../../intl';
|
||||||
import { Tip } from '../tip';
|
import { Tip } from '../tip';
|
||||||
import './title.less';
|
import './title.less';
|
||||||
|
|
||||||
export class Title extends Component<{ title: TitleContent; className?: string; onClick?: () => void }> {
|
/**
|
||||||
|
* 根据 keywords 将 label 分割成文字片段
|
||||||
|
* 示例:title = '自定义页面布局',keywords = '页面',返回结果为 ['自定义', '页面', '布局']
|
||||||
|
* @param label title
|
||||||
|
* @param keywords 关键字
|
||||||
|
* @returns 文字片段列表
|
||||||
|
*/
|
||||||
|
function splitLabelByKeywords(label: string, keywords: string): string[] {
|
||||||
|
const len = keywords.length;
|
||||||
|
const fragments = [];
|
||||||
|
let str = label;
|
||||||
|
|
||||||
|
while (str.length > 0) {
|
||||||
|
const index = str.indexOf(keywords);
|
||||||
|
|
||||||
|
if (index === 0) {
|
||||||
|
fragments.push(keywords);
|
||||||
|
str = str.slice(len);
|
||||||
|
} else if (index < 0) {
|
||||||
|
fragments.push(str);
|
||||||
|
str = '';
|
||||||
|
} else {
|
||||||
|
fragments.push(str.slice(0, index));
|
||||||
|
str = str.slice(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fragments;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Title extends Component<{
|
||||||
|
title: TitleContent;
|
||||||
|
className?: string;
|
||||||
|
onClick?: () => void;
|
||||||
|
match?: boolean;
|
||||||
|
keywords?: string;
|
||||||
|
}> {
|
||||||
constructor(props: any) {
|
constructor(props: any) {
|
||||||
super(props);
|
super(props);
|
||||||
this.handleClick = this.handleClick.bind(this);
|
this.handleClick = this.handleClick.bind(this);
|
||||||
@ -24,6 +60,32 @@ export class Title extends Component<{ title: TitleContent; className?: string;
|
|||||||
onClick && onClick(e);
|
onClick && onClick(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderLabel = (label: string | I18nData | ReactNode) => {
|
||||||
|
let { match, keywords } = this.props;
|
||||||
|
|
||||||
|
if (!label) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const intlLabel = intl(label);
|
||||||
|
|
||||||
|
if (typeof intlLabel !== 'string') {
|
||||||
|
return <span className="lc-title-txt">{intlLabel}</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
let labelToRender: ReactNode = intlLabel;
|
||||||
|
|
||||||
|
if (match && keywords) {
|
||||||
|
const fragments = splitLabelByKeywords(intlLabel as string, keywords);
|
||||||
|
|
||||||
|
labelToRender = fragments.map(f => <span style={{ color: f === keywords ? 'red' : 'inherit' }}>{f}</span>);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className="lc-title-txt">{labelToRender}</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
// eslint-disable-next-line prefer-const
|
// eslint-disable-next-line prefer-const
|
||||||
let { title, className } = this.props;
|
let { title, className } = this.props;
|
||||||
@ -61,7 +123,7 @@ export class Title extends Component<{ title: TitleContent; className?: string;
|
|||||||
onClick={this.handleClick}
|
onClick={this.handleClick}
|
||||||
>
|
>
|
||||||
{icon ? <b className="lc-title-icon">{icon}</b> : null}
|
{icon ? <b className="lc-title-icon">{icon}</b> : null}
|
||||||
{title.label ? <span className="lc-title-txt">{intl(title.label)}</span> : null}
|
{this.renderLabel(title.label)}
|
||||||
{tip}
|
{tip}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -117,7 +117,7 @@ body {
|
|||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
}
|
}
|
||||||
.lc-outline-pane {
|
.lc-outline-tree-container {
|
||||||
border-top: 1px solid var(--color-line-normal, rgba(31, 56, 88, 0.1));
|
border-top: 1px solid var(--color-line-normal, rgba(31, 56, 88, 0.1));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
11
packages/plugin-outline-pane/src/icons/filter.tsx
Normal file
11
packages/plugin-outline-pane/src/icons/filter.tsx
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { SVGIcon, IconProps } from '@alilc/lowcode-utils';
|
||||||
|
|
||||||
|
export function IconFilter(props: IconProps) {
|
||||||
|
return (
|
||||||
|
<SVGIcon viewBox="0 0 1024 1024" {...props}>
|
||||||
|
<path d="M911.457097 168.557714a35.986286 35.986286 0 0 1-8.009143 40.009143L621.73824 490.276571V914.285714c0 14.848-9.142857 28.013714-22.272 33.718857A42.349714 42.349714 0 0 1 585.166811 950.857143a34.084571 34.084571 0 0 1-25.709714-10.861714l-146.285714-146.285715A36.425143 36.425143 0 0 1 402.309669 768v-277.723429L120.599954 208.566857a35.986286 35.986286 0 0 1-8.009143-40.009143C118.295954 155.428571 131.461669 146.285714 146.309669 146.285714h731.428571c14.848 0 28.013714 9.142857 33.718857 22.272z" fill="#666" p-id="2025" />
|
||||||
|
</SVGIcon>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
IconFilter.displayName = 'IconFilter';
|
||||||
@ -3,6 +3,20 @@ import { computed, obx, intl, makeObservable, action } from '@alilc/lowcode-edit
|
|||||||
import { Node, DocumentModel, isLocationChildrenDetail, LocationChildrenDetail, Designer } from '@alilc/lowcode-designer';
|
import { Node, DocumentModel, isLocationChildrenDetail, LocationChildrenDetail, Designer } from '@alilc/lowcode-designer';
|
||||||
import { Tree } from './tree';
|
import { Tree } from './tree';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 大纲树过滤结果
|
||||||
|
*/
|
||||||
|
export interface FilterResult {
|
||||||
|
// 过滤条件是否生效
|
||||||
|
filterWorking: boolean;
|
||||||
|
// 命中子节点
|
||||||
|
matchChild: boolean;
|
||||||
|
// 命中本节点
|
||||||
|
matchSelf: boolean;
|
||||||
|
// 关键字
|
||||||
|
keywords: string;
|
||||||
|
}
|
||||||
|
|
||||||
export default class TreeNode {
|
export default class TreeNode {
|
||||||
get id(): string {
|
get id(): string {
|
||||||
return this.node.id;
|
return this.node.id;
|
||||||
@ -231,4 +245,20 @@ export default class TreeNode {
|
|||||||
this._node = node;
|
this._node = node;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@obx.ref private _filterResult: FilterResult = {
|
||||||
|
filterWorking: false,
|
||||||
|
matchChild: false,
|
||||||
|
matchSelf: false,
|
||||||
|
keywords: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
get filterReult(): FilterResult {
|
||||||
|
return this._filterResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
setFilterReult(val: FilterResult) {
|
||||||
|
this._filterResult = val;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
88
packages/plugin-outline-pane/src/views/filter-tree.ts
Normal file
88
packages/plugin-outline-pane/src/views/filter-tree.ts
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
import TreeNode from '../tree-node';
|
||||||
|
|
||||||
|
export const FilterType = {
|
||||||
|
CONDITION: 'CONDITION',
|
||||||
|
LOOP: 'LOOP',
|
||||||
|
LOCKED: 'LOCKED',
|
||||||
|
HIDDEN: 'HIDDEN',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FILTER_OPTIONS = [{
|
||||||
|
value: FilterType.CONDITION,
|
||||||
|
label: '条件渲染',
|
||||||
|
}, {
|
||||||
|
value: FilterType.LOOP,
|
||||||
|
label: '循环渲染',
|
||||||
|
}, {
|
||||||
|
value: FilterType.LOCKED,
|
||||||
|
label: '已锁定',
|
||||||
|
}, {
|
||||||
|
value: FilterType.HIDDEN,
|
||||||
|
label: '已隐藏',
|
||||||
|
}];
|
||||||
|
|
||||||
|
export const matchTreeNode = (
|
||||||
|
treeNode: TreeNode,
|
||||||
|
keywords: string,
|
||||||
|
filterOps: string[],
|
||||||
|
): boolean => {
|
||||||
|
// 无效节点
|
||||||
|
if (!treeNode || !treeNode.node) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 过滤条件为空,重置过滤结果
|
||||||
|
if (!keywords && filterOps.length === 0) {
|
||||||
|
treeNode.setFilterReult({
|
||||||
|
filterWorking: false,
|
||||||
|
matchChild: false,
|
||||||
|
matchSelf: false,
|
||||||
|
keywords: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
(treeNode.children || []).concat(treeNode.slots || []).forEach((childNode) => {
|
||||||
|
matchTreeNode(childNode, keywords, filterOps);
|
||||||
|
});
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { node } = treeNode;
|
||||||
|
|
||||||
|
// 命中过滤选项
|
||||||
|
const matchFilterOps = filterOps.length === 0 || !!filterOps.find((op: string) => {
|
||||||
|
switch (op) {
|
||||||
|
case FilterType.CONDITION:
|
||||||
|
return node.hasCondition();
|
||||||
|
case FilterType.LOOP:
|
||||||
|
return node.hasLoop();
|
||||||
|
case FilterType.LOCKED:
|
||||||
|
return treeNode.locked;
|
||||||
|
case FilterType.HIDDEN:
|
||||||
|
return treeNode.hidden;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 命中节点名
|
||||||
|
const matchKeywords = typeof treeNode.titleLabel === 'string' && treeNode.titleLabel.indexOf(keywords) > -1;
|
||||||
|
|
||||||
|
// 同时命中才展示(根结点永远命中)
|
||||||
|
const matchSelf = treeNode.isRoot() || (matchFilterOps && matchKeywords);
|
||||||
|
|
||||||
|
// 命中子节点
|
||||||
|
const matchChild = !!(treeNode.children || []).concat(treeNode.slots || [])
|
||||||
|
.map((childNode: TreeNode) => {
|
||||||
|
return matchTreeNode(childNode, keywords, filterOps);
|
||||||
|
}).find(Boolean);
|
||||||
|
|
||||||
|
treeNode.setFilterReult({
|
||||||
|
filterWorking: true,
|
||||||
|
matchChild,
|
||||||
|
matchSelf,
|
||||||
|
keywords,
|
||||||
|
});
|
||||||
|
|
||||||
|
return matchSelf || matchChild;
|
||||||
|
};
|
||||||
100
packages/plugin-outline-pane/src/views/filter.tsx
Normal file
100
packages/plugin-outline-pane/src/views/filter.tsx
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
import './style.less';
|
||||||
|
import { IconFilter } from '../icons/filter';
|
||||||
|
import { Search, Checkbox, Balloon, Divider } from '@alifd/next';
|
||||||
|
import TreeNode from '../tree-node';
|
||||||
|
import { Tree } from '../tree';
|
||||||
|
import { matchTreeNode, FILTER_OPTIONS } from './filter-tree';
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
keywords: string;
|
||||||
|
filterOps: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
tree: Tree;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class Filter extends Component<IProps, IState> {
|
||||||
|
state = {
|
||||||
|
keywords: '',
|
||||||
|
filterOps: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
handleSearchChange = (val: string) => {
|
||||||
|
this.setState({
|
||||||
|
keywords: val.trim(),
|
||||||
|
}, this.filterTree);
|
||||||
|
};
|
||||||
|
|
||||||
|
handleOptionChange = (val: string[]) => {
|
||||||
|
this.setState({
|
||||||
|
filterOps: val,
|
||||||
|
}, this.filterTree);
|
||||||
|
};
|
||||||
|
|
||||||
|
handleCheckAll = () => {
|
||||||
|
const { filterOps } = this.state;
|
||||||
|
const final = filterOps.length === FILTER_OPTIONS.length
|
||||||
|
? [] : FILTER_OPTIONS.map((op) => op.value);
|
||||||
|
|
||||||
|
this.handleOptionChange(final);
|
||||||
|
};
|
||||||
|
|
||||||
|
filterTree() {
|
||||||
|
const { tree } = this.props;
|
||||||
|
const { keywords, filterOps } = this.state;
|
||||||
|
|
||||||
|
matchTreeNode(tree.root as TreeNode, keywords, filterOps);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { keywords, filterOps } = this.state;
|
||||||
|
const indeterminate = filterOps.length > 0 && filterOps.length < FILTER_OPTIONS.length;
|
||||||
|
const checkAll = filterOps.length === FILTER_OPTIONS.length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="lc-outline-filter">
|
||||||
|
<Search
|
||||||
|
hasClear
|
||||||
|
shape="simple"
|
||||||
|
placeholder="过滤节点"
|
||||||
|
className="lc-outline-filter-search-input"
|
||||||
|
value={keywords}
|
||||||
|
onChange={this.handleSearchChange}
|
||||||
|
/>
|
||||||
|
<Balloon
|
||||||
|
v2
|
||||||
|
align="br"
|
||||||
|
closable={false}
|
||||||
|
triggerType="hover"
|
||||||
|
trigger={(
|
||||||
|
<div className="lc-outline-filter-icon">
|
||||||
|
<IconFilter />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
checked={checkAll}
|
||||||
|
indeterminate={indeterminate}
|
||||||
|
onChange={this.handleCheckAll}
|
||||||
|
>
|
||||||
|
全选
|
||||||
|
</Checkbox>
|
||||||
|
<Divider />
|
||||||
|
<Checkbox.Group
|
||||||
|
value={filterOps}
|
||||||
|
direction="ver"
|
||||||
|
onChange={this.handleOptionChange}
|
||||||
|
>
|
||||||
|
{FILTER_OPTIONS.map((op) => (
|
||||||
|
<Checkbox id={op.value} value={op.value}>
|
||||||
|
{op.label}
|
||||||
|
</Checkbox>
|
||||||
|
))}
|
||||||
|
</Checkbox.Group>
|
||||||
|
</Balloon>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -5,6 +5,7 @@ import { OutlineMain } from '../main';
|
|||||||
import TreeView from './tree';
|
import TreeView from './tree';
|
||||||
import './style.less';
|
import './style.less';
|
||||||
import { IEditor } from '@alilc/lowcode-types';
|
import { IEditor } from '@alilc/lowcode-types';
|
||||||
|
import Filter from './filter';
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class OutlinePane extends Component<{ config: any; editor: IEditor }> {
|
export class OutlinePane extends Component<{ config: any; editor: IEditor }> {
|
||||||
@ -27,6 +28,7 @@ export class OutlinePane extends Component<{ config: any; editor: IEditor }> {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="lc-outline-pane">
|
<div className="lc-outline-pane">
|
||||||
|
<Filter tree={tree} />
|
||||||
<div ref={(shell) => this.main.mount(shell)} className="lc-outline-tree-container">
|
<div ref={(shell) => this.main.mount(shell)} className="lc-outline-tree-container">
|
||||||
<TreeView key={tree.id} tree={tree} />
|
<TreeView key={tree.id} tree={tree} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -6,13 +6,41 @@
|
|||||||
background-color: white;
|
background-color: white;
|
||||||
|
|
||||||
> .lc-outline-tree-container {
|
> .lc-outline-tree-container {
|
||||||
top: 0;
|
top: 52px;
|
||||||
left: 0;
|
left: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
> .lc-outline-filter {
|
||||||
|
padding: 12px 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: right;
|
||||||
|
|
||||||
|
.lc-outline-filter-search-input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lc-outline-filter-icon {
|
||||||
|
background: #ebecf0;
|
||||||
|
border: 1px solid #c4c6cf;
|
||||||
|
height: 28px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 0 2px 2px 0;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-left: -2px;
|
||||||
|
z-index: 1;
|
||||||
|
padding: 0 6px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.lc-outline-tree {
|
.lc-outline-tree {
|
||||||
|
|||||||
@ -14,8 +14,11 @@ export default class TreeBranches extends Component<{
|
|||||||
render() {
|
render() {
|
||||||
const { treeNode, isModal } = this.props;
|
const { treeNode, isModal } = this.props;
|
||||||
const { expanded } = treeNode;
|
const { expanded } = treeNode;
|
||||||
|
const { filterWorking, matchChild } = treeNode.filterReult;
|
||||||
|
// 条件过滤生效时,如果命中了子节点,需要将该节点展开
|
||||||
|
const expandInFilterResult = filterWorking && matchChild;
|
||||||
|
|
||||||
if (!expanded) {
|
if (!expandInFilterResult && !expanded) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -40,12 +43,18 @@ class TreeNodeChildren extends Component<{
|
|||||||
const children: any = [];
|
const children: any = [];
|
||||||
let groupContents: any[] = [];
|
let groupContents: any[] = [];
|
||||||
let currentGrp: ExclusiveGroup;
|
let currentGrp: ExclusiveGroup;
|
||||||
|
const { filterWorking, matchSelf, keywords } = treeNode.filterReult;
|
||||||
|
|
||||||
const endGroup = () => {
|
const endGroup = () => {
|
||||||
if (groupContents.length > 0) {
|
if (groupContents.length > 0) {
|
||||||
children.push(
|
children.push(
|
||||||
<div key={currentGrp.id} className="condition-group-container" data-id={currentGrp.firstNode.id}>
|
<div key={currentGrp.id} className="condition-group-container" data-id={currentGrp.firstNode.id}>
|
||||||
<div className="condition-group-title">
|
<div className="condition-group-title">
|
||||||
<Title title={currentGrp.title} />
|
<Title
|
||||||
|
title={currentGrp.title}
|
||||||
|
match={filterWorking && matchSelf}
|
||||||
|
keywords={keywords}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
{groupContents}
|
{groupContents}
|
||||||
</div>,
|
</div>,
|
||||||
|
|||||||
@ -32,6 +32,13 @@ export default class TreeNodeView extends Component<{
|
|||||||
highlight: treeNode.isFocusingNode(),
|
highlight: treeNode.isFocusingNode(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { filterWorking, matchChild, matchSelf } = treeNode.filterReult;
|
||||||
|
|
||||||
|
// 条件过滤生效时,如果未命中本节点或子节点,则不展示该节点
|
||||||
|
if (filterWorking && !matchChild && !matchSelf) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={className} data-id={treeNode.id}>
|
<div className={className} data-id={treeNode.id}>
|
||||||
<TreeTitle treeNode={treeNode} isModal={isModal} />
|
<TreeTitle treeNode={treeNode} isModal={isModal} />
|
||||||
|
|||||||
@ -96,6 +96,7 @@ export default class TreeTitle extends Component<{
|
|||||||
marginLeft: -indent,
|
marginLeft: -indent,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
const { filterWorking, matchSelf, keywords } = treeNode.filterReult;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -143,7 +144,11 @@ export default class TreeTitle extends Component<{
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<Title title={treeNode.title} />
|
<Title
|
||||||
|
title={treeNode.title}
|
||||||
|
match={filterWorking && matchSelf}
|
||||||
|
keywords={keywords}
|
||||||
|
/>
|
||||||
{node.slotFor && (
|
{node.slotFor && (
|
||||||
<a className="tree-node-tag slot">
|
<a className="tree-node-tag slot">
|
||||||
{/* todo: click redirect to prop */}
|
{/* todo: click redirect to prop */}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user