feat: AnchorNav组件支持树形结构 (#8969)

Co-authored-by: yanglu19 <yanglu19@baidu.com>
This commit is contained in:
Dora 2024-01-02 20:33:14 +08:00 committed by GitHub
parent d9ace501e3
commit d9fb3ddb75
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 844 additions and 36 deletions

View File

@ -12,6 +12,8 @@ order: 68
## 基本用法 ## 基本用法
`6.1.0`及以上版本垂直方向支持配置子节点
```schema: scope="body" ```schema: scope="body"
{ {
"type": "anchor-nav", "type": "anchor-nav",
@ -39,6 +41,24 @@ order: 68
}, },
{ {
"title": "工作信息", "title": "工作信息",
"children": [
{
"title": "工作信息-1",
"type": "form",
"body": [
{
"type": "input-text",
"name": "workInfo1-1",
"label": "工作信息1-1"
},
{
"name": "workInfo1-2",
"type": "input-text",
"label": "工作信息1-2"
}
]
}
],
"body": [ "body": [
{ {
"type": "form", "type": "form",
@ -60,6 +80,29 @@ order: 68
}, },
{ {
"title": "兴趣爱好", "title": "兴趣爱好",
"children": [
{
"title": "兴趣爱好1-1",
"body": [
{
"type": "form",
"title": "兴趣爱好1-1",
"body": [
{
"type": "input-text",
"name": "interest1-1",
"label": "interest1-1"
},
{
"name": "interest1-2",
"type": "input-text",
"label": "interest1-2"
}
]
}
]
}
],
"body": [ "body": [
{ {
"type": "form", "type": "form",
@ -235,6 +278,25 @@ order: 68
{ {
"title": "工作信息", "title": "工作信息",
"href": "work", "href": "work",
"children": [
{
"title": "工作信息-1",
"href": "work-1",
"type": "form",
"body": [
{
"type": "input-text",
"name": "workInfo1-1",
"label": "工作信息1-1"
},
{
"name": "workInfo1-2",
"type": "input-text",
"label": "工作信息1-2"
}
]
}
],
"body": [ "body": [
{ {
"type": "form", "type": "form",
@ -332,6 +394,24 @@ order: 68
}, },
{ {
"title": "工作信息", "title": "工作信息",
"children": [
{
"title": "工作信息-1",
"type": "form",
"body": [
{
"type": "input-text",
"name": "workInfo1-1",
"label": "工作信息1-1"
},
{
"name": "workInfo1-2",
"type": "input-text",
"label": "工作信息1-2"
}
]
}
],
"body": [ "body": [
{ {
"type": "form", "type": "form",
@ -409,5 +489,5 @@ order: 68
| active | `string` | | 需要定位的区域 | | active | `string` | | 需要定位的区域 |
| links[x].title | `string` | | 区域 标题 | | links[x].title | `string` | | 区域 标题 |
| links[x].href | `string` | | 区域 标识 | | links[x].href | `string` | | 区域 标识 |
| links[x].body | [SchemaNode](../types/schemanode) | | 区域 内容区 | | links[x].body | [SchemaNode](../types/schemanode) | | 区域 内容区`6.1.0`及以上版本垂直方向支持配置子节点 |
| links[x].className | `string` | `"bg-white b-l b-r b-b wrapper-md"` | 区域成员 样式 | | links[x].className | `string` | `"bg-white b-l b-r b-b wrapper-md"` | 区域成员 样式 |

View File

@ -13,6 +13,9 @@
> .#{$ns}AnchorNav-link { > .#{$ns}AnchorNav-link {
position: relative; position: relative;
display: block; display: block;
&.#{$ns}AnchorNav-link-child {
margin-left: px2rem(16px);
}
> a { > a {
display: block; display: block;

View File

@ -84,7 +84,6 @@ export class AnchorNav extends React.Component<AnchorNavProps, AnchorNavState> {
const sectionRootDom = const sectionRootDom =
this.contentDom && (this.contentDom.current as HTMLElement); this.contentDom && (this.contentDom.current as HTMLElement);
this.updateSectionOffset(sectionRootDom, false); this.updateSectionOffset(sectionRootDom, false);
this.observer = new MutationObserver(() => this.observer = new MutationObserver(() =>
// TODO: 牺牲性能 // TODO: 牺牲性能
@ -185,7 +184,6 @@ export class AnchorNav extends React.Component<AnchorNavProps, AnchorNavState> {
this.setState({fromSelect: true}); this.setState({fromSelect: true});
// 滚动到对应段落 // 滚动到对应段落
this.scrollToSection(key); this.scrollToSection(key);
const sectionRootDom = const sectionRootDom =
this.contentDom && (this.contentDom.current as HTMLElement); this.contentDom && (this.contentDom.current as HTMLElement);
@ -215,11 +213,17 @@ export class AnchorNav extends React.Component<AnchorNavProps, AnchorNavState> {
const {classnames: cx, active: activeProp} = this.props; const {classnames: cx, active: activeProp} = this.props;
const {title, name} = link.props; const {title, name} = link.props;
const active = activeProp === undefined && index === 0 ? name : activeProp; const active = activeProp === undefined && index === 0 ? name : activeProp;
// 判断是否为子节点子节点key为 <父节点索引>-<子节点索引>
const isChild = link.key?.split('-').length >= 2;
return ( return (
<li <li
className={cx('AnchorNav-link', active === name ? 'is-active' : '')} className={cx(
key={index} 'AnchorNav-link',
isChild ? 'AnchorNav-link-child' : '',
String(active) === String(name) ? 'is-active' : ''
)}
key={link.key}
onClick={() => this.handleSelect(name)} onClick={() => this.handleSelect(name)}
> >
<a title={title}>{title}</a> <a title={title}>{title}</a>
@ -234,11 +238,11 @@ export class AnchorNav extends React.Component<AnchorNavProps, AnchorNavState> {
const {active: activeProp, classnames} = this.props; const {active: activeProp, classnames} = this.props;
const name = section.props.name; const name = section.props.name;
const key = section.key;
const active = activeProp === undefined && index === 0 ? name : activeProp; const active = activeProp === undefined && index === 0 ? name : activeProp;
return React.cloneElement(section, { return React.cloneElement(section, {
...section.props, ...section.props,
key: index, key,
classnames, classnames,
active active
}); });

View File

@ -206,3 +206,120 @@ test('Renderer:anchorNav with active', async () => {
container.querySelector('.two .cxd-AnchorNav-link.is-active') container.querySelector('.two .cxd-AnchorNav-link.is-active')
).toHaveTextContent('工作信息'); ).toHaveTextContent('工作信息');
}); });
// 4. 子菜单
test('Renderer:anchorNav with children', async () => {
const {container} = render(
amisRender(
{
type: 'page',
body: {
type: 'anchor-nav',
direction: 'horizontal',
links: [
{
title: '基本信息',
body: [
{
type: 'form',
title: '基本信息',
body: [
{
type: 'input-text',
name: 'name',
label: '姓名:'
},
{
name: 'email',
type: 'input-email',
label: '邮箱:'
}
]
}
],
children: [
{
title: '基本信息1-1',
body: [
{
type: 'form',
title: '基本信息1-1',
body: [
{
type: 'input-text',
name: 'cname1',
label: 'cname1'
},
{
name: 'caddress1',
type: 'input-text',
label: 'caddress1'
}
]
}
]
},
{
title: '基本信息1-2',
body: [
{
type: 'form',
title: '基本信息1-2',
body: [
{
type: 'input-text',
name: 'cname2',
label: 'cname2'
},
{
name: 'caddress2',
type: 'input-text',
label: 'caddress2'
}
]
}
]
}
]
},
{
title: '工作信息',
body: [
{
type: 'form',
title: '工作信息',
body: [
{
type: 'input-text',
name: 'cname',
label: '公司名称:'
},
{
name: 'caddress',
type: 'input-text',
label: '公司地址:'
}
]
}
]
}
]
}
},
{},
makeEnv({})
)
);
await wait(500);
expect(
container.querySelector('.cxd-AnchorNav-link-child')
).toHaveTextContent('基本信息1-1');
expect(
container.querySelector('.cxd-AnchorNav-link.is-active')
).toHaveTextContent('基本信息');
expect(container).toMatchSnapshot();
});

View File

@ -568,6 +568,565 @@ exports[`Renderer:anchorNav 1`] = `
</div> </div>
`; `;
exports[`Renderer:anchorNav with children 1`] = `
<div>
<div
class="cxd-Page"
>
<div
class="cxd-Page-content"
>
<div
class="cxd-Page-main"
>
<div
class="cxd-Page-body"
role="page-body"
>
<div
class="cxd-AnchorNav cxd-AnchorNav--horizontal"
>
<ul
class="cxd-AnchorNav-link-wrap"
role="anchorlist"
>
<li
class="cxd-AnchorNav-link is-active"
>
<a
title="基本信息"
>
基本信息
</a>
</li>
<li
class="cxd-AnchorNav-link cxd-AnchorNav-link-child"
>
<a
title="基本信息1-1"
>
基本信息1-1
</a>
</li>
<li
class="cxd-AnchorNav-link cxd-AnchorNav-link-child"
>
<a
title="基本信息1-2"
>
基本信息1-2
</a>
</li>
<li
class="cxd-AnchorNav-link"
>
<a
title="工作信息"
>
工作信息
</a>
</li>
</ul>
<div
class="cxd-AnchorNav-section-wrap"
>
<div
class="cxd-AnchorNav-section"
>
<div
class="cxd-Panel cxd-Panel--default cxd-Panel--form"
>
<div
class="cxd-Panel-heading"
>
<h3
class="cxd-Panel-title"
>
<span
class="cxd-TplField"
>
<span>
基本信息
</span>
</span>
</h3>
</div>
<div
class="cxd-Panel-body"
>
<form
class="cxd-Form cxd-Form--normal"
novalidate=""
>
<input
style="display: none;"
type="submit"
/>
<div
class="cxd-Form-item cxd-Form-item--normal"
data-role="form-item"
>
<label
class="cxd-Form-label"
>
<span>
<span
class="cxd-TplField"
>
<span>
姓名:
</span>
</span>
</span>
</label>
<div
class="cxd-Form-control cxd-TextControl"
>
<div
class="cxd-TextControl-input"
>
<input
autocomplete="off"
class=""
name="name"
placeholder=""
size="10"
type="text"
value=""
/>
</div>
</div>
</div>
<div
class="cxd-Form-item cxd-Form-item--normal"
data-role="form-item"
>
<label
class="cxd-Form-label"
>
<span>
<span
class="cxd-TplField"
>
<span>
邮箱:
</span>
</span>
</span>
</label>
<div
class="cxd-Form-control cxd-TextControl"
>
<div
class="cxd-TextControl-input"
>
<input
autocomplete="off"
class=""
name="email"
placeholder=""
size="10"
type="email"
value=""
/>
</div>
</div>
</div>
</form>
</div>
<div
class="cxd-Panel-footerWrap"
>
<div
class="cxd-Panel-btnToolbar cxd-Panel-footer"
>
<button
class="cxd-Button cxd-Button--primary cxd-Button--size-default"
type="submit"
>
<span>
提交
</span>
</button>
</div>
</div>
</div>
</div>
<div
class="cxd-AnchorNav-section"
>
<div
class="cxd-Panel cxd-Panel--default cxd-Panel--form"
>
<div
class="cxd-Panel-heading"
>
<h3
class="cxd-Panel-title"
>
<span
class="cxd-TplField"
>
<span>
基本信息1-1
</span>
</span>
</h3>
</div>
<div
class="cxd-Panel-body"
>
<form
class="cxd-Form cxd-Form--normal"
novalidate=""
>
<input
style="display: none;"
type="submit"
/>
<div
class="cxd-Form-item cxd-Form-item--normal"
data-role="form-item"
>
<label
class="cxd-Form-label"
>
<span>
<span
class="cxd-TplField"
>
<span>
cname1
</span>
</span>
</span>
</label>
<div
class="cxd-Form-control cxd-TextControl"
>
<div
class="cxd-TextControl-input"
>
<input
autocomplete="off"
class=""
name="cname1"
placeholder=""
size="10"
type="text"
value=""
/>
</div>
</div>
</div>
<div
class="cxd-Form-item cxd-Form-item--normal"
data-role="form-item"
>
<label
class="cxd-Form-label"
>
<span>
<span
class="cxd-TplField"
>
<span>
caddress1
</span>
</span>
</span>
</label>
<div
class="cxd-Form-control cxd-TextControl"
>
<div
class="cxd-TextControl-input"
>
<input
autocomplete="off"
class=""
name="caddress1"
placeholder=""
size="10"
type="text"
value=""
/>
</div>
</div>
</div>
</form>
</div>
<div
class="cxd-Panel-footerWrap"
>
<div
class="cxd-Panel-btnToolbar cxd-Panel-footer"
>
<button
class="cxd-Button cxd-Button--primary cxd-Button--size-default"
type="submit"
>
<span>
提交
</span>
</button>
</div>
</div>
</div>
</div>
<div
class="cxd-AnchorNav-section"
>
<div
class="cxd-Panel cxd-Panel--default cxd-Panel--form"
>
<div
class="cxd-Panel-heading"
>
<h3
class="cxd-Panel-title"
>
<span
class="cxd-TplField"
>
<span>
基本信息1-2
</span>
</span>
</h3>
</div>
<div
class="cxd-Panel-body"
>
<form
class="cxd-Form cxd-Form--normal"
novalidate=""
>
<input
style="display: none;"
type="submit"
/>
<div
class="cxd-Form-item cxd-Form-item--normal"
data-role="form-item"
>
<label
class="cxd-Form-label"
>
<span>
<span
class="cxd-TplField"
>
<span>
cname2
</span>
</span>
</span>
</label>
<div
class="cxd-Form-control cxd-TextControl"
>
<div
class="cxd-TextControl-input"
>
<input
autocomplete="off"
class=""
name="cname2"
placeholder=""
size="10"
type="text"
value=""
/>
</div>
</div>
</div>
<div
class="cxd-Form-item cxd-Form-item--normal"
data-role="form-item"
>
<label
class="cxd-Form-label"
>
<span>
<span
class="cxd-TplField"
>
<span>
caddress2
</span>
</span>
</span>
</label>
<div
class="cxd-Form-control cxd-TextControl"
>
<div
class="cxd-TextControl-input"
>
<input
autocomplete="off"
class=""
name="caddress2"
placeholder=""
size="10"
type="text"
value=""
/>
</div>
</div>
</div>
</form>
</div>
<div
class="cxd-Panel-footerWrap"
>
<div
class="cxd-Panel-btnToolbar cxd-Panel-footer"
>
<button
class="cxd-Button cxd-Button--primary cxd-Button--size-default"
type="submit"
>
<span>
提交
</span>
</button>
</div>
</div>
</div>
</div>
<div
class="cxd-AnchorNav-section"
>
<div
class="cxd-Panel cxd-Panel--default cxd-Panel--form"
>
<div
class="cxd-Panel-heading"
>
<h3
class="cxd-Panel-title"
>
<span
class="cxd-TplField"
>
<span>
工作信息
</span>
</span>
</h3>
</div>
<div
class="cxd-Panel-body"
>
<form
class="cxd-Form cxd-Form--normal"
novalidate=""
>
<input
style="display: none;"
type="submit"
/>
<div
class="cxd-Form-item cxd-Form-item--normal"
data-role="form-item"
>
<label
class="cxd-Form-label"
>
<span>
<span
class="cxd-TplField"
>
<span>
公司名称:
</span>
</span>
</span>
</label>
<div
class="cxd-Form-control cxd-TextControl"
>
<div
class="cxd-TextControl-input"
>
<input
autocomplete="off"
class=""
name="cname"
placeholder=""
size="10"
type="text"
value=""
/>
</div>
</div>
</div>
<div
class="cxd-Form-item cxd-Form-item--normal"
data-role="form-item"
>
<label
class="cxd-Form-label"
>
<span>
<span
class="cxd-TplField"
>
<span>
公司地址:
</span>
</span>
</span>
</label>
<div
class="cxd-Form-control cxd-TextControl"
>
<div
class="cxd-TextControl-input"
>
<input
autocomplete="off"
class=""
name="caddress"
placeholder=""
size="10"
type="text"
value=""
/>
</div>
</div>
</div>
</form>
</div>
<div
class="cxd-Panel-footerWrap"
>
<div
class="cxd-Panel-btnToolbar cxd-Panel-footer"
>
<button
class="cxd-Button cxd-Button--primary cxd-Button--size-default"
type="submit"
>
<span>
提交
</span>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
`;
exports[`Renderer:anchorNav with horizontal 1`] = ` exports[`Renderer:anchorNav with horizontal 1`] = `
<div> <div>
<div <div

View File

@ -3,7 +3,6 @@ import {Renderer, RendererProps} from 'amis-core';
import {AnchorNav as CAnchorNav, AnchorNavSection} from 'amis-ui'; import {AnchorNav as CAnchorNav, AnchorNavSection} from 'amis-ui';
import {isVisible, autobind} from 'amis-core'; import {isVisible, autobind} from 'amis-core';
import {filter} from 'amis-core'; import {filter} from 'amis-core';
import find from 'lodash/find';
import {BaseSchema, SchemaClassName, SchemaCollection} from '../Schema'; import {BaseSchema, SchemaClassName, SchemaCollection} from '../Schema';
/** /**
@ -26,6 +25,11 @@ export type AnchorNavSectionSchema = {
* *
*/ */
body?: SchemaCollection; body?: SchemaCollection;
/**
*
*/
children?: Array<AnchorNavSectionSchema>;
} & Omit<BaseSchema, 'type'>; } & Omit<BaseSchema, 'type'>;
/** /**
@ -73,7 +77,7 @@ export interface AnchorNavProps
sectionRender?: ( sectionRender?: (
section: AnchorNavSectionSchema, section: AnchorNavSectionSchema,
props: AnchorNavProps, props: AnchorNavProps,
index: number index: number | string
) => JSX.Element; ) => JSX.Element;
} }
@ -94,7 +98,7 @@ export default class AnchorNav extends React.Component<
renderSection?: ( renderSection?: (
section: AnchorNavSectionSchema, section: AnchorNavSectionSchema,
props: AnchorNavProps, props: AnchorNavProps,
index: number index: number | string
) => JSX.Element; ) => JSX.Element;
constructor(props: AnchorNavProps) { constructor(props: AnchorNavProps) {
@ -107,21 +111,43 @@ export default class AnchorNav extends React.Component<
if (typeof props.active !== 'undefined') { if (typeof props.active !== 'undefined') {
active = props.active; active = props.active;
} else { } else {
const section: AnchorNavSectionSchema = find( let section: AnchorNavSectionSchema | null = this.getActiveSection(
links, links,
section => section.href === props.active props.active,
) as AnchorNavSectionSchema; null
);
active = active =
section && section.href section && section.href
? section.href ? section.href
: (links[0] && links[0].href) || 0; : (links[0] && links[0].href) || 0;
} }
this.state = { this.state = {
active active
}; };
} }
// 获取激活的内容区
getActiveSection(
links: Array<AnchorNavSectionSchema>,
active: string | number | undefined,
section: AnchorNavSectionSchema | null
) {
if (section) {
return section;
}
links.forEach(link => {
if (link.href === active) {
section = link;
} else {
if (link.children) {
this.getActiveSection(link.children, active, section);
}
}
});
return section;
}
@autobind @autobind
handleSelect(key: any) { handleSelect(key: any) {
this.setState({ this.setState({
@ -132,7 +158,6 @@ export default class AnchorNav extends React.Component<
@autobind @autobind
locateTo(index: number) { locateTo(index: number) {
const {links} = this.props; const {links} = this.props;
Array.isArray(links) && Array.isArray(links) &&
links[index] && links[index] &&
this.setState({ this.setState({
@ -140,6 +165,46 @@ export default class AnchorNav extends React.Component<
}); });
} }
renderSections(links: AnchorNavSectionSchema[], parentIdx?: string | number) {
const {
classnames: cx,
classPrefix: ns,
sectionRender,
render,
data
} = this.props;
links = Array.isArray(links) ? links : [links];
let children: Array<JSX.Element | null> = [];
links.forEach((section, index) => {
if (isVisible(section, data)) {
// 若有子节点key为parentIdx-index
let curIdx = (parentIdx ? parentIdx + '-' : '') + index;
children.push(
/** 内容区 */
<AnchorNavSection
{...(section as any)}
title={filter(section.title, data)}
key={curIdx}
name={section.href || curIdx}
>
{this.renderSection
? this.renderSection(section, this.props, curIdx)
: sectionRender
? sectionRender(section, this.props, curIdx)
: render(`section/${curIdx}`, section.body || '')}
</AnchorNavSection>
);
if (section.children) {
children.push(...this.renderSections(section.children, curIdx));
}
}
});
return children.filter(item => !!item);
}
render() { render() {
const { const {
classnames: cx, classnames: cx,
@ -159,27 +224,7 @@ export default class AnchorNav extends React.Component<
return null; return null;
} }
links = Array.isArray(links) ? links : [links]; let children = this.renderSections(links);
let children: Array<JSX.Element | null> = [];
children = links
.map((section, index) =>
isVisible(section, data) ? (
<AnchorNavSection
{...(section as any)}
title={filter(section.title, data)}
key={index}
name={section.href || index}
>
{this.renderSection
? this.renderSection(section, this.props, index)
: sectionRender
? sectionRender(section, this.props, index)
: render(`section/${index}`, section.body || '')}
</AnchorNavSection>
) : null
)
.filter(item => !!item);
return ( return (
<CAnchorNav <CAnchorNav