feat: 编辑器支持只读模式 (#10857)

* feat: 扩展编辑器功能

* 弹框支持readonly

* bugfix

* bugfix

---------

Co-authored-by: qinhaoyan <30946345+qinhaoyan@users.noreply.github.com>
Co-authored-by: hsm-lv <80095014+hsm-lv@users.noreply.github.com>
This commit is contained in:
qkiroc 2024-09-02 14:54:51 +08:00 committed by GitHub
parent 859ae8d72a
commit 13f1890db7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 204 additions and 72 deletions

View File

@ -83,6 +83,7 @@
min-width: 980px; min-width: 980px;
overflow: hidden; overflow: hidden;
user-select: none; user-select: none;
position: relative;
// 覆盖amis默认top值避免导致未垂直居中 // 覆盖amis默认top值避免导致未垂直居中
.ae-Editor-toolbar svg.icon { .ae-Editor-toolbar svg.icon {
@ -125,6 +126,18 @@
border: 1px dashed rgb(206, 208, 211); border: 1px dashed rgb(206, 208, 211);
} }
.subEditor-container {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
& .subEditor-dialog {
height: 100%;
margin: 0;
}
}
// 弹窗编辑器面板 // 弹窗编辑器面板
.subEditor-dialog { .subEditor-dialog {
overflow: hidden; overflow: hidden;

View File

@ -135,6 +135,7 @@ export interface EditorProps extends PluginEventListener {
getHostNodeDataSchema?: () => Promise<any>; getHostNodeDataSchema?: () => Promise<any>;
getAvaiableContextFields?: (node: EditorNodeType) => Promise<any>; getAvaiableContextFields?: (node: EditorNodeType) => Promise<any>;
readonly?: boolean;
} }
export default class Editor extends Component<EditorProps> { export default class Editor extends Component<EditorProps> {
@ -275,6 +276,10 @@ export default class Editor extends Component<EditorProps> {
return; return;
} }
if (this.props.readonly) {
return;
}
const manager = this.manager; const manager = this.manager;
const store = manager.store; const store = manager.store;
@ -573,7 +578,8 @@ export default class Editor extends Component<EditorProps> {
previewProps, previewProps,
autoFocus, autoFocus,
isSubEditor, isSubEditor,
amisEnv amisEnv,
readonly
} = this.props; } = this.props;
return ( return (
@ -588,7 +594,7 @@ export default class Editor extends Component<EditorProps> {
)} )}
> >
<div className="ae-Editor-inner" onContextMenu={this.handleContextMenu}> <div className="ae-Editor-inner" onContextMenu={this.handleContextMenu}>
{!preview && ( {!preview && !readonly && (
<LeftPanels <LeftPanels
store={this.store} store={this.store}
manager={this.manager} manager={this.manager}
@ -618,6 +624,7 @@ export default class Editor extends Component<EditorProps> {
amisEnv={amisEnv} amisEnv={amisEnv}
autoFocus={autoFocus} autoFocus={autoFocus}
toolbarContainer={this.getToolbarContainer} toolbarContainer={this.getToolbarContainer}
readonly={readonly}
></Preview> ></Preview>
</div> </div>
@ -628,6 +635,7 @@ export default class Editor extends Component<EditorProps> {
theme={theme} theme={theme}
appLocale={appLocale} appLocale={appLocale}
amisEnv={amisEnv} amisEnv={amisEnv}
readonly={readonly}
/> />
)} )}
@ -639,6 +647,7 @@ export default class Editor extends Component<EditorProps> {
manager={this.manager} manager={this.manager}
theme={theme} theme={theme}
amisEnv={amisEnv} amisEnv={amisEnv}
readonly={readonly}
/> />
<ScaffoldModal <ScaffoldModal
store={this.store} store={this.store}

View File

@ -19,6 +19,7 @@ export interface HighlightBoxProps {
onSwitch?: (id: string) => void; onSwitch?: (id: string) => void;
manager: EditorManager; manager: EditorManager;
children?: React.ReactNode; children?: React.ReactNode;
readonly?: boolean;
} }
export default observer(function ({ export default observer(function ({
@ -30,7 +31,8 @@ export default observer(function ({
node, node,
toolbarContainer, toolbarContainer,
onSwitch, onSwitch,
manager manager,
readonly
}: HighlightBoxProps) { }: HighlightBoxProps) {
const handleWResizerMouseDown = React.useCallback( const handleWResizerMouseDown = React.useCallback(
(e: MouseEvent) => startResize(e, 'horizontal'), (e: MouseEvent) => startResize(e, 'horizontal'),
@ -250,7 +252,7 @@ export default observer(function ({
draggable={!!curFreeContainerId || isDraggableContainer} draggable={!!curFreeContainerId || isDraggableContainer}
onDragStart={handleDragStart} onDragStart={handleDragStart}
> >
{isActive ? ( {isActive && !readonly ? (
<div <div
className={`ae-Editor-toolbarPopover ${ className={`ae-Editor-toolbarPopover ${
isRightElem ? 'is-right-elem' : '' isRightElem ? 'is-right-elem' : ''

View File

@ -1,6 +1,6 @@
import {observer} from 'mobx-react'; import {observer} from 'mobx-react';
import React from 'react'; import React from 'react';
import {Tab, Tabs} from 'amis'; import {Tab, Tabs, toast} from 'amis';
import cx from 'classnames'; import cx from 'classnames';
import {EditorManager} from '../../manager'; import {EditorManager} from '../../manager';
import {EditorStoreType} from '../../store/editor'; import {EditorStoreType} from '../../store/editor';
@ -16,6 +16,7 @@ interface RightPanelsProps {
theme?: string; theme?: string;
appLocale?: string; appLocale?: string;
amisEnv?: any; amisEnv?: any;
readonly?: boolean;
} }
interface RightPanelsStates { interface RightPanelsStates {
@ -62,6 +63,29 @@ export class RightPanels extends React.Component<
return findDOMNode(this) as HTMLElement; return findDOMNode(this) as HTMLElement;
} }
@autobind
handlePanelChangeValue(
...arg: Parameters<typeof this.props.manager.panelChangeValue>
) {
const {manager, readonly} = this.props;
if (readonly) {
const diff = arg[1];
if (
!diff?.find((item: any) =>
item.path.find(
(p: string) => !p.startsWith('__') && p !== 'pullRefresh'
)
)
) {
return;
}
toast.error('不支持编辑');
} else {
manager.panelChangeValue(...arg);
}
}
render() { render() {
const {store, manager, theme} = this.props; const {store, manager, theme} = this.props;
const {isOpenStatus, isFixedStatus} = this.state; const {isOpenStatus, isFixedStatus} = this.state;
@ -77,7 +101,7 @@ export class RightPanels extends React.Component<
path: node?.path, path: node?.path,
node: node, node: node,
value: store.value, value: store.value,
onChange: manager.panelChangeValue, onChange: this.handlePanelChangeValue,
store: store, store: store,
manager: manager, manager: manager,
popOverContainer: this.getPopOverContainer popOverContainer: this.getPopOverContainer
@ -90,7 +114,7 @@ export class RightPanels extends React.Component<
info={node?.info} info={node?.info}
path={node?.path} path={node?.path}
value={store.value} value={store.value}
onChange={manager.panelChangeValue} onChange={this.handlePanelChangeValue}
store={store} store={store}
manager={manager} manager={manager}
popOverContainer={this.getPopOverContainer} popOverContainer={this.getPopOverContainer}

View File

@ -39,6 +39,8 @@ export interface PreviewProps {
autoFocus?: boolean; autoFocus?: boolean;
toolbarContainer?: () => any; toolbarContainer?: () => any;
readonly?: boolean;
} }
export interface PreviewState { export interface PreviewState {
@ -604,9 +606,11 @@ export default class Preview extends Component<PreviewProps> {
toolbarContainer={toolbarContainer} toolbarContainer={toolbarContainer}
onSwitch={this.handleNavSwitch} onSwitch={this.handleNavSwitch}
manager={manager} manager={manager}
readonly={this.props.readonly}
> >
{node.childRegions.map(region => {node.childRegions.map(region =>
!node.memberImmutable(region.region) && !node.memberImmutable(region.region) &&
!this.props.readonly &&
store.isRegionActive(region.id, region.region) ? ( store.isRegionActive(region.id, region.region) ? (
<RegionHighlightBox <RegionHighlightBox
manager={manager} manager={manager}

View File

@ -19,6 +19,7 @@ export interface SubEditorProps {
manager: EditorManager; manager: EditorManager;
theme?: string; theme?: string;
amisEnv?: RenderOptions; amisEnv?: RenderOptions;
readonly?: boolean;
} }
@observer @observer
@ -97,7 +98,7 @@ export class SubEditor extends React.Component<SubEditorProps> {
} }
buildSchema() { buildSchema() {
const {store, manager, amisEnv} = this.props; const {store, manager, amisEnv, readonly} = this.props;
const subEditorContext = store.subEditorContext; const subEditorContext = store.subEditorContext;
const config = manager.config; const config = manager.config;
let superEditorData: any = store.superEditorData; let superEditorData: any = store.superEditorData;
@ -118,6 +119,7 @@ export class SubEditor extends React.Component<SubEditorProps> {
? { ? {
type: 'form', type: 'form',
mode: 'normal', mode: 'normal',
wrapWithPanel: false,
wrapperComponent: 'div', wrapperComponent: 'div',
onValidate: async (value: any) => { onValidate: async (value: any) => {
const result = await store.subEditorContext?.validate?.(value); const result = await store.subEditorContext?.validate?.(value);
@ -190,6 +192,7 @@ export class SubEditor extends React.Component<SubEditorProps> {
getAvaiableContextFields={node => getAvaiableContextFields={node =>
manager.getAvailableContextFields(node) manager.getAvailableContextFields(node)
} }
readonly={readonly}
/> />
) )
} }
@ -244,10 +247,14 @@ export class SubEditor extends React.Component<SubEditorProps> {
} }
render() { render() {
const {store, theme, manager} = this.props; const {store, theme, manager, readonly} = this.props;
if (!store.subEditorContext) {
return null;
}
return render( return render(
{ {
type: 'dialog', type: readonly ? 'container' : 'dialog',
className: readonly ? 'subEditor-container' : 'subEditor-dialog',
...this.buildSchema() ...this.buildSchema()
}, },

View File

@ -101,6 +101,8 @@ export function SchemaFrom({
return schema; return schema;
}, [body, controls, submitOnChange]); }, [body, controls, submitOnChange]);
const [init, setInit] = React.useState(true);
const themeConfig = React.useMemo(() => getThemeConfig(), []); const themeConfig = React.useMemo(() => getThemeConfig(), []);
const submitSubscribers = React.useRef<Array<Function>>([]); const submitSubscribers = React.useRef<Array<Function>>([]);
const subscribeSubmit = React.useCallback( const subscribeSubmit = React.useCallback(
@ -147,10 +149,10 @@ export function SchemaFrom({
newValue = pipeOut ? await pipeOut(newValue, value) : newValue; newValue = pipeOut ? await pipeOut(newValue, value) : newValue;
const diffValue = diff(value, newValue); const diffValue = diff(value, newValue);
// 没有变化时不触发onChange // 没有变化时不触发onChange
if (!diffValue) { if (!diffValue || init) {
setInit(false);
return; return;
} }
onChange(newValue, diffValue, (schema, value, id, diff) => { onChange(newValue, diffValue, (schema, value, id, diff) => {
return submitSubscribers.current.reduce((schema, fn) => { return submitSubscribers.current.reduce((schema, fn) => {
return fn(schema, value, id, diff); return fn(schema, value, id, diff);

View File

@ -1029,6 +1029,9 @@ export abstract class BasePlugin implements PluginInterface {
static scene = ['global']; static scene = ['global'];
name?: string;
rendererName?: string;
/** /**
* rendererName * rendererName
* @param renderer * @param renderer
@ -1279,6 +1282,13 @@ export abstract class BasePlugin implements PluginInterface {
originalValue: node.schema.value // 记录原始值,循环引用检测需要 originalValue: node.schema.value // 记录原始值,循环引用检测需要
} as any; } as any;
} }
getKeyAndName() {
return {
key: this.rendererName,
name: this.name
};
}
} }
/** /**

View File

@ -23,7 +23,8 @@ import {
appTranslate, appTranslate,
JSONGetByPath, JSONGetByPath,
addModal, addModal,
mergeDefinitions mergeDefinitions,
getModals
} from '../../src/util'; } from '../../src/util';
import { import {
InsertEventContext, InsertEventContext,
@ -404,6 +405,9 @@ export const MainStore = types
): EditorNodeType | undefined { ): EditorNodeType | undefined {
return self.root.getNodeById(id, regionOrType); return self.root.getNodeById(id, regionOrType);
}, },
getNodeByComponentId(id: string): EditorNodeType | undefined {
return self.root.getNodeByComponentId(id);
},
get activeNodeInfo(): RendererInfo | null | undefined { get activeNodeInfo(): RendererInfo | null | undefined {
return this.getNodeById(self.activeId)?.info; return this.getNodeById(self.activeId)?.info;
@ -1039,64 +1043,7 @@ export const MainStore = types
// 获取弹窗大纲列表 // 获取弹窗大纲列表
get modals(): Array<EditorModalBody> { get modals(): Array<EditorModalBody> {
const schema = self.schema; const schema = self.schema;
const modals: Array<DialogSchema | DrawerSchema> = []; const modals: Array<DialogSchema | DrawerSchema> = getModals(schema);
JSONTraverse(schema, (value: any, key: string, host: any) => {
if (
key === 'actionType' &&
['dialog', 'drawer', 'confirmDialog'].includes(value)
) {
const key = value === 'drawer' ? 'drawer' : 'dialog';
const body = host[key] || host['args'];
if (
body &&
!body.$ref &&
!modals.find(item => item.$$id === body.$$id)
) {
modals.push({
...body,
type: key,
actionType: value
});
}
}
return value;
});
// 公共组件排在前面
Object.keys(schema.definitions || {})
.reverse()
.forEach(key => {
const definition = schema.definitions[key];
if (['dialog', 'drawer'].includes(definition.type)) {
// 不要把已经内嵌弹窗中的弹窗再放到外面
if (
definition.$$originId &&
modals.find(item => item.$$id === definition.$$originId)
) {
return;
}
modals.unshift({
...definition,
$$ref: key
});
}
});
// 子弹窗时,自己就是个弹窗
if (['dialog', 'drawer', 'confirmDialog'].includes(schema.type)) {
const idx = modals.findIndex(item => item.$$id === schema.$$id);
if (~idx) {
modals.splice(idx, 1);
}
modals.unshift({
...schema,
// 如果还包含这个,子弹窗里面收集弹窗的时候会出现多份内嵌弹窗
definitions: undefined
});
}
return modals; return modals;
}, },
@ -1367,7 +1314,8 @@ export const MainStore = types
setActiveId( setActiveId(
id: string, id: string,
region: string = '', region: string = '',
selections: Array<string> = [] selections: Array<string> = [],
onEditorActive: boolean = true
) { ) {
const node = id ? self.getNodeById(id) : undefined; const node = id ? self.getNodeById(id) : undefined;
@ -1381,6 +1329,39 @@ export const MainStore = types
// if (!self.panelKey && id) { // if (!self.panelKey && id) {
// self.panelKey = 'config'; // self.panelKey = 'config';
// } // }
const schema = self.getSchema(id);
onEditorActive && (window as any).onEditorActive?.(schema);
},
setActiveIdByComponentId(id: string) {
const node = self.getNodeByComponentId(id);
if (node) {
this.setActiveId(node.id, node.region, [], false);
this.closeSubEditor();
} else {
const modals = self.modals;
const modalSchema = find(modals, modal => modal.id === id);
if (modalSchema) {
this.openSubEditor({
value: modalSchema,
title: '弹窗预览',
onChange: (value: any) => {}
});
} else {
const subEditorRef = this.getSubEditorRef();
if (subEditorRef) {
subEditorRef.store.setActiveIdByComponentId(id);
const $$id = subEditorRef.props.value.$$id;
const modalSchema = find(modals, modal => modal.$$id === $$id);
this.openSubEditor({
value: modalSchema,
title: '弹窗预览',
onChange: (value: any) => {}
});
}
}
}
}, },
setSelections(ids: Array<string>) { setSelections(ids: Array<string>) {

View File

@ -104,6 +104,28 @@ export const EditorNode = types
return resolved; return resolved;
}, },
getNodeByComponentId(id: string) {
let pool = self.children.concat();
let resolved: any = undefined;
while (pool.length) {
const item = pool.shift();
const schema = item.schema;
if (schema && schema.id === id) {
resolved = item;
break;
}
// 将当前节点的子节点全部放置到 pool中
if (item.children.length) {
pool.push.apply(pool, item.uniqueChildren);
}
}
return resolved;
},
setInfo(value: RendererInfo) { setInfo(value: RendererInfo) {
info = value; info = value;
}, },

View File

@ -23,6 +23,8 @@ import merge from 'lodash/merge';
import {EditorModalBody} from './store/editor'; import {EditorModalBody} from './store/editor';
import {filter} from 'lodash'; import {filter} from 'lodash';
import type {SchemaType} from 'amis/lib/Schema'; import type {SchemaType} from 'amis/lib/Schema';
import type {DialogSchema} from '../../amis/src/renderers/Dialog';
import type {DrawerSchema} from '../../amis/src/renderers/Drawer';
const { const {
guid, guid,
@ -1852,6 +1854,62 @@ export function setDefaultColSize(
return tempList; return tempList;
} }
export function getModals(schema: any) {
const modals: Array<DialogSchema | DrawerSchema> = [];
JSONTraverse(schema, (value: any, key: string, host: any) => {
if (
key === 'actionType' &&
['dialog', 'drawer', 'confirmDialog'].includes(value)
) {
const key = value === 'drawer' ? 'drawer' : 'dialog';
const body = host[key] || host['args'];
if (body && !body.$ref && !modals.find(item => item.$$id === body.$$id)) {
modals.push({
...body,
type: key,
actionType: value
});
}
}
return value;
});
// 公共组件排在前面
Object.keys(schema.definitions || {})
.reverse()
.forEach(key => {
const definition = schema.definitions[key];
if (['dialog', 'drawer'].includes(definition.type)) {
// 不要把已经内嵌弹窗中的弹窗再放到外面
if (
definition.$$originId &&
modals.find(item => item.$$id === definition.$$originId)
) {
return;
}
modals.unshift({
...definition,
$$ref: key
});
}
});
// 子弹窗时,自己就是个弹窗
if (['dialog', 'drawer', 'confirmDialog'].includes(schema.type)) {
const idx = modals.findIndex(item => item.$$id === schema.$$id);
if (~idx) {
modals.splice(idx, 1);
}
modals.unshift({
...schema,
// 如果还包含这个,子弹窗里面收集弹窗的时候会出现多份内嵌弹窗
definitions: undefined
});
}
return modals;
}
export const RAW_TYPE_MAP: { export const RAW_TYPE_MAP: {
[k in SchemaType | 'user-select' | 'department-select']?: [k in SchemaType | 'user-select' | 'department-select']?:
| 'string' | 'string'