Explorar o código

✨ feat(组态编辑器): 全面优化编辑界面与配置交互

- 重构顶部操作栏布局与视觉样式,提升按钮辨识度和操作反馈
- 新增组件置顶、置底功能,并支持边界禁用和历史记录
- 优化左侧组件库,调整栏宽、分类布局、搜索和组件提示
- 重构组件管理弹窗,完善分类筛选、全选和素材管理交互
- 优化右侧属性面板与页面设置,统一表单层级和视觉规范
- 升级图形结构树,增加搜索、数量统计、批量显隐和层级调整
- 重构事件配置流程,优化触发方式、执行动作和触发条件设置
- 升级设备绑定交互,完善绑定关系编辑、校验和状态展示
Zimo hai 19 horas
pai
achega
5ae730ce27

+ 102 - 1
src/components/mt-edit/components/layout/header-panel/index.vue

@@ -49,6 +49,43 @@
       </div>
       </div>
 
 
       <div class="tool-group">
       <div class="tool-group">
+        <el-popover
+          v-model:visible="layerPopoverVisible"
+          placement="bottom"
+          :width="320"
+          trigger="click"
+          :disabled="!headerPanelProps.layerEnabled">
+          <template #reference>
+            <tool-button label="层级" :disabled="!headerPanelProps.layerEnabled">
+              <Files />
+            </tool-button>
+          </template>
+          <div class="layer-control-panel">
+            <div class="layer-control-panel__heading">
+              <strong>调整层级</strong>
+              <span>控制选中组件在画布中的前后顺序</span>
+            </div>
+            <div class="layer-control-panel__actions">
+              <tool-button
+                label="置顶"
+                compact
+                :disabled="!headerPanelProps.canMoveLayerUp"
+                @click="changeLayer('top')">
+                <Top />
+              </tool-button>
+              <tool-button
+                label="置底"
+                compact
+                :disabled="!headerPanelProps.canMoveLayerDown"
+                @click="changeLayer('bottom')">
+                <Bottom />
+              </tool-button>
+            </div>
+            <div class="layer-control-panel__hint">
+              快捷键:{{ systemShortcutKey }} + → 置顶,{{ systemShortcutKey }} + ← 置底
+            </div>
+          </div>
+        </el-popover>
         <el-popover
         <el-popover
           placement="bottom"
           placement="bottom"
           :width="460"
           :width="460"
@@ -172,6 +209,8 @@
 import deepOilLogo from '@/assets/imgs/logo.png'
 import deepOilLogo from '@/assets/imgs/logo.png'
 import SvgAnalysis from '@/components/mt-edit/components/svg-analysis/index.vue'
 import SvgAnalysis from '@/components/mt-edit/components/svg-analysis/index.vue'
 import type { IRealTimeData } from '@/components/mt-edit/store/types'
 import type { IRealTimeData } from '@/components/mt-edit/store/types'
+import { getSystemShortcutKeyName } from '@/components/mt-edit/utils'
+import { Bottom, Files, Top } from '@element-plus/icons-vue'
 import { useDark, useFullscreen, useToggle } from '@vueuse/core'
 import { useDark, useFullscreen, useToggle } from '@vueuse/core'
 import { ElButton, ElIcon, ElImage, ElPopover, ElTag } from 'element-plus'
 import { ElButton, ElIcon, ElImage, ElPopover, ElTag } from 'element-plus'
 import { defineComponent, h, ref } from 'vue'
 import { defineComponent, h, ref } from 'vue'
@@ -184,6 +223,9 @@ type HeaderPanelProps = {
   groupEnabled: boolean
   groupEnabled: boolean
   unGroupEnabled: boolean
   unGroupEnabled: boolean
   alignEnabled: boolean
   alignEnabled: boolean
+  layerEnabled: boolean
+  canMoveLayerUp: boolean
+  canMoveLayerDown: boolean
   deleteEnabled: boolean
   deleteEnabled: boolean
   lockState: boolean
   lockState: boolean
   undoEnabled: boolean
   undoEnabled: boolean
@@ -250,6 +292,7 @@ const emits = defineEmits([
   'onDeleteClick',
   'onDeleteClick',
   'onExportClick',
   'onExportClick',
   'onTreeClick',
   'onTreeClick',
+  'changeLayer',
   'alignSelected',
   'alignSelected',
   'update:lockState',
   'update:lockState',
   'onRedoClick',
   'onRedoClick',
@@ -265,12 +308,18 @@ const isDark = useDark({ selector: '#mt-edit' })
 const { isFullscreen, toggle } = useFullscreen()
 const { isFullscreen, toggle } = useFullscreen()
 const toggleDark = useToggle(isDark)
 const toggleDark = useToggle(isDark)
 const drawline_selected = ref(false)
 const drawline_selected = ref(false)
+const layerPopoverVisible = ref(false)
+const systemShortcutKey = getSystemShortcutKeyName()
 const is_npm_env = ref(import.meta.env.MODE === 'npm')
 const is_npm_env = ref(import.meta.env.MODE === 'npm')
 const onGroupClick = () => emits('onGroupClick')
 const onGroupClick = () => emits('onGroupClick')
 const onUngroupClick = () => emits('onUngroupClick')
 const onUngroupClick = () => emits('onUngroupClick')
 const onDeleteClick = () => emits('onDeleteClick')
 const onDeleteClick = () => emits('onDeleteClick')
 const onExportClick = () => emits('onExportClick')
 const onExportClick = () => emits('onExportClick')
 const onTreeClick = () => emits('onTreeClick')
 const onTreeClick = () => emits('onTreeClick')
+const changeLayer = (type: 'top' | 'bottom') => {
+  emits('changeLayer', type)
+  layerPopoverVisible.value = false
+}
 const alignSelected = (
 const alignSelected = (
   type:
   type:
     | 'left'
     | 'left'
@@ -498,6 +547,57 @@ const onDrawLineClick = () => {
   width: 100%;
   width: 100%;
 }
 }
 
 
+.layer-control-panel__heading {
+  display: flex;
+  padding-bottom: 10px;
+  border-bottom: 1px solid #e8ecf2;
+  flex-direction: column;
+  gap: 3px;
+}
+
+.layer-control-panel__heading strong {
+  font-size: 13px;
+  line-height: 19px;
+  color: #273244;
+}
+
+.layer-control-panel__heading span,
+.layer-control-panel__hint {
+  font-size: 11px;
+  line-height: 17px;
+  color: #8a95a6;
+}
+
+.layer-control-panel__actions {
+  display: grid;
+  padding: 10px 0;
+  grid-template-columns: repeat(2, minmax(0, 1fr));
+  gap: 7px;
+}
+
+.layer-control-panel__actions :deep(.tool-button-wrap),
+.layer-control-panel__actions :deep(.tool-button) {
+  width: 100%;
+}
+
+.layer-control-panel__actions :deep(.tool-button) {
+  padding: 9px 10px;
+  background: #f7f9fc;
+  border: 1px solid #e5eaf1;
+  justify-content: flex-start;
+}
+
+.layer-control-panel__actions :deep(.tool-button:not(.is-disabled):hover) {
+  background: var(--el-color-primary-light-9);
+  border-color: var(--el-color-primary-light-7);
+}
+
+.layer-control-panel__hint {
+  padding-top: 8px;
+  text-align: center;
+  border-top: 1px solid #e8ecf2;
+}
+
 .dark .editor-header {
 .dark .editor-header {
   color: #cbd5e1;
   color: #cbd5e1;
   background: rgb(24 31 43 / 97%);
   background: rgb(24 31 43 / 97%);
@@ -512,7 +612,8 @@ const onDrawLineClick = () => {
 }
 }
 
 
 .dark .editor-brand__name,
 .dark .editor-brand__name,
-.dark .align-panel__title {
+.dark .align-panel__title,
+.dark .layer-control-panel__heading strong {
   color: #f1f5f9;
   color: #f1f5f9;
 }
 }
 
 

+ 38 - 0
src/components/mt-edit/index.vue

@@ -14,6 +14,9 @@
           :group-enabled="header_group_enabled"
           :group-enabled="header_group_enabled"
           :un-group-enabled="header_un_group_enabled"
           :un-group-enabled="header_un_group_enabled"
           :align-enabled="header_align_enabled"
           :align-enabled="header_align_enabled"
+          :layer-enabled="header_layer_enabled"
+          :can-move-layer-up="header_can_move_layer_up"
+          :can-move-layer-down="header_can_move_layer_down"
           :delete-enabled="header_delete_enabled"
           :delete-enabled="header_delete_enabled"
           :undo-enabled="cacheStore.historyIndex > 0"
           :undo-enabled="cacheStore.historyIndex > 0"
           :redo-enabled="cacheStore.historyIndex < cacheStore.history.length - 1"
           :redo-enabled="cacheStore.historyIndex < cacheStore.history.length - 1"
@@ -25,6 +28,7 @@
           @on-delete-click="onDeleteClick"
           @on-delete-click="onDeleteClick"
           @on-export-click="onExportClick"
           @on-export-click="onExportClick"
           @on-tree-click="done_json_tree_visiable = true"
           @on-tree-click="done_json_tree_visiable = true"
+          @change-layer="onChangeLayer"
           @align-selected="onAlignSelected"
           @align-selected="onAlignSelected"
           @on-redo-click="onRedoClick"
           @on-redo-click="onRedoClick"
           @on-undo-click="onUndoClick"
           @on-undo-click="onUndoClick"
@@ -166,6 +170,18 @@ const header_align_enabled = computed(() => {
   )
   )
   return selected_items.length > 1
   return selected_items.length > 1
 })
 })
+const selected_layer_index = computed(() => {
+  if (globalStore.selected_items_id.length !== 1) {
+    return -1
+  }
+  return globalStore.done_json.findIndex((item) => item.id === globalStore.selected_items_id[0])
+})
+const header_layer_enabled = computed(() => selected_layer_index.value >= 0)
+const header_can_move_layer_up = computed(
+  () =>
+    selected_layer_index.value >= 0 && selected_layer_index.value < globalStore.done_json.length - 1
+)
+const header_can_move_layer_down = computed(() => selected_layer_index.value > 0)
 const import_visible = ref(false)
 const import_visible = ref(false)
 const export_visible = ref(false)
 const export_visible = ref(false)
 const done_json_tree_visiable = ref(false)
 const done_json_tree_visiable = ref(false)
@@ -226,6 +242,28 @@ const onDoneTreeMoveLayer = ({ id, direction }: { id: string; direction: 'up' |
   globalStore.setSingleSelect(id)
   globalStore.setSingleSelect(id)
   cacheStore.addHistory(globalStore.done_json)
   cacheStore.addHistory(globalStore.done_json)
 }
 }
+const onChangeLayer = (type: 'top' | 'bottom') => {
+  const currentIndex = selected_layer_index.value
+  if (currentIndex < 0) {
+    return
+  }
+
+  const lastIndex = globalStore.done_json.length - 1
+  const targetIndex = type === 'top' ? lastIndex : 0
+
+  if (targetIndex < 0 || targetIndex > lastIndex || targetIndex === currentIndex) {
+    ElMessage.info(type === 'top' ? '已经是最上层了' : '已经是最下层了')
+    return
+  }
+
+  const selectedId = globalStore.selected_items_id[0]
+  const doneJson = [...globalStore.done_json]
+  const [selectedItem] = doneJson.splice(currentIndex, 1)
+  doneJson.splice(targetIndex, 0, selectedItem)
+  globalStore.setGlobalStoreDoneJson(doneJson)
+  globalStore.setSingleSelect(selectedId)
+  cacheStore.addHistory(doneJson)
+}
 const onAlignSelected = (
 const onAlignSelected = (
   type:
   type:
     | 'left'
     | 'left'

+ 43 - 47
src/views/maotu/components/DeviceBindPanel.vue

@@ -5,26 +5,13 @@
         <el-icon><Connection /></el-icon>
         <el-icon><Connection /></el-icon>
       </div>
       </div>
       <div class="device-bind-overview__content">
       <div class="device-bind-overview__content">
-        <div class="device-bind-overview__title">
-          <strong>设备数据绑定</strong>
-          <el-tag size="small" :type="bindCount ? 'primary' : 'info'" effect="light">
-            {{ bindCount ? `${bindCount} 项` : '未绑定' }}
-          </el-tag>
-        </div>
-        <p>
-          {{
-            bindCount
-              ? `已将 ${bindCount} 个设备数据点映射到当前组件`
-              : '将设备实时数据映射到组件属性'
-          }}
-        </p>
-        <div class="device-bind-overview__meta">
-          <span>{{ devices.length }} 台可用设备</span>
-          <span>{{ nodePropOptions.length }} 个可绑定属性</span>
-        </div>
+        <strong>设备绑定</strong>
+        <span class="device-bind-overview__status" :class="{ 'is-active': bindCount }">
+          {{ bindCount ? `已绑定 ${bindCount} 项` : '尚未绑定' }}
+        </span>
       </div>
       </div>
       <el-button type="primary" plain size="small" @click="openDrawer">
       <el-button type="primary" plain size="small" @click="openDrawer">
-        {{ bindCount ? '管理绑定' : '开始绑定' }}
+        {{ bindCount ? '管理' : '绑定' }}
       </el-button>
       </el-button>
     </div>
     </div>
 
 
@@ -457,7 +444,7 @@ function removeBind(index: number) {
 .device-bind-overview {
 .device-bind-overview {
   display: flex;
   display: flex;
   min-width: 0;
   min-width: 0;
-  padding: 12px;
+  padding: 10px 12px;
   background: linear-gradient(145deg, #f7fbff 0%, #fff 75%);
   background: linear-gradient(145deg, #f7fbff 0%, #fff 75%);
   border: 1px solid var(--el-color-primary-light-7);
   border: 1px solid var(--el-color-primary-light-7);
   border-radius: 10px;
   border-radius: 10px;
@@ -472,59 +459,68 @@ function removeBind(index: number) {
 
 
 .device-bind-overview__icon {
 .device-bind-overview__icon {
   display: inline-flex;
   display: inline-flex;
-  width: 34px;
-  height: 34px;
-  font-size: 18px;
+  width: 32px;
+  height: 32px;
+  font-size: 17px;
   color: var(--el-color-primary);
   color: var(--el-color-primary);
   background: var(--el-color-primary-light-9);
   background: var(--el-color-primary-light-9);
-  border-radius: 9px;
+  border-radius: 8px;
   align-items: center;
   align-items: center;
   justify-content: center;
   justify-content: center;
   flex: none;
   flex: none;
 }
 }
 
 
 .device-bind-overview__content {
 .device-bind-overview__content {
-  min-width: 0;
-  margin-right: auto;
-}
-
-.device-bind-overview__title {
   display: flex;
   display: flex;
   min-width: 0;
   min-width: 0;
-  align-items: center;
-  gap: 6px;
+  margin-right: auto;
+  flex-direction: column;
+  gap: 1px;
 }
 }
 
 
-.device-bind-overview__title strong {
+.device-bind-overview__content strong {
   overflow: hidden;
   overflow: hidden;
-  font-size: 12px;
+  font-size: 13px;
+  font-weight: 600;
   line-height: 18px;
   line-height: 18px;
-  color: #344054;
+  color: #25324a;
   text-overflow: ellipsis;
   text-overflow: ellipsis;
   white-space: nowrap;
   white-space: nowrap;
 }
 }
 
 
-.device-bind-overview p {
-  margin: 3px 0;
-  overflow: hidden;
+.device-bind-overview__status {
+  position: relative;
+  padding-left: 10px;
   font-size: 10px;
   font-size: 10px;
-  line-height: 15px;
-  color: #7d899b;
-  text-overflow: ellipsis;
-  white-space: nowrap;
+  line-height: 16px;
+  color: #8a95a6;
 }
 }
 
 
-.device-bind-overview__meta {
-  display: flex;
-  font-size: 10px;
-  line-height: 15px;
-  color: #98a2b3;
-  gap: 8px;
+.device-bind-overview__status::before {
+  position: absolute;
+  top: 50%;
+  left: 0;
+  width: 5px;
+  height: 5px;
+  background: #c3cad5;
+  border-radius: 50%;
+  content: '';
+  transform: translateY(-50%);
+}
+
+.device-bind-overview__status.is-active {
+  color: var(--el-color-primary);
+}
+
+.device-bind-overview__status.is-active::before {
+  background: var(--el-color-primary);
 }
 }
 
 
 .device-bind-overview > :deep(.el-button) {
 .device-bind-overview > :deep(.el-button) {
-  padding: 5px 8px;
+  min-width: 50px;
+  padding: 5px 10px;
   margin: 0;
   margin: 0;
+  border-radius: 7px;
   flex: none;
   flex: none;
 }
 }