diff --git a/packages/amis-core/src/SchemaRenderer.tsx b/packages/amis-core/src/SchemaRenderer.tsx index 601fe639abe..fb2510dbff3 100644 --- a/packages/amis-core/src/SchemaRenderer.tsx +++ b/packages/amis-core/src/SchemaRenderer.tsx @@ -1,4 +1,5 @@ import difference from 'lodash/difference'; +import findLastIndex from 'lodash/findLastIndex'; import omit from 'lodash/omit'; import React from 'react'; import {isValidElementType} from 'react-is'; @@ -42,6 +43,9 @@ import {evalExpression, filter} from './utils/tpl'; import Animations from './components/Animations'; import {cloneObject} from './utils/object'; import {observeGlobalVars} from './globalVar'; +import type {IRootStore} from './store/root'; +import {createObjectFromChain, extractObjectChain} from './utils'; +import {IIRendererStore} from './store/index'; interface SchemaRendererProps extends Partial>, @@ -110,6 +114,8 @@ export class SchemaRenderer extends React.Component { unbindGlobalEvent: (() => void) | undefined = undefined; isStatic: any = undefined; + subStore?: IIRendererStore | null; + constructor(props: SchemaRendererProps) { super(props); @@ -118,6 +124,7 @@ export class SchemaRenderer extends React.Component { this.reRender = this.reRender.bind(this); this.resolveRenderer(this.props); this.dispatchEvent = this.dispatchEvent.bind(this); + this.storeRef = this.storeRef.bind(this); this.handleGlobalVarChange = this.handleGlobalVarChange.bind(this); const schema = props.schema; @@ -183,19 +190,57 @@ export class SchemaRenderer extends React.Component { return false; } + storeRef(store: IIRendererStore | null) { + this.subStore = store; + } + handleGlobalVarChange() { const handler = this.renderer?.onGlobalVarChanged; - const newData = cloneObject(this.props.data); + const topStore: IRootStore = this.props.topStore; + const chain = extractObjectChain(this.props.data).filter( + (item: any) => !item.hasOwnProperty('__isTempGlobalLayer') + ); + const globalLayerIdx = findLastIndex( + chain, + item => + item.hasOwnProperty('global') || item.hasOwnProperty('globalState') + ); + + const globalData = { + ...topStore.nextGlobalData, + + // 兼容旧的全局变量 + __page: topStore.nextGlobalData.__page, + appVariables: topStore.nextGlobalData.appVariables, + __isTempGlobalLayer: true + }; + + if (globalLayerIdx !== -1) { + chain.splice(globalLayerIdx + 1, 0, globalData); + } + const newData = createObjectFromChain(chain); // 如果渲染器自己做了实现,且返回 false,则不再继续往下执行 if (handler?.(this.cRef, this.props.schema, newData) === false) { return; } + // 强制刷新并通过一个临时对象让下发给组件的全局变量更新 + // 等 react 完成一轮渲染后,将临时渲染切成正式渲染 + // 也就是说删掉临时对象,后续渲染读取真正变更后的全局变量 + // + + // 为什么这么做?因为很多组件内部都会 diff this.props.data 和 prevProps.data + // 如果对应的数据没有发生变化,则会跳过组件状态的更新 this.tmpData = newData; - this.forceUpdate(() => { - delete this.tmpData; - }); + this.subStore?.temporaryUpdateGlobalVars(globalData); + topStore.addSyncGlobalVarStatePendingTask( + callback => this.forceUpdate(callback), + () => { + delete this.tmpData; + this.subStore?.unDoTemporaryUpdateGlobalVars(); + } + ); } resolveRenderer(props: SchemaRendererProps, force = false): any { @@ -369,6 +414,9 @@ export class SchemaRenderer extends React.Component { return render!($path, schema as any, rest) as JSX.Element; } + // 用于全局变量刷新 + (rest as any).data = this.tmpData || rest.data; + const detectData = schema && (schema.detectField === '&' ? rest : rest[schema.detectField || 'data']); @@ -548,9 +596,6 @@ export class SchemaRenderer extends React.Component { mobileUI: schema.useMobileUI === false ? false : rest.mobileUI }; - // 用于全局变量刷新 - props.data = this.tmpData || props.data; - // style 支持公式 if (schema.style) { (props as any).style = buildStyle(schema.style, detectData); @@ -588,9 +633,13 @@ export class SchemaRenderer extends React.Component { } let component = supportRef ? ( - + ) : ( - + ); if (schema.animations) { diff --git a/packages/amis-core/src/WithStore.tsx b/packages/amis-core/src/WithStore.tsx index 8b520770c79..de9a0a4ac14 100644 --- a/packages/amis-core/src/WithStore.tsx +++ b/packages/amis-core/src/WithStore.tsx @@ -19,6 +19,7 @@ import { } from './utils/helper'; import {dataMapping, tokenize} from './utils/tpl-builtin'; import {RootStoreContext} from './WithRootStore'; +import {extractObjectChain} from './utils/object'; /** * 忽略静态数据中的 schema 属性 @@ -50,6 +51,7 @@ export function HocStoreFactory(renderer: { store?: IIRendererStore; data?: RendererData; scope?: RendererData; + storeRef?: (store: IIRendererStore | null) => void; rootStore: any; topStore: any; }; @@ -84,6 +86,9 @@ export function HocStoreFactory(renderer: { parentId: this.props.store ? this.props.store.id : '' }) as IIRendererStore; store.setTopStore(props.topStore); + + props.storeRef?.(store); + this.store = store; const extendsData = @@ -402,6 +407,7 @@ export function HocStoreFactory(renderer: { // @ts-ignore delete this.store; + this.props.storeRef?.(null); } renderChild( @@ -424,7 +430,7 @@ export function HocStoreFactory(renderer: { } render() { - const {detectField, ...rest} = this.props; + const {detectField, storeRef, ...rest} = this.props; if (this.state.hidden || this.state.visible === false) { return null; diff --git a/packages/amis-core/src/globalVar.ts b/packages/amis-core/src/globalVar.ts index 29b2bbf05c5..944a138dc93 100644 --- a/packages/amis-core/src/globalVar.ts +++ b/packages/amis-core/src/globalVar.ts @@ -9,6 +9,7 @@ import {isExpression} from './utils/formula'; import {reaction} from 'mobx'; import {resolveVariableAndFilter} from './utils/resolveVariableAndFilter'; import {IRootStore} from './store/root'; +import isPlainObject from 'lodash/isPlainObject'; /** * 全局变量的定义 @@ -298,6 +299,17 @@ export function observeGlobalVars( key, value }); + } else if (isPlainObject(value) && !value.type) { + // 最多支持两层,多了可能就会有性能问题了 + // 再多一层主要是为了支持某些 api 配置的是对象形式 + Object.keys(value).forEach(k => { + if (isGlobalVarExpression(value[k])) { + expressions.push({ + key: `${key}.${k}`, + value: value[k] + }); + } + }); } else if ( [ 'items', @@ -338,7 +350,7 @@ export function observeGlobalVars( exp => `${exp.key}:${resolveVariableAndFilter( exp.value, - topStore.downStream, + topStore.nextGlobalData, '| json' // 如果用了复杂对象,要靠这个来比较 )}` ) diff --git a/packages/amis-core/src/renderers/Item.tsx b/packages/amis-core/src/renderers/Item.tsx index b714b638ed2..14f1959481c 100644 --- a/packages/amis-core/src/renderers/Item.tsx +++ b/packages/amis-core/src/renderers/Item.tsx @@ -2363,24 +2363,7 @@ export function registerFormItem(config: FormItemConfig): RendererConfig { ...config, weight: typeof config.weight !== 'undefined' ? config.weight : -100, // 优先级高点 component: Control as any, - isFormItem: true, - onGlobalVarChanged: function (instance, schema, data): any { - if (config.onGlobalVarChanged?.apply(this, arguments) === false) { - return false; - } - - if (isGlobalVarExpression(schema.source)) { - (instance.props as any).reloadOptions?.(); - } - - // 目前表单项的全局变量更新要靠这个方式 - if (isGlobalVarExpression(schema.value)) { - (instance.props as any).onChange( - resolveVariableAndFilter(schema.value, data, '| raw') - ); - return false; - } - } + isFormItem: true }); } diff --git a/packages/amis-core/src/store/iRenderer.ts b/packages/amis-core/src/store/iRenderer.ts index 44b4f10cb75..3c76755875c 100644 --- a/packages/amis-core/src/store/iRenderer.ts +++ b/packages/amis-core/src/store/iRenderer.ts @@ -19,6 +19,7 @@ import { injectObjectChain } from '../utils'; import {DataChangeReason} from '../types'; +import findLastIndex from 'lodash/findLastIndex'; export const iRendererStore = StoreNode.named('iRendererStore') .props({ @@ -97,6 +98,35 @@ export const iRendererStore = StoreNode.named('iRendererStore') self.upStreamData = data; }, + // 临时更新全局变量 + temporaryUpdateGlobalVars(globalVar: any) { + const chain = extractObjectChain(self.data).filter( + (item: any) => !item.hasOwnProperty('__isTempGlobalLayer') + ); + const idx = findLastIndex( + chain, + item => + item.hasOwnProperty('global') || item.hasOwnProperty('globalState') + ); + + if (idx !== -1) { + chain.splice(idx + 1, 0, { + ...globalVar, + __isTempGlobalLayer: true + }); + } + + self.data = createObjectFromChain(chain); + }, + + // 撤销临时更新全局变量 + unDoTemporaryUpdateGlobalVars() { + const chain = extractObjectChain(self.data).filter( + (item: any) => !item.hasOwnProperty('__isTempGlobalLayer') + ); + self.data = createObjectFromChain(chain); + }, + reset() { self.data = self.pristine; }, diff --git a/packages/amis-core/src/store/root.ts b/packages/amis-core/src/store/root.ts index 600301a6055..df073657af4 100644 --- a/packages/amis-core/src/store/root.ts +++ b/packages/amis-core/src/store/root.ts @@ -4,7 +4,8 @@ import {ServiceStore} from './service'; import { createObjectFromChain, extractObjectChain, - isObjectShallowModified + isObjectShallowModified, + createObject } from '../utils'; import { GlobalVariableItem, @@ -27,6 +28,16 @@ export const RootStore = ServiceStore.named('RootStore') runtimeErrorStack: types.frozen(), query: types.frozen(), ready: false, + + // 临时变更,等 react 完成一轮渲染后,将临时变更切成正式变更 + // 主要是为了让可能需要重新渲染的部分组件可以实现 this.props.data 和 prevProps.data 不一致 + // 因为很多组件内部会 diff this.props.data 和 prevProps.data 来决定是否更新的逻辑 + globalVarTempStates: types.optional( + types.map(types.frozen()), + {} + ), + + // 正式变更 globalVarStates: types.optional( types.map(types.frozen()), {} @@ -43,6 +54,46 @@ export const RootStore = ServiceStore.named('RootStore') }; }) .views(self => ({ + get nextGlobalData(): any { + let globalData = {} as any; + let globalState = {} as any; + + if (self.globalVarTempStates.size) { + let touched = false; + let saved = true; + let errors: any = {}; + let initialized = true; + self.globalVarTempStates.forEach((state, key) => { + globalData[key] = state.value; + touched = touched || state.touched; + if (!state.saved) { + saved = false; + } + + if (state.errorMessages.length) { + errors[key] = state.errorMessages; + } + if (!state.initialized) { + initialized = false; + } + }); + + globalState = { + fields: self.globalVarTempStates.toJSON(), + initialized: initialized, + touched: touched, + saved: saved, + errors: errors, + valid: !Object.keys(errors).length + }; + } + + return createObject(self.context, { + global: globalData, + globalState: globalState + }); + }, + get downStream() { let result = self.data; @@ -168,7 +219,7 @@ export const RootStore = ServiceStore.named('RootStore') if (getter) { await getGlobalVarData(item, context, getter); - const state = self.globalVarStates.get(item.key)!; + const state = self.globalVarTempStates.get(item.key)!; updateState(item.key, { initialized: true, pristine: state.value @@ -234,7 +285,7 @@ export const RootStore = ServiceStore.named('RootStore') continue; } - const state = self.globalVarStates.get(key); + const state = self.globalVarTempStates.get(key); if (state) { updateState(key, { value: data[key], @@ -275,7 +326,7 @@ export const RootStore = ServiceStore.named('RootStore') const itemsNotInitialized: Array = []; for (let item of globalVars) { - let state = self.globalVarStates.get(item.key); + let state = self.globalVarTempStates.get(item.key); if (state?.initialized) { continue; } @@ -323,8 +374,10 @@ export const RootStore = ServiceStore.named('RootStore') self.globalVars = yield initializeGlobalVars(newVars, updateVars); removeVars.forEach(item => { - self.globalVarStates.delete(item.key); + self.globalVarTempStates.delete(item.key); }); + + syncGlobalVarStates(); }); // 更新全局变量的值 @@ -350,7 +403,7 @@ export const RootStore = ServiceStore.named('RootStore') value: any; } ) => { - const state = self.globalVarStates.get(key); + const state = self.globalVarTempStates.get(key); if (!state) { return; } @@ -483,7 +536,7 @@ export const RootStore = ServiceStore.named('RootStore') }> = []; const values: any = {}; for (let varItem of self.globalVars) { - const state = self.globalVarStates.get(varItem.key); + const state = self.globalVarTempStates.get(varItem.key); if (!state?.touched) { continue; } else if (key && key !== varItem.key) { @@ -536,6 +589,32 @@ export const RootStore = ServiceStore.named('RootStore') leading: false }); + function syncGlobalVarStates() { + self.globalVarStates.clear(); + self.globalVarTempStates.forEach((state, key) => { + self.globalVarStates.set(key, {...state}); + }); + } + + let pendingCount = 0; + let callbacks: Array<() => void> = []; + function addSyncGlobalVarStatePendingTask( + fn: (callback: () => void) => void, + callback?: () => void + ) { + pendingCount++; + callback && callbacks.push(callback); + fn(() => { + pendingCount--; + + if (pendingCount === 0) { + callbacks.forEach(callback => callback()); + callbacks = []; + (self as any).syncGlobalVarStates(); + } + }); + } + return { updateContext(context: any) { // 因为 context 不是受控属性,直接共用引用好了 @@ -543,12 +622,12 @@ export const RootStore = ServiceStore.named('RootStore') Object.assign(self.context, context); }, updateGlobalVarState(key: string, state: Partial) { - const origin = self.globalVarStates.get(key); + const origin = self.globalVarTempStates.get(key); const newState = { ...(origin || createGlobalVarState()), ...state }; - self.globalVarStates.set(key, newState as any); + self.globalVarTempStates.set(key, newState as any); }, setGlobalVars, updateGlobalVarValue, @@ -565,6 +644,8 @@ export const RootStore = ServiceStore.named('RootStore') } }, init: init, + syncGlobalVarStates, + addSyncGlobalVarStatePendingTask, afterDestroy() { lazySaveGlobalVarValues.flush(); } diff --git a/packages/amis-editor-core/src/variable.ts b/packages/amis-editor-core/src/variable.ts index 4da7dc87798..f33e35df6d8 100644 --- a/packages/amis-editor-core/src/variable.ts +++ b/packages/amis-editor-core/src/variable.ts @@ -259,15 +259,24 @@ export class VariableManager { const options = [ ...this.getVariableOptions(), - ...this.getPageVariablesOptions() + ...this.getPageVariablesOptions(), + ...this.getGlobalVariablesOptions() ]; - const node = findTree( - options, - item => item[valueField ?? 'value'] === path - ); - + let nodePaths: Array = []; + const node = findTree(options, (item, key, level, paths) => { + if (item[valueField ?? 'value'] === path) { + nodePaths = paths.concat(item); + return true; + } + return false; + }); return node - ? node[labelField ?? 'label'] ?? node[valueField ?? 'value'] ?? '' + ? nodePaths + .map( + node => + node[labelField ?? 'label'] ?? node[valueField ?? 'value'] ?? '' + ) + .join(' / ') : ''; } @@ -288,7 +297,11 @@ export class VariableManager { if (item.type === 'array') { delete item.children; } + if (item.value === 'global') { + item.disabled = true; + } }); + return options; } } diff --git a/packages/amis/src/renderers/Tabs.tsx b/packages/amis/src/renderers/Tabs.tsx index d5e89713878..fe4476afc0f 100644 --- a/packages/amis/src/renderers/Tabs.tsx +++ b/packages/amis/src/renderers/Tabs.tsx @@ -1117,23 +1117,7 @@ export default class Tabs extends React.Component { } } @Renderer({ - type: 'tabs', - onGlobalVarChanged(instance, schema, data): any { - if (isGlobalVarExpression(schema.source)) { - // tabs 要靠手动刷新了 - const [newLocalTabs, isFromSource] = (instance as any).initTabArray( - (instance.props as any).tabs, - (instance.props as any).source, - data - ); - - instance.setState({ - localTabs: newLocalTabs, - isFromSource - }); - return false; - } - } + type: 'tabs' }) export class TabsRenderer extends Tabs { static contextType = ScopedContext;