import React from 'react'; import {Renderer, RendererEnv, RendererProps} from '../factory'; import getExprProperties from '../utils/filter-schema'; import {filter, evalExpression} from '../utils/tpl'; import { autobind, createObject, findTree, isUnfolded, mapTree, someTree, spliceTree } from '../utils/helper'; import {ScopedContext, IScopedContext} from '../Scoped'; import {themeable, ThemeProps} from '../theme'; import {Icon} from '../components/icons'; import {BaseSchema, SchemaApi, SchemaIcon, SchemaUrlPath} from '../Schema'; import {generateIcon} from '../utils/icon'; import { RemoteOptionsProps, withRemoteConfig } from '../components/WithRemoteConfig'; import {Payload} from '../types'; import Spinner from '../components/Spinner'; export type NavItemSchema = { /** * 文字说明 */ label?: string; /** * 图标类名,参考 fontawesome 4。 */ icon?: SchemaIcon; to?: SchemaUrlPath; unfolded?: boolean; active?: boolean; defer?: boolean; deferApi?: SchemaApi; children?: Array; } & Omit; /** * Nav 导航渲染器 * 文档:https://baidu.gitee.io/amis/docs/components/nav */ export interface NavSchema extends BaseSchema { /** * 指定为 Nav 导航渲染器 */ type: 'nav'; /** * 链接地址集合 */ links?: Array; /** * @default 24 */ indentSize: number; /** * 可以通过 API 拉取。 */ source?: SchemaApi; /** * 懒加载 api,如果不配置复用 source 接口。 */ deferApi?: SchemaApi; /** * true 为垂直排列,false 为水平排列类似如 tabs。 */ stacked?: boolean; } export interface Link { className?: string; label?: string; to?: string; icon?: string; active?: boolean; activeOn?: string; unfolded?: boolean; children?: Links; defer?: boolean; loading?: boolean; loaded?: boolean; [propName: string]: any; } export interface Links extends Array {} export interface NavigationState { links: Links; error?: string; } export interface NavigationProps extends ThemeProps, Omit { onSelect?: (item: Link) => void | false; onToggle?: (item: Link) => void; togglerClassName?: string; links?: Array; loading?: boolean; } export class Navigation extends React.Component< NavigationProps, NavigationState > { static defaultProps = { indentSize: 24 }; @autobind handleClick(link: Link) { this.props.onSelect?.(link); } @autobind toggleLink(target: Link) { this.props.onToggle?.(target); } renderItem(link: Link, index: number, depth = 1) { if (link.hidden === true || link.visible === false) { return null; } const isActive: boolean = !!link.active; const {disabled, togglerClassName, classnames: cx, indentSize} = this.props; const hasSub = (link.defer && !link.loaded) || (link.children && link.children.length); return (
  • {generateIcon(cx, link.icon, 'Nav-itemIcon')} {link.label} {link.loading ? ( ) : hasSub ? ( this.toggleLink(link)} className={cx('Nav-itemToggler', togglerClassName)} > ) : null} {Array.isArray(link.children) && link.children.length ? (
      {link.children.map((link, index) => this.renderItem(link, index, depth + 1) )}
    ) : null}
  • ); } render(): JSX.Element { const {className, stacked, classnames: cx, links, loading} = this.props; return (
      {Array.isArray(links) ? links.map((item, index) => this.renderItem(item, index)) : null}
    ); } } const ThemedNavigation = themeable(Navigation); const ConditionBuilderWithRemoteOptions = withRemoteConfig({ adaptor: (config: any, props: any) => { const links = Array.isArray(config) ? config : config.links || config.options || config.items || config.rows; if (!Array.isArray(links)) { throw new Error('payload.data.options is not array.'); } return links; }, afterLoad: (response: any, config: any, props: any) => { if (response.value && !someTree(config, item => item.active)) { const {env} = props; env.jumpTo(filter(response.value as string, props.data)); } }, normalizeConfig( links: Array, origin: Array | undefined, props: any, motivation?: string ) { if (Array.isArray(links) && motivation !== 'toggle') { const {data, env, unfoldedField, foldedField} = props; links = mapTree( links, (link: Link) => { const item: any = { ...link, ...getExprProperties(link, data as object), active: (motivation !== 'location-change' && link.active) || (link.activeOn ? evalExpression(link.activeOn as string, data) : !!( link.hasOwnProperty('to') && env && env.isCurrentUrl(filter(link.to as string, data)) )) }; item.unfolded = isUnfolded(item, {unfoldedField, foldedField}) || (link.children && link.children.some(link => !!link.active)); return item; }, 1, true ); } return links; }, beforeDeferLoad(item: Link, indexes: Array, links: Array) { return spliceTree(links, indexes, 1, { ...item, loading: true }); }, afterDeferLoad( item: Link, indexes: Array, ret: Payload, links: Array ) { const newItem = { ...item, loading: false, loaded: true, error: ret.ok ? undefined : ret.msg }; const children = Array.isArray(ret.data) ? ret.data : ret.data.links || ret.data.options || ret.data.items || ret.data.rows; if (Array.isArray(children)) { newItem.children = children.concat(); newItem.unfolded = true; } return spliceTree(links, indexes, 1, newItem); } })( class extends React.Component< RemoteOptionsProps & React.ComponentProps & { location?: any; env?: RendererEnv; data?: any; unfoldedField?: string; foldedField?: string; } > { constructor(props: any) { super(props); this.toggleLink = this.toggleLink.bind(this); this.handleSelect = this.handleSelect.bind(this); } componentDidMount() { if (Array.isArray(this.props.links)) { this.props.updateConfig(this.props.links, 'mount'); } } componentDidUpdate(prevProps: any) { if (this.props.location !== prevProps.location) { this.props.updateConfig(this.props.config, 'location-change'); } else if (this.props.links !== prevProps.links) { this.props.updateConfig(this.props.links, 'update'); } } toggleLink(target: Link) { const {config, updateConfig, deferLoad} = this.props; if (target.defer && !target.loaded) { deferLoad(target); } else { updateConfig( mapTree(config, (link: Link) => target === link ? { ...link, unfolded: !link.unfolded } : link ), 'toggle' ); } } handleSelect(link: Link) { const {onSelect, env, data} = this.props; if (onSelect && onSelect(link) === false) { return; } if ( !link.to && ((link.children && link.children.length) || (link.defer && !link.loaded)) ) { this.toggleLink(link); return; } env?.jumpTo(filter(link.to as string, data), link as any); } render() { const {loading, config, deferLoad, updateConfig, ...rest} = this.props; return ( ); } } ); export default ThemedNavigation; @Renderer({ test: /(^|\/)(?:nav|navigation)$/, name: 'nav' }) export class NavigationRenderer extends React.Component { static contextType = ScopedContext; remoteRef: | { loadConfig: (ctx?: any) => Promise | void; setConfig: (value: any) => void; } | undefined = undefined; @autobind remoteConfigRef(ref: any) { this.remoteRef = ref; } componentWillMount() { const scoped = this.context as IScopedContext; scoped.registerComponent(this); } componentWillUnmount() { const scoped = this.context as IScopedContext; scoped.unRegisterComponent(this); } @autobind reload(target?: string, query?: any, values?: object) { if (query) { return this.receive(query); } const {data, env, source, translate: __} = this.props; const finalData = values ? createObject(data, values) : data; this.remoteRef?.loadConfig(finalData); } @autobind receive(values: object) { this.reload(undefined, undefined, values); } render() { const {...rest} = this.props; return ( ); } }