import React from 'react'; import {Renderer, RendererProps} from '../factory'; import {Action, Schema, SchemaNode} from '../types'; import find from 'lodash/find'; import { isVisible, autobind, isDisabled, isObject, createObject, getVariable } from '../utils/helper'; import findIndex from 'lodash/findIndex'; import {Tabs as CTabs, Tab} from '../components/Tabs'; import {ClassNamesFn} from '../theme'; import { BaseSchema, SchemaClassName, SchemaCollection, SchemaIcon } from '../Schema'; import {ActionSchema} from './Action'; import {filter} from '../utils/tpl'; import {resolveVariable, tokenize} from '../utils/tpl-builtin'; export interface TabSchema extends Omit { /** * Tab 标题 */ title?: string; /** * 内容 * @deprecated 用 body 属性 */ tab?: SchemaCollection; /** * 内容 */ body?: SchemaCollection; /** * 徽标 */ badge?: number; /** * 设置以后将跟url的hash对应 */ hash?: string; /** * 按钮图标 */ icon?: SchemaIcon; iconPosition?: 'left' | 'right'; /** * 设置以后内容每次都会重新渲染 */ reload?: boolean; /** * 点开时才加载卡片内容 */ mountOnEnter?: boolean; /** * 卡片隐藏就销毁卡片节点。 */ unmountOnExit?: boolean; } /** * 选项卡控件。 * 文档:https://baidu.gitee.io/amis/docs/components/tabs */ export interface TabsSchema extends BaseSchema { type: 'tabs'; /** * 选项卡成员。当配置了 source 时,选项卡成员,将会根据目标数据进行重复。 */ tabs: Array; /** * 关联已有数据,选项卡直接根据目标数据重复。 */ source?: string; /** * 类名 */ tabsClassName?: SchemaClassName; /** * 展示形式 */ tabsMode?: '' | 'line' | 'card' | 'radio' | 'vertical' | 'tiled'; /** * 内容类名 */ contentClassName?: SchemaClassName; /** * 卡片是否只有在点开的时候加载? */ mountOnEnter?: boolean; /** * 卡片隐藏的时候是否销毁卡片内容 */ unmountOnExit?: boolean; /** * 可以在右侧配置点其他功能按钮。 */ toolbar?: ActionSchema; } export interface TabsProps extends RendererProps, Omit { activeKey?: string | number; location?: any; tabRender?: (tab: TabSchema, props: TabsProps, index: number) => JSX.Element; } export interface TabsState { activeKey: any; prevKey: any; } export default class Tabs extends React.Component { static defaultProps: Partial = { className: '', mode: '', mountOnEnter: true, unmountOnExit: false }; renderTab?: (tab: TabSchema, props: TabsProps, index: number) => JSX.Element; activeKey: any; constructor(props: TabsProps) { super(props); const location = props.location || window.location; const tabs = props.tabs; let activeKey: any = 0; if (typeof props.activeKey !== 'undefined') { activeKey = typeof props.activeKey === 'string' ? tokenize(props.activeKey, props.data) : props.activeKey; } else if (location && Array.isArray(tabs)) { const hash = location.hash.substring(1); const tab: TabSchema = find(tabs, tab => tab.hash === hash) as TabSchema; activeKey = tab && tab.hash ? tab.hash : (tabs[0] && tabs[0].hash) || 0; } this.state = { prevKey: undefined, activeKey: (this.activeKey = activeKey) }; } componentDidMount() { this.autoJumpToNeighbour(this.activeKey); let {name, value, onChange, source, tabs, data} = this.props; // 如果没有配置 name ,说明不需要同步表单值 if ( !name || typeof onChange !== 'function' || // 如果关联某个变量数据,则不启用 source ) { return; } value = value ?? getVariable(data, name); // 如果有值,切到对应的 tab if (value && Array.isArray(tabs)) { const key = this.resolveKeyByValue(value); key !== undefined && this.handleSelect(key); } else { const tab = this.resolveTabByKey(this.activeKey); if (tab && value !== ((tab as any).value ?? tab.title)) { onChange((tab as any).value ?? tab.title, name); } } } componentDidUpdate(preProps: TabsProps, prevState: any) { const props = this.props; if (props.location && props.location.hash !== preProps.location.hash) { const hash = props.location.hash.substring(1); if (!hash) { return; } const tab: TabSchema = find( props.tabs, tab => tab.hash === hash ) as TabSchema; if (tab && tab.hash && tab.hash !== this.state.activeKey) { this.setState({ activeKey: (this.activeKey = tab.hash), prevKey: this.state.activeKey }); } } else if (preProps.tabs !== props.tabs) { let activeKey: any = this.state.activeKey; const location = props.location; let tab: TabSchema | null = null; if (location && Array.isArray(props.tabs)) { const hash = location.hash.substring(1); tab = find(props.tabs, tab => tab.hash === hash) as TabSchema; } if (tab) { activeKey = tab.hash; } else if ( !props.tabs || !props.tabs.some((item, index) => item.hash ? item.hash === activeKey : index === activeKey ) ) { activeKey = (props.tabs && props.tabs[0] && props.tabs[0].hash) || 0; } this.setState({ prevKey: undefined, activeKey: (this.activeKey = activeKey) }); } this.autoJumpToNeighbour(this.activeKey); let {name, value, onChange, source, data} = this.props; // 如果没有配置 name ,说明不需要同步表单值 if ( !name || typeof onChange !== 'function' || // 如果关联某个变量数据,则不启用 source ) { return; } let key: any; value = value ?? getVariable(data, name); const prevValue = preProps.value ?? getVariable(preProps.data, preProps.name); if ( value !== prevValue && (key = this.resolveKeyByValue(value)) !== undefined && key !== this.activeKey ) { this.handleSelect(key); } else if (this.activeKey !== prevState.activeKey) { const tab = this.resolveTabByKey(this.activeKey); if (tab && value !== ((tab as any).value ?? tab.title)) { onChange((tab as any).value ?? tab.title, name); } } } resolveTabByKey(key: any) { const tabs = this.props.tabs; if (!Array.isArray(tabs)) { return; } return find(tabs, (tab: TabSchema, index) => tab.hash ? tab.hash === key : index === key ); } resolveKeyByValue(value: any) { const tabs = this.props.tabs; if (!Array.isArray(tabs)) { return; } const tab: TabSchema = find( tabs, tab => ((tab as any).value ?? tab.title) === value ) as TabSchema; return tab && tab.hash ? tab.hash : tabs.indexOf(tab); } @autobind autoJumpToNeighbour(key: any) { const {tabs, data} = this.props; if (!Array.isArray(tabs)) { return; } // 当前 tab 可能不可见,所以需要自动切到一个可见的 tab, 向前找,找一圈 const tabIndex = findIndex(tabs, (tab: TabSchema, index) => tab.hash ? tab.hash === key : index === key ); if (tabs[tabIndex] && !isVisible(tabs[tabIndex], this.props.data)) { let len = tabs.length; let i = tabIndex - 1 + len; let tries = len - 1; while (tries--) { const index = i-- % len; if (isVisible(tabs[index], data)) { let activeKey = tabs[index].hash || index; this.setState({ activeKey: (this.activeKey = activeKey) }); break; } } } } @autobind handleSelect(key: any) { const {env} = this.props; // 是 hash,需要更新到地址栏 if (typeof key === 'string' && env) { env.updateLocation(`#${key}`); } else if (typeof this.state.activeKey === 'string' && env) { env.updateLocation(`#`); } this.setState({ activeKey: (this.activeKey = key), prevKey: this.state.activeKey }); } @autobind switchTo(index: number) { const {tabs} = this.props; Array.isArray(tabs) && tabs[index] && this.setState({ activeKey: (this.activeKey = tabs[index].hash || index) }); } @autobind currentIndex(): number { const {tabs} = this.props; return Array.isArray(tabs) ? findIndex(tabs, (tab: TabSchema, index) => tab.hash ? tab.hash === this.state.activeKey : index === this.state.activeKey ) : -1; } renderToolbar() { const {toolbar, render, classnames: cx, toolbarClassName} = this.props; return toolbar ? (
{render('toolbar', toolbar)}
) : null; } renderTabs() { const { classnames: cx, classPrefix: ns, contentClassName, tabRender, className, render, data, mode: dMode, tabsMode, unmountOnExit, source, formStore } = this.props; const mode = tabsMode || dMode; const arr = resolveVariable(source, data); let mountOnEnter = this.props.mountOnEnter; // 如果在form下面,其他tabs默认需要渲染出来 // 否则在其他 tab 下面的必填项检测不到 if (formStore) { mountOnEnter = false; } let tabs = this.props.tabs; if (!tabs) { return null; } tabs = Array.isArray(tabs) ? tabs : [tabs]; let children: Array = []; if (Array.isArray(arr)) { arr.forEach((value, index) => { const ctx = createObject( data, isObject(value) ? {index, ...value} : {item: value, index} ); children.push( ...tabs.map((tab, tabIndex) => isVisible(tab, ctx) ? ( {render( `item/${index}/${tabIndex}`, tab.tab || tab.body || '', { data: ctx } )} ) : null ) ); }); } else { children = tabs.map((tab, index) => isVisible(tab, data) ? ( {this.renderTab ? this.renderTab(tab, this.props, index) : tabRender ? tabRender(tab, this.props, index) : render(`tab/${index}`, tab.tab || tab.body || '')} ) : null ); } return ( {children} ); } render() { return this.renderTabs(); } } @Renderer({ type: 'tabs', name: 'tabs' }) export class TabsRenderer extends Tabs {}