Browse Source

fix: 修改锁定功能

Co-authored-by: Copilot <copilot@github.com>
jiaxing.liao 1 month ago
parent
commit
66f547aa0a

+ 1 - 1
src/renderer/src/lvgl-widgets/ImageBg.vue

@@ -5,7 +5,7 @@
     :style="{ opacity: imageStyle?.opacity }"
     v-bind="$attrs"
   >
-    <img v-bind="imageProps" ref="imgRef" class="absolute z-0" :src="getImgSrc" />
+    <img v-bind="imageProps" ref="imgRef" class="absolute z-0" :src="getImgSrc" draggable="false" />
     <div v-if="hasCoverColor" class="absolute z-1 pointer-events-none" :style="maskStyle"></div>
   </div>
 </template>

+ 1 - 1
src/renderer/src/lvgl-widgets/container/style.json

@@ -15,7 +15,7 @@
         },
         "border": {
           "color": "#2092f5ff",
-          "width": 2,
+          "width": 0,
           "radius": 0,
           "side": ["all"]
         },

+ 34 - 12
src/renderer/src/store/modules/action.ts

@@ -30,6 +30,22 @@ export const useActionStore = defineStore('action', () => {
   const appStore = useAppStore()
   const clipboard = ref<BaseWidget[]>()
 
+  const isLockedWidget = (widget?: BaseWidget | Page) => {
+    return !!(widget && 'locked' in widget && widget.locked)
+  }
+
+  const hasLockedDescendant = (widget?: BaseWidget | Page): boolean => {
+    return !!widget?.children?.some((child) => isLockedWidget(child) || hasLockedDescendant(child))
+  }
+
+  const isWidgetTreeLocked = (widget?: BaseWidget | Page) => {
+    return isLockedWidget(widget) || hasLockedDescendant(widget)
+  }
+
+  const getEditableWidgets = (widgets = projectStore.activeWidgets) => {
+    return widgets.filter((widget) => !isWidgetTreeLocked(widget))
+  }
+
   const findWidgetLocation = (
     parent: Page | BaseWidget,
     widgetId: string,
@@ -98,7 +114,8 @@ export const useActionStore = defineStore('action', () => {
    * @param type 对齐类型
    */
   const onAlign = (type: AlignType) => {
-    const widgets = projectStore.activeWidgets
+    const widgets = getEditableWidgets()
+    if (!widgets.length) return
     const screen = projectStore.activeScreen
     switch (type) {
       case 'left': {
@@ -203,7 +220,8 @@ export const useActionStore = defineStore('action', () => {
    * @param type 宽高匹配类型
    */
   const onMatchSize = (type: 'width' | 'height' | 'both') => {
-    const widgets = projectStore.activeWidgets
+    const widgets = getEditableWidgets()
+    if (!widgets.length) return
     switch (type) {
       case 'width': {
         const width = Math.max(...widgets.map((widget) => widget.props.width))
@@ -238,7 +256,8 @@ export const useActionStore = defineStore('action', () => {
    * @param type 层级调整类型
    */
   const onLevel = (type: 'up' | 'down' | 'top' | 'bottom') => {
-    let widgetIds = projectStore.activeWidgets.map((widget) => widget.id)
+    let widgetIds = getEditableWidgets().map((widget) => widget.id)
+    if (!widgetIds.length) return
 
     bfsWalk(projectStore.activePage, (child) => {
       // 对子节点遍历排序
@@ -296,7 +315,7 @@ export const useActionStore = defineStore('action', () => {
       screen.pages.forEach((page) => {
         bfsWalk(page, (child) => {
           const index = child?.children?.findIndex((item) => item.id === widgetId) ?? -1
-          if (index !== -1) {
+          if (index !== -1 && !isWidgetTreeLocked(child.children[index])) {
             child.children.splice(index, 1)
           }
         })
@@ -308,7 +327,7 @@ export const useActionStore = defineStore('action', () => {
    * 删除控件
    */
   const onDelete = () => {
-    const widgetIds = projectStore.activeWidgets.map((widget) => widget.id)
+    const widgetIds = getEditableWidgets().map((widget) => widget.id)
     if (!widgetIds.length) return
 
     projectStore.project?.screens.forEach((screen) => {
@@ -324,7 +343,7 @@ export const useActionStore = defineStore('action', () => {
         })
       })
     })
-    projectStore.activeWidgets = []
+    projectStore.activeWidgets = projectStore.activeWidgets.filter((widget) => widget.locked)
   }
 
   /**
@@ -339,7 +358,7 @@ export const useActionStore = defineStore('action', () => {
    * 复用
    */
   const onCopyFrom = () => {
-    const widgetIds = projectStore.activeWidgets.map((widget) => widget.id)
+    const widgetIds = getEditableWidgets().map((widget) => widget.id)
     if (!widgetIds.length) return
 
     bfsWalk(projectStore.activePage, (child) => {
@@ -383,8 +402,10 @@ export const useActionStore = defineStore('action', () => {
   const onPaste = () => {
     if (!clipboard.value?.length) return
 
-    const list =
-      projectStore.activeWidgets?.[0]?.children || projectStore.activePage?.children || []
+    const target = projectStore.activeWidgets?.[0]
+    const list = !isWidgetTreeLocked(target)
+      ? target?.children || projectStore.activePage?.children || []
+      : projectStore.activePage?.children || []
     const newArr: BaseWidget[] = []
     clipboard.value.forEach((obj) => {
       obj.props.x += 10
@@ -404,7 +425,7 @@ export const useActionStore = defineStore('action', () => {
    * 剪切
    */
   const onCut = () => {
-    const widgetIds = projectStore.activeWidgets.map((widget) => widget.id)
+    const widgetIds = getEditableWidgets().map((widget) => widget.id)
     if (!widgetIds.length) return
     clipboard.value = []
     bfsWalk(projectStore.activePage, (child) => {
@@ -414,14 +435,14 @@ export const useActionStore = defineStore('action', () => {
         child.children.splice(index, 1)
       }
     })
-    projectStore.activeWidgets = []
+    projectStore.activeWidgets = projectStore.activeWidgets.filter((widget) => widget.locked)
   }
 
   /**
    * 隐藏/显示
    */
   const onHidden = (hidden: boolean) => {
-    const widgetIds = projectStore.activeWidgets.map((widget) => widget.id)
+    const widgetIds = getEditableWidgets().map((widget) => widget.id)
     if (!widgetIds.length) return
 
     bfsWalk(projectStore.activePage, (child) => {
@@ -452,6 +473,7 @@ export const useActionStore = defineStore('action', () => {
     widgetIds.forEach((widgetId) => {
       const location = findWidgetLocationInProject(widgetId)
       if (!location || location.parent.type === 'page') return
+      if (isWidgetTreeLocked(location.widget)) return
 
       const { widget, list, index, ancestors, page, screen } = location
       const offset = getUnbindOffset(ancestors.slice(1))

+ 10 - 0
src/renderer/src/views/designer/config/property/index.vue

@@ -112,6 +112,7 @@
         </el-form>
       </el-collapse-item>
     </el-collapse>
+    <div class="absolute left-0 top-0 w-full h-full" @click="handleClickMask"></div>
   </div>
 </template>
 
@@ -142,6 +143,7 @@ const part = ref<{
 })
 
 const editTheme = ref(DEFAULT_THEME_KEY)
+const isActiveWidgetLocked = computed(() => !!projectStore.activeWidget?.locked)
 
 const getStyleTheme = (item?: Record<string, any>) => {
   return item?.theme || DEFAULT_THEME_KEY
@@ -193,6 +195,13 @@ watch(
   }
 )
 
+const handleClickMask = (e: MouseEvent) => {
+  if (isActiveWidgetLocked.value) {
+    e.stopPropagation()
+    e.preventDefault()
+  }
+}
+
 const setPartByEditThemeStyle = () => {
   const widgetStyles = projectStore.activeWidget?.style || []
   const currentPartThemeStyle = widgetStyles.find(
@@ -342,6 +351,7 @@ const onChangeStyleByState = (type: 'add' | 'delete') => {
   overflow: auto;
   overscroll-behavior: contain;
   contain: layout paint;
+  position: relative;
 }
 
 ::v-deep(.el-collapse-icon-position-right .el-collapse-item__header) {

+ 9 - 0
src/renderer/src/views/designer/sidebar/Hierarchy.vue

@@ -238,10 +238,17 @@ const isDescendantNode = (node: HierarchyNodeData, childId: string) => {
   return node.children.some((item) => item.id === childId || isDescendantNode(item, childId))
 }
 
+const hasLockedWidgetTree = (node: HierarchyNodeData): boolean => {
+  return (
+    !!(node.raw as BaseWidget).locked || node.children.some((child) => hasLockedWidgetTree(child))
+  )
+}
+
 const allowDrag: AllowDragFunction = (node) => {
   const currentNode = getHierarchyNode(node.data)
   return (
     currentNode.nodeType === 'widget' &&
+    !hasLockedWidgetTree(currentNode) &&
     !!currentNode.pageId &&
     currentNode.pageId === projectStore.activePageId
   )
@@ -252,6 +259,7 @@ const allowDrop: AllowDropFunction = (dragNode, dropNode, type) => {
   const dropData = getHierarchyNode(dropNode.data)
 
   if (dragData.nodeType !== 'widget' || dropData.nodeType === 'screen') return false
+  if (hasLockedWidgetTree(dragData)) return false
   if (!dragData.pageId || !dropData.pageId || dragData.pageId !== dropData.pageId) return false
   if (dragData.id === dropData.id || isDescendantNode(dragData, dropData.id)) return false
 
@@ -312,6 +320,7 @@ const handleNodeDrop = (dragNode: TreeDragNode, dropNode: TreeDragNode, dropType
   if (dragData.nodeType !== 'widget' || !dragData.pageId || dragData.pageId !== targetData.pageId) {
     return
   }
+  if (hasLockedWidgetTree(dragData)) return
 
   const page = findPageById(dragData.pageId)
   if (!page) return

+ 2 - 1
src/renderer/src/views/designer/sidebar/components/PageTreeItem.vue

@@ -24,7 +24,7 @@
       <el-tooltip v-if="isBind" content="解除绑定">
         <span @click.capture.stop="unbind"><LuUnlink size="14px" /></span>
       </el-tooltip>
-      <el-tooltip v-if="data.type !== 'page'" content="删除">
+      <el-tooltip v-if="data.type !== 'page' && !data.locked" content="删除">
         <span @click.capture.stop="deleteWidget(data)"><LuTrash2 size="14px" /></span>
       </el-tooltip>
       <el-tooltip content="隐藏/显示">
@@ -81,6 +81,7 @@ const isBind = computed(() => {
 
 // 删除控件
 const deleteWidget = (data: BaseWidget) => {
+  if (data.locked) return
   actionStore.onDeleteById(data.id)
 }
 

+ 7 - 7
src/renderer/src/views/designer/sidebar/components/ScreenTreeItem.vue

@@ -13,33 +13,33 @@
         <span>{{ data.name }}</span>
       </div>
       <div class="flex items-center gap-4px pr-12px invisible group-hover/item:visible">
-        <el-tooltip v-if="data.type !== 'page'" content="Add page">
+        <el-tooltip v-if="data.type !== 'page'" content="新增页面">
           <span @click.capture.stop="addPage(data)"><LuPlus size="14px" /></span>
         </el-tooltip>
-        <el-tooltip content="Rename">
+        <el-tooltip content="重命名">
           <span><LuPencilLine size="14px" @click.capture.stop="edit = true" /></span>
         </el-tooltip>
-        <el-tooltip content="Delete" v-if="data.type === 'page'">
+        <el-tooltip content="删除" v-if="data.type === 'page'">
           <span>
-            <el-popconfirm class="box-item" title="Delete this page?" @confirm="deletePage(data)">
+            <el-popconfirm class="box-item" title="删除该页面?" @confirm="deletePage(data)">
               <template #reference>
                 <span><LuTrash2 size="14px" /></span>
               </template>
             </el-popconfirm>
           </span>
         </el-tooltip>
-        <el-tooltip content="Show / Hide" v-if="data.type === 'page'">
+        <!-- <el-tooltip content="显示/隐藏" v-if="data.type === 'page'">
           <span @click.capture.stop="data.hidden = !data.hidden">
             <LuEye size="14px" v-if="!data.hidden" />
             <LuEyeOff size="14px" v-else />
           </span>
         </el-tooltip>
-        <el-tooltip content="Lock / Unlock" v-if="data.type === 'page'">
+        <el-tooltip content="锁定/解锁" v-if="data.type === 'page'">
           <span @click.capture.stop="data.locked = !data.locked">
             <LuLock size="14px" v-if="!data.locked" />
             <LuUnlock size="14px" v-else />
           </span>
-        </el-tooltip>
+        </el-tooltip> -->
       </div>
     </template>
     <template v-else>

+ 13 - 1
src/renderer/src/views/designer/tools/Operate.vue

@@ -39,7 +39,8 @@ const projectMenu = computed((): MenuItemType[] => {
   const disabledAlign = projectStore.activeWidgets.length < 1
   const disabledAvg = projectStore.activeWidgets.length < 3
   const disabledLevel = !projectStore.activeWidgets.length
-  return [
+  const hasLockedSelection = projectStore.activeWidgets.some((widget) => widget.locked)
+  const menuItems: MenuItemType[] = [
     {
       key: 'undo',
       label: '撤销',
@@ -173,6 +174,17 @@ const projectMenu = computed((): MenuItemType[] => {
       onClick: () => actionStore.onLevel('bottom')
     }
   ]
+
+  return menuItems.map((item) => {
+    if (item.type || item.key === 'undo' || item.key === 'redo' || !hasLockedSelection) {
+      return item
+    }
+
+    return {
+      ...item,
+      disabled: true
+    }
+  })
 })
 </script>
 

+ 10 - 0
src/renderer/src/views/designer/workspace/stage/ContextMenu.vue

@@ -55,6 +55,7 @@ import {
 } from 'vue-icons-plus/lu'
 
 import { useActionStore } from '@/store/modules/action'
+import { useProjectStore } from '@/store/modules/project'
 
 type MenuItem = {
   label: string
@@ -73,6 +74,7 @@ const props = defineProps<{
 
 const dropdownRef = ref<DropdownInstance>()
 const actionStore = useActionStore()
+const projectStore = useProjectStore()
 const bus = useEventBus('context-menu')
 const createCustomCompBus = useEventBus('create-custom-comp')
 
@@ -90,6 +92,10 @@ const createCustomComponent = () => {
   createCustomCompBus.emit('create', props.id)
 }
 
+const isCurrentWidgetLocked = computed(() => {
+  return props.widgetType === 'widget' && !!projectStore.activeWidgetMap[props.id]?.locked
+})
+
 const widgetMenus = computed<MenuItem[]>(() => {
   const menus: MenuItem[] = [
     {
@@ -130,6 +136,10 @@ const widgetMenus = computed<MenuItem[]>(() => {
     }
   ]
 
+  if (isCurrentWidgetLocked.value) {
+    return menus.filter((item) => ['copy'].includes(item.value))
+  }
+
   if (actionStore.isWidgetBoundById(props.id)) {
     menus.push({
       label: '解绑',

+ 38 - 2
src/renderer/src/views/designer/workspace/stage/Moveable.vue

@@ -2,8 +2,8 @@
   <Moveable
     v-if="pageId && pageId === projectStore.activePageId"
     :target="moveableTarget"
-    :draggable="true"
-    :resizable="true"
+    :draggable="canTransformSelection"
+    :resizable="canTransformSelection"
     :groupable="isGroupSelection"
     :rotatable="false"
     :padding="2"
@@ -79,6 +79,12 @@ const projectStore = useProjectStore()
 const appStore = useAppStore()
 const isGroupSelection = computed(() => elements.value.length > 1)
 const isIndividualGroupable = computed(() => elements.value.length === 1)
+const canTransformSelection = computed(() => {
+  return (
+    projectStore.activeWidgets.length > 0 &&
+    projectStore.activeWidgets.every((item) => !hasLockedWidgetTree(item))
+  )
+})
 const moveableTarget = computed<HTMLElement | HTMLElement[] | null>(() => {
   if (isGroupSelection.value) {
     return elements.value
@@ -157,6 +163,11 @@ useMutationObserver(
 const zIndex = ref()
 
 const individualGroupableProps = (element: HTMLElement | SVGElement | null | undefined) => {
+  const widgetId = element?.getAttribute('widget-id') || ''
+  if (widgetId && hasLockedWidgetTree(projectStore.activeWidgetMap[widgetId])) {
+    return { draggable: false, resizable: false, rotatable: false }
+  }
+
   const widgetType = element?.getAttribute('widget-type') || ''
   if (['lv_checkbox', 'qrcode'].includes(widgetType)) {
     return { resizable: false }
@@ -190,14 +201,25 @@ const getScale = (widget: Page | BaseWidget) => {
   return (widget.props?.scale ?? 256) / 256
 }
 
+const hasLockedWidgetTree = (widget?: BaseWidget | Page): boolean => {
+  return (
+    !!widget &&
+    (!!widget.locked || !!widget.children?.some((child) => hasLockedWidgetTree(child)))
+  )
+}
+
 // 拖拽开始
 const onDragStart = (e) => {
+  const id = e.target.attributes['widget-id']?.value
+  if (id && hasLockedWidgetTree(projectStore.activeWidgetMap[id])) return false
   zIndex.value = e.target.style.zIndex
   e.target.style.zIndex = 999
   appStore.draging = true
 }
 // 渲染节点拖拽
 const onDrag = (e) => {
+  const id = e.target.attributes['widget-id']?.value
+  if (id && hasLockedWidgetTree(projectStore.activeWidgetMap[id])) return
   // 当前选中节点整体移动
   e.target.style.transform = e.transform
 }
@@ -205,6 +227,10 @@ const onDrag = (e) => {
 const onDragEnd = (e) => {
   e.target.style.zIndex = zIndex.value
   const id = e.target.attributes['widget-id']?.value
+  if (id && hasLockedWidgetTree(projectStore.activeWidgetMap[id])) {
+    appStore.draging = false
+    return
+  }
   if (id && projectStore.activeWidgetMap[id] && e?.lastEvent?.translate) {
     projectStore.activeWidgetMap[id].props.x = Math.round(e.lastEvent.translate[0])
     projectStore.activeWidgetMap[id].props.y = Math.round(e.lastEvent.translate[1])
@@ -213,6 +239,8 @@ const onDragEnd = (e) => {
 }
 // 渲染节点缩放
 const onResize = (e) => {
+  const id = e.target.attributes['widget-id']?.value
+  if (id && hasLockedWidgetTree(projectStore.activeWidgetMap[id])) return
   e.target.style.width = `${e.width}px`
   e.target.style.height = `${e.height}px`
   e.target.style.transform = e.drag.transform
@@ -220,6 +248,7 @@ const onResize = (e) => {
 // 节点缩放完成
 const onResizeEnd = (e) => {
   const id = e.target.attributes['widget-id']?.value
+  if (id && hasLockedWidgetTree(projectStore.activeWidgetMap[id])) return
   if (e.lastEvent && id && projectStore.activeWidgetMap[id]) {
     const scale = getScale(projectStore.activeWidgetMap[id])
     let width = Math.round(e.lastEvent.width / scale)
@@ -269,12 +298,16 @@ const onRender = (e) => {
 // 节点组拖拽
 const onDragGroup = ({ events }) => {
   events.forEach((ev) => {
+    const id = ev.target.attributes['widget-id']?.value
+    if (id && hasLockedWidgetTree(projectStore.activeWidgetMap[id])) return
     ev.target.style.transform = ev.transform
   })
 }
 // 节点组缩放
 const onResizeGroup = ({ events }) => {
   events.forEach((ev) => {
+    const id = ev.target.attributes['widget-id']?.value
+    if (id && hasLockedWidgetTree(projectStore.activeWidgetMap[id])) return
     ev.target.style.width = `${ev.width}px`
     ev.target.style.height = `${ev.height}px`
     ev.target.style.transform = ev.drag.transform
@@ -284,6 +317,7 @@ const onResizeGroup = ({ events }) => {
 const onDragGroupEnd = ({ events }) => {
   events.forEach((ev) => {
     const id = ev.target.attributes['widget-id']?.value
+    if (id && hasLockedWidgetTree(projectStore.activeWidgetMap[id])) return
     if (id && projectStore.activeWidgetMap[id] && ev?.lastEvent?.translate) {
       projectStore.activeWidgetMap[id].props.x = Math.round(ev.lastEvent.translate[0])
       projectStore.activeWidgetMap[id].props.y = Math.round(ev.lastEvent.translate[1])
@@ -294,6 +328,7 @@ const onDragGroupEnd = ({ events }) => {
 const onResizeGroupEnd = ({ events }) => {
   events.forEach((ev) => {
     const id = ev.target.attributes['widget-id']?.value
+    if (id && hasLockedWidgetTree(projectStore.activeWidgetMap[id])) return
     if (ev.lastEvent && id && projectStore.activeWidgetMap[id]) {
       const scale = getScale(projectStore.activeWidgetMap[id])
       let width = Math.round(ev.lastEvent.width / scale)
@@ -341,6 +376,7 @@ const onResizeGroupEnd = ({ events }) => {
 // 旋转结束
 const onRotateEnd = (e) => {
   const id = e.target.attributes['widget-id']?.value
+  if (id && hasLockedWidgetTree(projectStore.activeWidgetMap[id])) return
   if (e.lastEvent && id && projectStore.activeWidgetMap[id]) {
     // 设置位置
     projectStore.activeWidgetMap[id].props.rotation = Math.round(e.lastEvent.rotate)

+ 17 - 0
src/renderer/src/views/designer/workspace/stage/Node.vue

@@ -222,6 +222,13 @@ const childStyle = computed((): CSSProperties => {
   return {}
 })
 
+const hasLockedWidgetTree = (widget?: BaseWidget | Page): boolean => {
+  return (
+    !!widget &&
+    (!!widget.locked || !!widget.children?.some((child) => hasLockedWidgetTree(child)))
+  )
+}
+
 const getDropPosition = (event?: DragEvent) => {
   const element = nodeRef.value
   const rect = element?.getBoundingClientRect()
@@ -245,6 +252,7 @@ useDrop(nodeRef, {
     const { schema } = props
 
     dropStyle.value = {}
+    if (hasLockedWidgetTree(schema)) return
     if (!content || !schema?.children) return
 
     const { x, y } = getDropPosition(event)
@@ -316,6 +324,13 @@ watch(nodeState, (state) => {
     const { elementW, elementH, elementX, elementY, clientX, clientY } = state
 
     if (dropFlag && !appStore.draging) {
+      if (hasLockedWidgetTree(schema) || hasLockedWidgetTree(projectStore.activeWidgets[0])) {
+        dropFlag = false
+        clearDropMouseupListener()
+        dropStyle.value = {}
+        return
+      }
+
       dropFlag = false
       clearDropMouseupListener()
       const scale = elementW / schema.props.width
@@ -343,6 +358,8 @@ watch(nodeState, (state) => {
       elementY < elementH &&
       appStore.draging &&
       projectStore.activeWidgets.length === 1 &&
+      !hasLockedWidgetTree(projectStore.activeWidgets[0]) &&
+      !hasLockedWidgetTree(schema) &&
       projectStore.activeWidgets[0].id !== schema.id &&
       !isDescendant(schema, projectStore.activeWidgets[0].id) &&
       schema.type !== 'page'