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;
overflow: hidden;
user-select: none;
position: relative;
// 覆盖amis默认top值避免导致未垂直居中
.ae-Editor-toolbar svg.icon {
@ -125,6 +126,18 @@
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 {
overflow: hidden;

View File

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

View File

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

View File

@ -1,6 +1,6 @@
import {observer} from 'mobx-react';
import React from 'react';
import {Tab, Tabs} from 'amis';
import {Tab, Tabs, toast} from 'amis';
import cx from 'classnames';
import {EditorManager} from '../../manager';
import {EditorStoreType} from '../../store/editor';
@ -16,6 +16,7 @@ interface RightPanelsProps {
theme?: string;
appLocale?: string;
amisEnv?: any;
readonly?: boolean;
}
interface RightPanelsStates {
@ -62,6 +63,29 @@ export class RightPanels extends React.Component<
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() {
const {store, manager, theme} = this.props;
const {isOpenStatus, isFixedStatus} = this.state;
@ -77,7 +101,7 @@ export class RightPanels extends React.Component<
path: node?.path,
node: node,
value: store.value,
onChange: manager.panelChangeValue,
onChange: this.handlePanelChangeValue,
store: store,
manager: manager,
popOverContainer: this.getPopOverContainer
@ -90,7 +114,7 @@ export class RightPanels extends React.Component<
info={node?.info}
path={node?.path}
value={store.value}
onChange={manager.panelChangeValue}
onChange={this.handlePanelChangeValue}
store={store}
manager={manager}
popOverContainer={this.getPopOverContainer}

View File

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

View File

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

View File

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

View File

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

View File

@ -23,7 +23,8 @@ import {
appTranslate,
JSONGetByPath,
addModal,
mergeDefinitions
mergeDefinitions,
getModals
} from '../../src/util';
import {
InsertEventContext,
@ -404,6 +405,9 @@ export const MainStore = types
): EditorNodeType | undefined {
return self.root.getNodeById(id, regionOrType);
},
getNodeByComponentId(id: string): EditorNodeType | undefined {
return self.root.getNodeByComponentId(id);
},
get activeNodeInfo(): RendererInfo | null | undefined {
return this.getNodeById(self.activeId)?.info;
@ -1039,64 +1043,7 @@ export const MainStore = types
// 获取弹窗大纲列表
get modals(): Array<EditorModalBody> {
const schema = self.schema;
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
});
}
const modals: Array<DialogSchema | DrawerSchema> = getModals(schema);
return modals;
},
@ -1367,7 +1314,8 @@ export const MainStore = types
setActiveId(
id: string,
region: string = '',
selections: Array<string> = []
selections: Array<string> = [],
onEditorActive: boolean = true
) {
const node = id ? self.getNodeById(id) : undefined;
@ -1381,6 +1329,39 @@ export const MainStore = types
// if (!self.panelKey && id) {
// 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>) {

View File

@ -104,6 +104,28 @@ export const EditorNode = types
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) {
info = value;
},

View File

@ -23,6 +23,8 @@ import merge from 'lodash/merge';
import {EditorModalBody} from './store/editor';
import {filter} from 'lodash';
import type {SchemaType} from 'amis/lib/Schema';
import type {DialogSchema} from '../../amis/src/renderers/Dialog';
import type {DrawerSchema} from '../../amis/src/renderers/Drawer';
const {
guid,
@ -1852,6 +1854,62 @@ export function setDefaultColSize(
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: {
[k in SchemaType | 'user-select' | 'department-select']?:
| 'string'