diff --git a/.stylelintrc.js b/.stylelintrc.js index 74a5a54e3..2ba42f6d5 100644 --- a/.stylelintrc.js +++ b/.stylelintrc.js @@ -1,3 +1,6 @@ module.exports = { extends: 'stylelint-config-ali', + rules: { + "selector-max-id": 2 + } }; diff --git a/packages/designer/src/builtin-simulator/bem-tools/border-detecting.tsx b/packages/designer/src/builtin-simulator/bem-tools/border-detecting.tsx index e3e9089dc..296b90ad2 100644 --- a/packages/designer/src/builtin-simulator/bem-tools/border-detecting.tsx +++ b/packages/designer/src/builtin-simulator/bem-tools/border-detecting.tsx @@ -1,8 +1,11 @@ import { Component, Fragment, PureComponent } from 'react'; import classNames from 'classnames'; import { computed, observer, Title } from '@ali/lowcode-editor-core'; -import { BuiltinSimulatorHost } from '../host'; import { TitleContent } from '@ali/lowcode-types'; +import { getClosestNode } from '@ali/lowcode-utils'; + +import { BuiltinSimulatorHost } from '../host'; + export class BorderDetectingInstance extends PureComponent<{ title: TitleContent; @@ -10,9 +13,10 @@ export class BorderDetectingInstance extends PureComponent<{ scale: number; scrollX: number; scrollY: number; + isLocked?: boolean; }> { render() { - const { title, rect, scale, scrollX, scrollY } = this.props; + const { title, rect, scale, scrollX, scrollY, isLocked } = this.props; if (!rect) { return null; } @@ -32,6 +36,9 @@ export class BorderDetectingInstance extends PureComponent<{ return (
+ { + isLocked ? (<Title title="已锁定" className="lc-borders-status" />) : null + } </div> ); } @@ -74,12 +81,33 @@ export class BorderDetecting extends Component<{ host: BuiltinSimulatorHost }> { const { host } = this.props; const { current } = this; + const canHoverHook = current?.componentMeta.getMetadata()?.experimental?.callbacks?.onHoverHook; const canHover = (canHoverHook && typeof canHoverHook === 'function') ? canHoverHook(current) : true; + if (!canHover || !current || host.viewport.scrolling || host.liveEditing.editing) { return null; } + const lockedNode = getClosestNode(current, (n) => { + return n?.getExtraProp('isLocked')?.getValue() === true; + }); + if (lockedNode && lockedNode.getId() !== current.getId()) { + // return null; + // 选中父节锁定的节点 + return ( + <BorderDetectingInstance + key="line-h" + title={current.title} + scale={this.scale} + scrollX={this.scrollX} + scrollY={this.scrollY} + // @ts-ignore + rect={host.computeComponentInstanceRect(host.getComponentInstances(lockedNode)[0], lockedNode.componentMeta.rootSelector)} + isLocked={lockedNode?.getId() !== current.getId()} + /> + ); + } const instances = host.getComponentInstances(current); if (!instances || instances.length < 1) { return null; diff --git a/packages/designer/src/builtin-simulator/bem-tools/borders.less b/packages/designer/src/builtin-simulator/bem-tools/borders.less index b5df1dc52..2cdc9fb40 100644 --- a/packages/designer/src/builtin-simulator/bem-tools/borders.less +++ b/packages/designer/src/builtin-simulator/bem-tools/borders.less @@ -11,10 +11,13 @@ will-change: transform, width, height; overflow: visible; & > &-title { - position: absolute; color: var(--color-brand-light); - top: 0; - left: 0; + transform: translateY(-100%); + font-weight: lighter; + } + & > &-status { + margin-left: 5px; + color: #3c3c3c; transform: translateY(-100%); font-weight: lighter; } diff --git a/packages/designer/src/builtin-simulator/host.ts b/packages/designer/src/builtin-simulator/host.ts index efe00d474..a37d24441 100644 --- a/packages/designer/src/builtin-simulator/host.ts +++ b/packages/designer/src/builtin-simulator/host.ts @@ -30,6 +30,7 @@ import { isFormEvent, hasOwnProperty, UtilsMetadata, + getClosestNode, } from '@ali/lowcode-utils'; import { DragObjectType, @@ -463,7 +464,7 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp } setupRendererChannel() { - const editor = this.designer.editor; + const { editor } = this.designer; editor.on('node.innerProp.change', ({ node, prop, oldValue, newValue }) => { // 在 Node 初始化阶段的属性变更都跳过 if (!node.isInited) return; @@ -545,71 +546,20 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp if (!node) { return; } - const isRGLNode = node?.getParent()?.isRGLContainer; const rglNode = node?.getParent(); + const isRGLNode = rglNode?.isRGLContainer; if (isRGLNode) { // 如果拖拽的是磁铁块的右下角handle,则直接跳过 if (downEvent.target.classList.contains('react-resizable-handle')) return; + // 禁止多选 isMulti = false; designer.dragon.emitter.emit('rgl.switch', { action: 'start', rglNode, }); - const judgeEnterOtherRGL = (e: MouseEvent) => { - const _nodeInst = this.getNodeInstanceFromElement(e.target as Element); - const _node = _nodeInst?.node; - if (!_node) return { status: false }; - const { isRGL: _isRGL, rglNode: _rglNode } = _node.getRGL(); - const status = !!( - _isRGL && - _rglNode?.id !== rglNode?.id && - _rglNode?.getParent() !== node && - _node !== nodeInst?.node - ); - return { status, rglNode: _rglNode }; - }; - const move = (e: MouseEvent) => { - if (!isShaken(downEvent, e)) { - if (nodeInst.instance && nodeInst.instance.style) { - nodeInst.instance.style.pointerEvents = 'none'; - } - } - const { status, rglNode: _rglNode } = judgeEnterOtherRGL(e); - if (status) { - designer.dragon.emitter.emit('rgl.add.placeholder', { - rglNode: _rglNode, - node, - event: e, - fromRglNode: rglNode, - }); - } else { - designer.dragon.emitter.emit('rgl.remove.placeholder'); - } - }; - const over = (e: MouseEvent) => { - const { status, rglNode: _rglNode } = judgeEnterOtherRGL(e); - if (status) { - designer.dragon.emitter.emit('rgl.drop', { - rglNode: _rglNode, - node, - fromRglNode: rglNode, - }); - } - designer.dragon.emitter.emit('rgl.remove.placeholder'); - if (nodeInst.instance && nodeInst.instance.style) { - nodeInst.instance.style.pointerEvents = ''; - } - designer.dragon.emitter.emit('rgl.switch', { - action: 'end', - rglNode, - }); - doc.removeEventListener('mouseup', over, true); - doc.removeEventListener('mousemove', move, true); - }; - doc.addEventListener('mouseup', over, true); - doc.addEventListener('mousemove', move, true); } else { // stop response document focus event + // 禁止原生拖拽 downEvent.stopPropagation(); downEvent.preventDefault(); } @@ -620,6 +570,11 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp const isLeftButton = downEvent.which === 1 || downEvent.button === 0; const checkSelect = (e: MouseEvent) => { doc.removeEventListener('mouseup', checkSelect, true); + // 取消移动; + designer.dragon.emitter.emit('rgl.switch', { + action: 'end', + rglNode, + }); // 鼠标是否移动 ? - 鼠标抖动应该也需要支持选中事件,偶尔点击不能选中,磁帖块移除shaken检测 if (!isShaken(downEvent, e) || isRGLNode) { let { id } = node; @@ -665,16 +620,14 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp } else { // will clear current selection & select dragment in dragstart } - if (!isRGLNode) { - designer.dragon.boost( - { - type: DragObjectType.Node, - nodes, - }, - downEvent, - true, - ); - } + designer.dragon.boost( + { + type: DragObjectType.Node, + nodes, + }, + downEvent, + isRGLNode ? rglNode : undefined, + ); if (ignoreUpSelected) { // multi select mode has add selected, should return return; @@ -1253,7 +1206,11 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp } const dropContainer = this.getDropContainer(e); const canDropIn = dropContainer?.container?.componentMeta?.prototype?.options?.canDropIn; - + const lockedNode = getClosestNode(dropContainer?.container as Node, (node) => { + return node?.getExtraProp('isLocked')?.getValue() === true; + }); + // const isLocked = dropContainer?.container?.getExtraProp('isLocked')?.getValue(); + if (lockedNode) return null; if ( !dropContainer || canDropIn === false || @@ -1325,8 +1282,7 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp const inst = instances ? instances.length > 1 ? instances.find( - (_inst) => - this.getClosestNodeInstance(_inst, container.id)?.instance === containerInstance, + (_inst) => this.getClosestNodeInstance(_inst, container.id)?.instance === containerInstance, ) : instances[0] : null; diff --git a/packages/designer/src/builtin-simulator/utils/clickable.ts b/packages/designer/src/builtin-simulator/utils/clickable.ts index 4169aa03c..1b2f53691 100644 --- a/packages/designer/src/builtin-simulator/utils/clickable.ts +++ b/packages/designer/src/builtin-simulator/utils/clickable.ts @@ -1,3 +1,4 @@ +import { getClosestNode } from '@ali/lowcode-utils'; import { Node } from '../../document'; /** @@ -13,8 +14,14 @@ export const getClosestClickableNode = ( // 执行 onClickHook 来判断当前节点是否可点击 while (node) { const onClickHook = node.componentMeta?.getMetadata()?.experimental?.callbacks?.onClickHook; - const canClick = + const lockedNode = getClosestNode(node, (n) => { + return n?.getExtraProp('isLocked')?.getValue() === true; + }); + let canClick = onClickHook && typeof onClickHook === 'function' ? onClickHook(event, node) : true; + if (lockedNode && lockedNode.getId() !== node.getId()) { + canClick = false; + } if (canClick) { break; } diff --git a/packages/designer/src/component-meta.ts b/packages/designer/src/component-meta.ts index aa3a60090..0da0d5f09 100644 --- a/packages/designer/src/component-meta.ts +++ b/packages/designer/src/component-meta.ts @@ -1,3 +1,4 @@ +import { ReactElement } from 'react'; import { ComponentMetadata, NpmInfo, @@ -12,18 +13,13 @@ import { LiveTextEditingConfig, FieldConfig, } from '@ali/lowcode-types'; -import { computed } from '@ali/lowcode-editor-core'; +import { computed, engineConfig } from '@ali/lowcode-editor-core'; +import EventEmitter from 'events'; + import { isNode, Node, ParentalNode } from './document'; import { Designer } from './designer'; import { intlNode } from './locale'; -import { IconContainer } from './icons/container'; -import { IconPage } from './icons/page'; -import { IconComponent } from './icons/component'; -import { IconRemove } from './icons/remove'; -import { IconClone } from './icons/clone'; -import { ReactElement } from 'react'; -import { IconHidden } from './icons/hidden'; -import EventEmitter from 'events'; +import { IconLock, IconUnlock, IconContainer, IconPage, IconComponent, IconRemove, IconClone, IconHidden } from './icons'; function ensureAList(list?: string | string[]): string[] | null { if (!list) { @@ -148,6 +144,7 @@ export class ComponentMeta { // if _title is TitleConfig get _title.icon return ( this._transformedMetadata?.icon || + // eslint-disable-next-line (this.componentName === 'Page' ? IconPage : this.isContainer ? IconContainer : IconComponent) ); } @@ -262,7 +259,7 @@ export class ComponentMeta { // eslint-disable-next-line prefer-const let { disableBehaviors, actions } = this._transformedMetadata?.configure.component || {}; const disabled = - ensureAList(disableBehaviors) || (this.isRootComponent(false) ? ['copy', 'remove'] : null); + ensureAList(disableBehaviors) || (this.isRootComponent(false) ? ['copy', 'remove', 'lock', 'unlock'] : null); actions = builtinComponentActions.concat( this.designer.getGlobalComponentActions() || [], actions || [], @@ -471,6 +468,36 @@ const builtinComponentActions: ComponentAction[] = [ }, important: true, }, + { + name: 'lock', + content: { + icon: IconUnlock, // 解锁icon + title: intlNode('lock'), + action(node: Node) { + node.getExtraProp('isLocked', true)?.setValue(true); + }, + }, + condition: (node: Node) => { + const isLocked = node.getExtraProp('isLocked')?.getValue(); + return (engineConfig.get('enableCanvasLock', false) && node.isContainer() && isLocked !== true); + }, + important: true, + }, + { + name: 'unlock', + content: { + icon: IconLock, // 锁定icon + title: intlNode('unlock'), + action(node: Node) { + node.getExtraProp('isLocked', true)?.setValue(false); + }, + }, + condition: (node: Node) => { + const isLocked = node.getExtraProp('isLocked')?.getValue(); + return (engineConfig.get('enableCanvasLock', false) && node.isContainer() && isLocked === true); + }, + important: true, + }, ]; export function removeBuiltinComponentAction(name: string) { @@ -484,11 +511,11 @@ export function addBuiltinComponentAction(action: ComponentAction) { } export function modifyBuiltinComponentAction( - actionName, + actionName: string, handle: (action: ComponentAction) => void, ) { const builtinAction = builtinComponentActions.find((action) => action.name === actionName); if (builtinAction) { handle(builtinAction); } -} \ No newline at end of file +} diff --git a/packages/designer/src/designer/dragon.ts b/packages/designer/src/designer/dragon.ts index eb08e7b4a..56c2177be 100644 --- a/packages/designer/src/designer/dragon.ts +++ b/packages/designer/src/designer/dragon.ts @@ -247,7 +247,7 @@ export class Dragon { * @param dragObject 拖拽对象 * @param boostEvent 拖拽初始时事件 */ - boost(dragObject: DragObject, boostEvent: MouseEvent | DragEvent, isFromRGLNode?: boolean) { + boost(dragObject: DragObject, boostEvent: MouseEvent | DragEvent, fromRglNode?: Node) { const { designer } = this; const masterSensors = this.getMasterSensors(); const handleEvents = makeEventsHandler(boostEvent, masterSensors); @@ -318,14 +318,31 @@ export class Dragon { return; } lastArrive = e; + + const { isRGL, rglNode } = getRGL(e); const locateEvent = createLocateEvent(e); const sensor = chooseSensor(locateEvent); - const { isRGL, rglNode } = getRGL(e); + if (isRGL) { + // 禁止被拖拽元素的阻断 + const nodeInst = dragObject.nodes[0].getDOMNode(); + if (nodeInst && nodeInst.style) { + this.nodeInstPointerEvents = true; + nodeInst.style.pointerEvents = 'none'; + } + // 原生拖拽 + this.emitter.emit('rgl.sleeping', false); + if (fromRglNode && fromRglNode.id === rglNode.id) { + designer.clearLocation(); + this.clearState(); + this.emitter.emit('drag', locateEvent); + return; + } this._canDrop = !!sensor?.locate(locateEvent); if (this._canDrop) { this.emitter.emit('rgl.add.placeholder', { rglNode, + fromRglNode, node: locateEvent.dragObject.nodes[0], event: e, }); @@ -337,6 +354,7 @@ export class Dragon { } else { this._canDrop = false; this.emitter.emit('rgl.remove.placeholder'); + this.emitter.emit('rgl.sleeping', true); } if (sensor) { sensor.fixEvent(locateEvent); @@ -397,6 +415,15 @@ export class Dragon { // end-tail drag process const over = (e?: any) => { + // 禁止被拖拽元素的阻断 + if (this.nodeInstPointerEvents) { + const nodeInst = dragObject.nodes[0].getDOMNode(); + if (nodeInst && nodeInst.style) { + nodeInst.style.pointerEvents = ''; + } + this.nodeInstPointerEvents = false; + } + // 发送drop事件 if (e) { const { isRGL, rglNode } = getRGL(e); @@ -414,6 +441,9 @@ export class Dragon { } } + // 移除磁帖占位消息 + this.emitter.emit('rgl.remove.placeholder'); + /* istanbul ignore next */ if (e && isDragEvent(e)) { e.preventDefault(); diff --git a/packages/designer/src/icons/index.ts b/packages/designer/src/icons/index.ts new file mode 100644 index 000000000..f01864604 --- /dev/null +++ b/packages/designer/src/icons/index.ts @@ -0,0 +1,10 @@ +export * from './lock'; +export * from './hidden'; +export * from './remove'; +export * from './setting'; +export * from './component'; +export * from './clone'; +export * from './page'; +export * from './container'; +export * from './unlock'; + diff --git a/packages/designer/src/icons/lock.tsx b/packages/designer/src/icons/lock.tsx new file mode 100644 index 000000000..84a117b0e --- /dev/null +++ b/packages/designer/src/icons/lock.tsx @@ -0,0 +1,11 @@ +import { SVGIcon, IconProps } from '@ali/lowcode-utils'; + +export function IconLock(props: IconProps) { + return ( + <SVGIcon viewBox="0 0 1024 1024" {...props}> + <path d="M704 480v-160c0-105.6-86.4-192-192-192s-192 86.4-192 192v160H160v416h704V480h-160z m-320-160c0-70.4 57.6-128 128-128s128 57.6 128 128v160h-256v-160z m416 512H224v-288h576v288z" fill="#ffffff" p-id="2680" /> + <path d="M480 768h64v-160h-64z" fill="#ffffff" p-id="2681" /> + </SVGIcon> + ); +} +IconLock.displayName = 'IconLock'; diff --git a/packages/designer/src/icons/unlock.tsx b/packages/designer/src/icons/unlock.tsx new file mode 100644 index 000000000..4ec6a3f70 --- /dev/null +++ b/packages/designer/src/icons/unlock.tsx @@ -0,0 +1,11 @@ +import { SVGIcon, IconProps } from '@ali/lowcode-utils'; + +export function IconUnlock(props: IconProps) { + return ( + <SVGIcon viewBox="0 0 1024 1024" {...props}> + <path d="M384 480v-160c0-70.4 57.6-128 128-128s128 57.6 128 128v64h64v-64c0-105.6-86.4-192-192-192s-192 86.4-192 192v160H160v416h704V480H384z m416 352H224v-288h576v288z" fill="#ffffff" p-id="2813" /> + <path d="M416 736h192v-64h-192z" fill="#ffffff" p-id="2814" /> + </SVGIcon> + ); +} +IconUnlock.displayName = 'IconUnlock'; diff --git a/packages/designer/src/locale/en-US.json b/packages/designer/src/locale/en-US.json index d895e4aba..cb96a5bc7 100644 --- a/packages/designer/src/locale/en-US.json +++ b/packages/designer/src/locale/en-US.json @@ -2,6 +2,8 @@ "copy": "Copy", "remove": "Remove", "hide": "Hide", + "lock": "Lock", + "unlock": "Unlock", "Condition Group": "Condition Group", "No opened document": "No opened document, open some document to editing" } diff --git a/packages/designer/src/locale/zh-CN.json b/packages/designer/src/locale/zh-CN.json index dc57f75ff..0caf4fef0 100644 --- a/packages/designer/src/locale/zh-CN.json +++ b/packages/designer/src/locale/zh-CN.json @@ -2,6 +2,8 @@ "copy": "复制", "remove": "删除", "hide": "隐藏", + "lock": "锁定", + "unlock": "解锁", "Condition Group": "条件组", "No opened document": "没有打开的页面,请选择页面打开编辑" } diff --git a/packages/editor-core/src/config.ts b/packages/editor-core/src/config.ts index 7e0734ef8..398b7b7fe 100644 --- a/packages/editor-core/src/config.ts +++ b/packages/editor-core/src/config.ts @@ -51,6 +51,10 @@ export interface EngineOptions { * 禁止默认的设置器,默认值:false */ disableDefaultSetters: boolean; + /** + * 打开画布的锁定操作,默认值:false + */ + enableCanvasLock: boolean; /** * Vision-polyfill settings */ diff --git a/packages/editor-skeleton/src/components/settings/settings-primary-pane.tsx b/packages/editor-skeleton/src/components/settings/settings-primary-pane.tsx index ec3aed989..44aa8ea82 100644 --- a/packages/editor-skeleton/src/components/settings/settings-primary-pane.tsx +++ b/packages/editor-skeleton/src/components/settings/settings-primary-pane.tsx @@ -118,6 +118,7 @@ export class SettingsPrimaryPane extends Component<{ editor: Editor; config: any render() { const { settings } = this.main; + const editor = globalContext.get(Editor); if (!settings) { // 未选中节点,提示选中 或者 显示根节点设置 return ( @@ -140,7 +141,7 @@ export class SettingsPrimaryPane extends Component<{ editor: Editor; config: any } if (!settings.isSameComponent) { - // todo: future support 获取设置项交集编辑 + // TODO: future support 获取设置项交集编辑 return ( <div className="lc-settings-main"> <div className="lc-settings-notice"> @@ -179,7 +180,19 @@ export class SettingsPrimaryPane extends Component<{ editor: Editor; config: any matched = true; } return ( - <Tab.Item className="lc-settings-tab-item" title={<Title title={field.title} />} key={field.name}> + <Tab.Item + className="lc-settings-tab-item" + title={<Title title={field.title} />} + key={field.name} + onClick={ + () => { + editor?.emit('skeleton.settingsPane.change', { + name: field.name, + title: field.title, + }); + } + } + > <SkeletonContext.Consumer> {(skeleton) => { if (skeleton) { diff --git a/packages/engine/src/engine-core.ts b/packages/engine/src/engine-core.ts index 3adf939a2..a893700f4 100644 --- a/packages/engine/src/engine-core.ts +++ b/packages/engine/src/engine-core.ts @@ -178,7 +178,6 @@ export async function init(container?: Element, options?: EngineOptions) { } } engineContainer.id = 'engine'; - engineConfig.setConfig(engineOptions as any); await plugins.init(); diff --git a/packages/plugin-outline-pane/src/icons/index.ts b/packages/plugin-outline-pane/src/icons/index.ts new file mode 100644 index 000000000..4681aeb29 --- /dev/null +++ b/packages/plugin-outline-pane/src/icons/index.ts @@ -0,0 +1,2 @@ +export * from './lock'; +export * from './unlock'; diff --git a/packages/plugin-outline-pane/src/tree-node.ts b/packages/plugin-outline-pane/src/tree-node.ts index e021d6b65..75e715e92 100644 --- a/packages/plugin-outline-pane/src/tree-node.ts +++ b/packages/plugin-outline-pane/src/tree-node.ts @@ -12,6 +12,7 @@ export default class TreeNode { * 是否可以展开 */ @computed get expandable(): boolean { + if (this.locked) return false; return this.hasChildren() || this.hasSlots() || this.dropDetail?.index != null; } @@ -86,14 +87,14 @@ export default class TreeNode { } @computed get locked(): boolean { - return this.node.getExtraProp('locked', false)?.getValue() === true; + return this.node.getExtraProp('isLocked', false)?.getValue() === true; } setLocked(flag: boolean) { if (flag) { - this.node.getExtraProp('locked', true)?.setValue(true); + this.node.getExtraProp('isLocked', true)?.setValue(true); } else { - this.node.getExtraProp('locked', false)?.remove(); + this.node.getExtraProp('isLocked', false)?.remove(); } } @@ -140,7 +141,7 @@ export default class TreeNode { return this.node.componentMeta.icon; } - @computed get parent() { + @computed get parent(): TreeNode | null { const { parent } = this.node; if (parent) { return this.tree.getTreeNode(parent); diff --git a/packages/plugin-outline-pane/src/tree.ts b/packages/plugin-outline-pane/src/tree.ts index a34bdfd21..d7cd77291 100644 --- a/packages/plugin-outline-pane/src/tree.ts +++ b/packages/plugin-outline-pane/src/tree.ts @@ -12,7 +12,9 @@ export class Tree { constructor(document: DocumentModel) { this.document = document; - this.root = this.getTreeNode(document.rootNode); + if (document.rootNode) { + this.root = this.getTreeNode(document.rootNode); + } this.id = document.id; } diff --git a/packages/plugin-outline-pane/src/views/tree-title.tsx b/packages/plugin-outline-pane/src/views/tree-title.tsx index 165b88534..ea48ae684 100644 --- a/packages/plugin-outline-pane/src/views/tree-title.tsx +++ b/packages/plugin-outline-pane/src/views/tree-title.tsx @@ -1,6 +1,8 @@ import { Component, KeyboardEvent, FocusEvent, Fragment } from 'react'; import classNames from 'classnames'; import { observer, Title, Tip, globalContext, Editor } from '@ali/lowcode-editor-core'; +import { createIcon } from '@ali/lowcode-utils'; + import { IconArrowRight } from '../icons/arrow-right'; import { IconEyeClose } from '../icons/eye-close'; import { intl, intlNode } from '../locale'; @@ -10,7 +12,8 @@ import { IconCond } from '../icons/cond'; import { IconLoop } from '../icons/loop'; import { IconRadioActive } from '../icons/radio-active'; import { IconRadio } from '../icons/radio'; -import { createIcon } from '@ali/lowcode-utils'; +import { IconLock, IconUnlock } from '../icons'; + function emitOutlineEvent(type: string, treeNode: TreeNode, rest?: Record<string, unknown>) { const editor = globalContext.get(Editor); @@ -83,6 +86,7 @@ export default class TreeTitle extends Component<{ const isCNode = !treeNode.isRoot(); const { node } = treeNode; const isNodeParent = node.isParental(); + const isContainer = node.isContainer(); let style: any; if (isCNode) { const { depth } = treeNode; @@ -164,34 +168,34 @@ export default class TreeTitle extends Component<{ )} </div> {isCNode && isNodeParent && !isModal && <HideBtn treeNode={treeNode} />} - {/* isCNode && isNodeParent && <LockBtn treeNode={treeNode} /> */} + {isContainer && isCNode && isNodeParent && <LockBtn treeNode={treeNode} />} </div> ); } } -// @observer -// class LockBtn extends Component<{ treeNode: TreeNode }> { -// shouldComponentUpdate() { -// return false; -// } +@observer +class LockBtn extends Component<{ treeNode: TreeNode }> { + shouldComponentUpdate() { + return false; + } -// render() { -// const { treeNode } = this.props; -// return ( -// <div -// className="tree-node-lock-btn" -// onClick={(e) => { -// e.stopPropagation(); -// treeNode.setLocked(!treeNode.locked); -// }} -// > -// {treeNode.locked ? <IconLock /> : <IconUnlock />} -// <Tip>{treeNode.locked ? intl('Unlock') : intl('Lock')}</Tip> -// </div> -// ); -// } -// } + render() { + const { treeNode } = this.props; + return ( + <div + className="tree-node-lock-btn" + onClick={(e) => { + e.stopPropagation(); + treeNode.setLocked(!treeNode.locked); + }} + > + {treeNode.locked ? <IconLock /> : <IconUnlock />} + <Tip>{treeNode.locked ? intl('Unlock') : intl('Lock')}</Tip> + </div> + ); + } +} @observer class HideBtn extends Component<{ treeNode: TreeNode }> { @@ -217,6 +221,7 @@ class HideBtn extends Component<{ treeNode: TreeNode }> { } } + @observer class ExpandBtn extends Component<{ treeNode: TreeNode }> { shouldComponentUpdate() { diff --git a/packages/react-simulator-renderer/src/renderer-view.tsx b/packages/react-simulator-renderer/src/renderer-view.tsx index 2b46c40e3..cf8186120 100644 --- a/packages/react-simulator-renderer/src/renderer-view.tsx +++ b/packages/react-simulator-renderer/src/renderer-view.tsx @@ -1,10 +1,12 @@ +import { ReactInstance, Fragment, Component, createElement } from 'react'; +import { Router, Route, Switch } from 'react-router'; +import cn from 'classnames'; import { Node } from '@ali/lowcode-designer'; import LowCodeRenderer from '@ali/lowcode-react-renderer'; -import { ReactInstance, Fragment, Component, createElement } from 'react'; import { observer } from '@recore/obx-react'; -import { isFromVC } from '@ali/lowcode-utils'; +import { isFromVC, getClosestNode } from '@ali/lowcode-utils'; import { SimulatorRendererContainer, DocumentInstance } from './renderer'; -import { Router, Route, Switch } from 'react-router'; + import './renderer.less'; const DEFAULT_SIMULATOR_LOCALE = 'zh-CN'; @@ -171,9 +173,16 @@ class Renderer extends Component<{ (children == null || (Array.isArray(children) && !children.length)) && (!viewProps.style || Object.keys(viewProps.style).length === 0) ) { + let defaultPlaceholder = '拖拽组件或模板到这里'; + const lockedNode = getClosestNode(leaf, (node) => { + return node?.getExtraProp('isLocked')?.getValue() === true; + }); + if (lockedNode) { + defaultPlaceholder = '锁定元素及子元素无法编辑'; + } children = ( - <div className="lc-container-placeholder" style={viewProps.placeholderStyle}> - {viewProps.placeholder || '拖拽组件或模板到这里'} + <div className={cn('lc-container-placeholder', { 'lc-container-locked': !!lockedNode })} style={viewProps.placeholderStyle}> + {viewProps.placeholder || defaultPlaceholder} </div> ); } diff --git a/packages/react-simulator-renderer/src/renderer.less b/packages/react-simulator-renderer/src/renderer.less index aed023f1b..8f0119353 100644 --- a/packages/react-simulator-renderer/src/renderer.less +++ b/packages/react-simulator-renderer/src/renderer.less @@ -65,6 +65,10 @@ html.engine-cursor-ew-resize, html.engine-cursor-ew-resize * { align-items: center; justify-content: center; font-size: 14px; + + &.lc-container-locked { + background: #eccfcf; + } } body.engine-document { @@ -87,3 +91,5 @@ body.engine-document { #app { height: 100vh; } + + diff --git a/packages/types/src/schema.ts b/packages/types/src/schema.ts index 21856bc57..85f8521bd 100644 --- a/packages/types/src/schema.ts +++ b/packages/types/src/schema.ts @@ -22,6 +22,7 @@ export interface NodeSchema { loop?: CompositeValue; // 循环数据 loopArgs?: [string, string]; // 循环迭代对象、索引名称 ["item", "index"] children?: NodeData | NodeData[]; // 子节点 + isLocked?: boolean; // 是否锁定 // ------- future support ----- conditionGroup?: string; diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index ca211fd36..c79c8ddb9 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -22,3 +22,4 @@ export * from './build-components'; export * from './appHelper'; export * from './misc'; export * from './schema'; +export * from './node-helper'; diff --git a/packages/utils/src/node-helper.ts b/packages/utils/src/node-helper.ts new file mode 100644 index 000000000..0b6b768ff --- /dev/null +++ b/packages/utils/src/node-helper.ts @@ -0,0 +1,14 @@ +// 仅使用类型 +import { Node } from '@ali/lowcode-designer'; + +export const getClosestNode = (node: Node, until: (node: Node) => boolean): Node | undefined => { + if (!node) { + return undefined; + } + if (until(node)) { + return node; + } else { + // @ts-ignore + return getClosestNode(node.getParent(), until); + } +};