Quellcode durchsuchen

feat: 添加事件编辑器

jiaxing.liao vor 3 Monaten
Ursprung
Commit
0bd898b74e

+ 3 - 1
electron.vite.config.ts

@@ -6,6 +6,7 @@ import AutoImport from 'unplugin-auto-import/vite'
 import Components from 'unplugin-vue-components/vite'
 import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
 import UnoCSS from 'unocss/vite'
+import vueJsx from '@vitejs/plugin-vue-jsx'
 
 export default defineConfig({
   main: {
@@ -36,7 +37,8 @@ export default defineConfig({
       Components({
         resolvers: [ElementPlusResolver()]
       }),
-      UnoCSS()
+      UnoCSS(),
+      vueJsx()
     ]
   }
 })

+ 2 - 0
package.json

@@ -36,6 +36,7 @@
     "normalize.css": "^8.0.1",
     "pinia": "^3.0.3",
     "reka-ui": "^2.6.0",
+    "simple-mind-map": "0.14.0-fix.1",
     "uuid": "^13.0.0",
     "vue-hooks-plus": "^2.4.1",
     "vue-i18n": "^11.1.12",
@@ -48,6 +49,7 @@
     "@electron-toolkit/tsconfig": "^2.0.0",
     "@types/node": "^22.18.6",
     "@vitejs/plugin-vue": "^6.0.1",
+    "@vitejs/plugin-vue-jsx": "^5.1.2",
     "electron": "^38.1.2",
     "electron-builder": "^25.1.8",
     "electron-vite": "^4.0.1",

Datei-Diff unterdrückt, da er zu groß ist
+ 940 - 8
pnpm-lock.yaml


+ 65 - 0
src/renderer/components.d.ts

@@ -5,6 +5,7 @@
 // ------
 // Generated by unplugin-vue-components
 // Read more: https://github.com/vuejs/core/pull/3399
+import { GlobalComponents } from 'vue'
 
 export {}
 
@@ -76,3 +77,67 @@ declare module 'vue' {
     vLoading: typeof import('element-plus/es')['ElLoadingDirective']
   }
 }
+
+// For TSX support
+declare global {
+  const CodeEditor: typeof import('./src/components/CodeEditor/index.vue')['default']
+  const ElAutocomplete: typeof import('element-plus/es')['ElAutocomplete']
+  const ElAutoComplete: typeof import('element-plus/es')['ElAutoComplete']
+  const ElButton: typeof import('element-plus/es')['ElButton']
+  const ElCard: typeof import('element-plus/es')['ElCard']
+  const ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
+  const ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
+  const ElCol: typeof import('element-plus/es')['ElCol']
+  const ElCollapse: typeof import('element-plus/es')['ElCollapse']
+  const ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
+  const ElColorPicker: typeof import('element-plus/es')['ElColorPicker']
+  const ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
+  const ElContainer: typeof import('element-plus/es')['ElContainer']
+  const ElDescriptions: typeof import('element-plus/es')['ElDescriptions']
+  const ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem']
+  const ElDialog: typeof import('element-plus/es')['ElDialog']
+  const ElDivider: typeof import('element-plus/es')['ElDivider']
+  const ElDropdown: typeof import('element-plus/es')['ElDropdown']
+  const ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
+  const ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
+  const ElEmpty: typeof import('element-plus/es')['ElEmpty']
+  const ElFlex: typeof import('element-plus/es')['ElFlex']
+  const ElForm: typeof import('element-plus/es')['ElForm']
+  const ElFormItem: typeof import('element-plus/es')['ElFormItem']
+  const ElFromItem: typeof import('element-plus/es')['ElFromItem']
+  const ElHeader: typeof import('element-plus/es')['ElHeader']
+  const ElIcon: typeof import('element-plus/es')['ElIcon']
+  const ElImage: typeof import('element-plus/es')['ElImage']
+  const ElInput: typeof import('element-plus/es')['ElInput']
+  const ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
+  const ElMain: typeof import('element-plus/es')['ElMain']
+  const ElMenu: typeof import('element-plus/es')['ElMenu']
+  const ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
+  const ElOption: typeof import('element-plus/es')['ElOption']
+  const ElPopconfirm: typeof import('element-plus/es')['ElPopconfirm']
+  const ElPopover: typeof import('element-plus/es')['ElPopover']
+  const ElRadio: typeof import('element-plus/es')['ElRadio']
+  const ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
+  const ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
+  const ElRow: typeof import('element-plus/es')['ElRow']
+  const ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
+  const ElSelect: typeof import('element-plus/es')['ElSelect']
+  const ElSelectV2: typeof import('element-plus/es')['ElSelectV2']
+  const ElSlider: typeof import('element-plus/es')['ElSlider']
+  const ElSpace: typeof import('element-plus/es')['ElSpace']
+  const ElSplitter: typeof import('element-plus/es')['ElSplitter']
+  const ElSplitterPanel: typeof import('element-plus/es')['ElSplitterPanel']
+  const ElTabPane: typeof import('element-plus/es')['ElTabPane']
+  const ElTabPanel: typeof import('element-plus/es')['ElTabPanel']
+  const ElTabs: typeof import('element-plus/es')['ElTabs']
+  const ElTooltip: typeof import('element-plus/es')['ElTooltip']
+  const ElTree: typeof import('element-plus/es')['ElTree']
+  const LocalImage: typeof import('./src/components/LocalImage/index.vue')['default']
+  const MonacoEditor: typeof import('./src/components/MonacoEditor/index.vue')['default']
+  const PanelTitle: typeof import('./src/components/PanelTitle/index.vue')['default']
+  const RouterLink: typeof import('vue-router')['RouterLink']
+  const RouterView: typeof import('vue-router')['RouterView']
+  const SplitterCollapse: typeof import('./src/components/SplitterCollapse/index.vue')['default']
+  const SplitterCollapseItem: typeof import('./src/components/SplitterCollapse/SplitterCollapseItem.vue')['default']
+  const ViewTitle: typeof import('./src/components/ViewTitle/index.vue')['default']
+}

+ 4 - 1
src/renderer/src/store/modules/app.ts

@@ -29,6 +29,8 @@ export const useAppStore = defineStore('app', () => {
   const loading = ref(false)
   // 联调工具路径
   const sunmicroPath = useLocalStorage('sunmicroPath', '')
+  // 底部工具
+  const showComposite = ref(true)
 
   const { locale } = useI18n()
 
@@ -65,6 +67,7 @@ export const useAppStore = defineStore('app', () => {
     toggleLayout,
     showGeneralModal,
     setTheme,
-    sunmicroPath
+    sunmicroPath,
+    showComposite
   }
 })

+ 7 - 1
src/renderer/src/store/modules/project.ts

@@ -42,6 +42,7 @@ export const useProjectStore = defineStore('project', () => {
   // 项目信息
   const project = ref<IProject>()
 
+  // 历史记录
   const { history, undo, redo, canRedo, canUndo, clear } = useDebouncedRefHistory(project, {
     debounce: 300,
     deep: true,
@@ -65,6 +66,9 @@ export const useProjectStore = defineStore('project', () => {
     return pages?.flat().find((page) => page.id === activePageId.value)
   })
 
+  // 当前选中元素
+  const activeWidgets = ref<any[]>([])
+
   /**
    * 创建应用
    * @param meta 应用元信息
@@ -98,7 +102,7 @@ export const useProjectStore = defineStore('project', () => {
       project.value?.screens.push(newScreen)
     })
     activePageId.value = project.value.screens[0].pages[0].id
-
+    activeWidgets.value = [project.value.screens[0].pages[0]]
     // 3、创建BIN
     if (meta.resourcePackaging === 'c_bin' && meta.binNum > 0) {
       for (let i = 0; i < meta.binNum; i++) {
@@ -163,6 +167,7 @@ export const useProjectStore = defineStore('project', () => {
     clear()
     projectPath.value = path
     activePageId.value = newProject.screens[0].pages?.[0]?.id
+    activeWidgets.value = [newProject.screens[0].pages?.[0]]
     imageCompressFormat.value = newProject.meta.imageCompress
     recentProjectStore.addProject({
       id: v4(),
@@ -261,6 +266,7 @@ export const useProjectStore = defineStore('project', () => {
     loadProject,
     openLocalProject,
     saveProject,
+    activeWidgets,
 
     // 历史记录
     history,

+ 23 - 3
src/renderer/src/style.less

@@ -13,13 +13,33 @@ body {
 
 
 .el-tabs {
-  --el-tabs-header-height: 28px !important;
-  .el-tabs__active-bar {
-    background-color: var(--text-active);
+  --el-tabs-header-height: 32px !important;
+  &.el-tabs--border-card .el-tabs__item.is-active, .el-tabs__item:hover {
+    color: var(--text-active) !important;
   }
   .el-tabs__item.is-active, .el-tabs__item:hover {
     color: var(--text-active);
   }
+  .el-tabs__nav-next, .el-tabs__nav-prev {
+    line-height: 32px;
+  }
+  .el-tabs__active-bar {
+    background-color: transparent;
+    &::after {
+      content: "";
+      display: block;
+      height: 2px;
+      width: 16px;
+      background: var(--text-secondary);
+      position: absolute;
+      left: 50%;
+      bottom: 0;
+      transform: translateX(-50%);
+    }
+  }
+  .el-tabs__nav-wrap:after {
+    height: 1px;
+  }
 }
 
 .el-collapse {

+ 1 - 0
src/renderer/src/types/baseWidget.d.ts

@@ -21,4 +21,5 @@ export type BaseWidget = {
   events: WidgetEvent[]
   // 子控件
   children: BaseWidget[]
+  [key: string]: any
 }

+ 7 - 0
src/renderer/src/views/designer/workspace/composite/Log.vue

@@ -0,0 +1,7 @@
+<template>
+  <div class="p-12px text-10px">日志输出</div>
+</template>
+
+<script setup lang="ts"></script>
+
+<style scoped></style>

+ 110 - 0
src/renderer/src/views/designer/workspace/composite/eventEdit/NodeItem.vue

@@ -0,0 +1,110 @@
+<template>
+  <!-- 添加节点 -->
+  <SelectPopover
+    v-if="node.nodeType === 'add'"
+    v-bind="addOptions"
+    @confirm="(val) => val && emit('add', node.parentId!, node.parentType!, val)"
+  >
+    <div
+      v-if="node.nodeType === 'add'"
+      :style="addBtnStyle"
+      class="w-120px h-24px bg-#0068ff text-#fff grid place-items-center rounded-4px hover:box-shadow-md cursor-pointer"
+    >
+      <LuPlus />
+    </div>
+  </SelectPopover>
+
+  <!-- 主节点 -->
+  <div
+    v-if="node.nodeType === 'root'"
+    class="w-200px h-40px bg-#0068ff text-#fff grid place-items-center rounded-4px"
+  >
+    {{ node.name }}
+  </div>
+
+  <!-- 事件节点 -->
+  <div
+    v-if="node.nodeType === 'event'"
+    class="w-200px h-40px bg-#4b85e2 text-#fff grid place-items-center rounded-4px"
+  >
+    {{ node.name }}
+  </div>
+
+  <!-- 目标节点 -->
+  <div
+    v-if="node.nodeType === 'target'"
+    class="w-200px h-40px bg-#4B9889 text-#fff grid place-items-center rounded-4px"
+  >
+    {{ node.name }}
+  </div>
+
+  <!-- 动作节点 -->
+  <div
+    v-if="node.nodeType === 'action'"
+    class="w-200px h-40px bg-#4EB2BF text-#fff grid place-items-center rounded-4px"
+  >
+    {{ node.name }}
+  </div>
+</template>
+
+<script setup lang="ts">
+import type { NodeItemType } from './type'
+
+import { LuPlus } from 'vue-icons-plus/lu'
+import SelectPopover from './SelectPopover.vue'
+import { computed } from 'vue'
+import { widgetEventOptions, pageEventOptions } from './config'
+
+const emit = defineEmits<{
+  (e: 'add', parentId: string, parentType: string, events: string[]): void
+  (e: 'delete', node: NodeItemType): void
+}>()
+const props = defineProps<{
+  node: NodeItemType
+}>()
+
+// 添加节点弹窗配置
+const addOptions = computed(() => {
+  const node = props.node
+  if (node.nodeType === 'add') {
+    switch (node.parentType) {
+      case 'root':
+        return {
+          title: '触发方式',
+          multiple: true,
+          options: widgetEventOptions
+        }
+      case 'event':
+        return {
+          title: '目标',
+          multiple: true,
+          options: [
+            { label: '事件', value: 'event' },
+            { label: '条件', value: 'condition' }
+          ]
+        }
+      case 'target':
+        return {
+          title: '动作',
+          multiple: true,
+          options: [
+            { label: '事件', value: 'event' },
+            { label: '条件', value: 'condition' }
+          ]
+        }
+    }
+  }
+
+  return
+})
+
+const addBtnStyle = computed(() => {
+  const node = props.node
+  return {
+    backgroundColor:
+      node.parentType === 'root' ? '#4b85e2' : node.parentType === 'event' ? '#4B9889' : '#4EB2BF'
+  }
+})
+</script>
+
+<style scoped></style>

+ 100 - 0
src/renderer/src/views/designer/workspace/composite/eventEdit/SelectPopover.vue

@@ -0,0 +1,100 @@
+<template>
+  <el-popover
+    append-to="body"
+    trigger="click"
+    popper-style="width: 440px;"
+    ref="popoverRef"
+    @hide="handleCancel"
+  >
+    <template #reference>
+      <slot></slot>
+    </template>
+    <div>
+      <div class="h-32px leading-32px pl-12px bg-bg-tertiary flex justify-between items-center">
+        <span>{{ title }}</span>
+        <el-input v-model="search" style="width: 200px" placeholder="search..." size="small" />
+      </div>
+      <div class="my-12px flex flex-wrap gap-x-12px gap-y-8px">
+        <div
+          style="width: calc(50% - 6px)"
+          class="shrink-0"
+          v-for="item in getOptions || []"
+          :key="item.value"
+        >
+          <div
+            class="w-full h-20px border-dashed border-1px border-border grid place-items-center cursor-pointer hover:bg-bg-primary"
+            :class="
+              selectedList.includes(item.value)
+                ? 'bg-accent-blue hover:bg-accent-blue! text-white'
+                : ''
+            "
+            @click="handleSelect(item.value)"
+          >
+            {{ item.label }}
+          </div>
+        </div>
+        <div v-if="!getOptions?.length" class="w-full h-120px grid place-items-center">无数据~</div>
+      </div>
+      <div class="w-full flex justify-end">
+        <el-button size="small" @click="handleCancel">取消</el-button>
+        <el-button size="small" type="primary" @click="handleConfirm">确定</el-button>
+      </div>
+    </div>
+  </el-popover>
+</template>
+
+<script setup lang="ts">
+import type { PopoverInstance } from 'element-plus'
+import { computed, ref } from 'vue'
+
+const emit = defineEmits<{
+  (e: 'confirm', value: string[]): void
+}>()
+const props = defineProps<{
+  title?: string
+  multiple?: boolean
+  options?: { label: string; value: string }[]
+  selected?: string | string[]
+}>()
+
+const selectedList = ref<string[]>([])
+const search = ref('')
+const popoverRef = ref<PopoverInstance>()
+// 搜索
+const getOptions = computed(() => {
+  if (!search.value) {
+    return props.options
+  }
+  return props.options?.filter((item) => item.label.includes(search.value))
+})
+
+// 选中处理
+const handleSelect = (value: string) => {
+  if (selectedList.value.includes(value)) {
+    selectedList.value = selectedList.value.filter((item) => item !== value)
+    return
+  }
+  if (props.multiple) {
+    selectedList.value = [...selectedList.value, value]
+  } else {
+    selectedList.value = [value]
+  }
+}
+
+// 取消
+const handleCancel = () => {
+  popoverRef.value?.hide()
+  selectedList.value = []
+}
+
+// 确定
+const handleConfirm = () => {
+  const result = selectedList.value
+  handleCancel()
+  setTimeout(() => {
+    emit('confirm', result)
+  }, 500)
+}
+</script>
+
+<style lang="less" scoped></style>

+ 50 - 0
src/renderer/src/views/designer/workspace/composite/eventEdit/config.ts

@@ -0,0 +1,50 @@
+/**
+ * @description: 组件事件选项
+ */
+export const widgetEventOptions = [
+  { label: 'Clicked', value: 'LV_EVENT_SINGLE_CLICKED' },
+  { label: 'Short Clicked', value: 'LV_EVENT_SHORT_CLICKED' },
+  { label: 'Key', value: 'LV_EVENT_KEY' },
+  { label: 'Pressed', value: 'LV_EVENT_PRESSED' },
+  { label: 'Pressing', value: 'LV_EVENT_PRESSING' },
+  { label: 'Press Lost', value: 'LV_EVENT_PRESS_LOST' },
+  { label: 'Long Pressed', value: 'LV_EVENT_LONG_PRESSED' },
+  { label: 'Long Pressed Repeat', value: 'LV_EVENT_LONG_PRESSED_REPEAT' },
+  { label: 'Released', value: 'LV_EVENT_RELEASED' },
+  { label: 'Value Changed', value: 'LV_EVENT_VALUE_CHANGED' },
+  { label: 'Scroll', value: 'LV_EVENT_SCROLL' },
+  { label: 'Scroll Begin', value: 'LV_EVENT_SCROLL_BEGIN' },
+  { label: 'Scroll End', value: 'LV_EVENT_SCROLL_END' },
+  { label: 'Focused', value: 'LV_EVENT_FOCUSED' },
+  { label: 'Defocused', value: 'LV_EVENT_DEFOCUSED' },
+  { label: 'Leave', value: 'LV_EVENT_LEAVE' },
+  { label: 'Hit Test', value: 'LV_EVENT_HIT_TEST' }
+]
+
+/**
+ * 页面事件选项
+ */
+export const pageEventOptions = [
+  { label: 'Clicked', value: 'LV_EVENT_SINGLE_CLICKED' },
+  { label: 'Short Clicked', value: 'LV_EVENT_SHORT_CLICKED' },
+  { label: 'Key', value: 'LV_EVENT_KEY' },
+  { label: 'Pressed', value: 'LV_EVENT_PRESSED' },
+  { label: 'Pressing', value: 'LV_EVENT_PRESSING' },
+  { label: 'Press Lost', value: 'LV_EVENT_PRESS_LOST' },
+  { label: 'Long Pressed', value: 'LV_EVENT_LONG_PRESSED' },
+  { label: 'Long Pressed Repeat', value: 'LV_EVENT_LONG_PRESSED_REPEAT' },
+  { label: 'Released', value: 'LV_EVENT_RELEASED' },
+  { label: 'Value Changed', value: 'LV_EVENT_VALUE_CHANGED' },
+  { label: 'Scroll', value: 'LV_EVENT_SCROLL' },
+  { label: 'Scroll Begin', value: 'LV_EVENT_SCROLL_BEGIN' },
+  { label: 'Scroll End', value: 'LV_EVENT_SCROLL_END' },
+  { label: 'Focused', value: 'LV_EVENT_FOCUSED' },
+  { label: 'Defocused', value: 'LV_EVENT_DEFOCUSED' },
+  { label: 'Leave', value: 'LV_EVENT_LEAVE' },
+  { label: 'Hit Test', value: 'LV_EVENT_HIT_TEST' },
+  { label: 'Unload Start', value: 'LV_EVENT_SCREEN_UNLOAD_START' },
+  { label: 'Load Start', value: 'LV_EVENT_SCREEN_LOAD_START' },
+  { label: 'Loaded', value: 'LV_EVENT_SCREEN_LOADED' },
+  { label: 'Unloaded', value: 'LV_EVENT_SCREEN_UNLOADED' },
+  { label: 'Created', value: 'LV_EVENT_CHILD_CREATED' }
+]

+ 163 - 0
src/renderer/src/views/designer/workspace/composite/eventEdit/index.vue

@@ -0,0 +1,163 @@
+<template>
+  <div class="w-full h-full" ref="containerRef"></div>
+</template>
+
+<script setup lang="tsx">
+import { ref, defineComponent, createApp, watch, nextTick } from 'vue'
+import MindMap from 'simple-mind-map'
+import { useElementSize, watchOnce } from '@vueuse/core'
+import defaultTheme from './theme'
+import { useResizeObserver } from '@vueuse/core'
+import NodeItem from './NodeItem.vue'
+import { useProjectStore } from '@/store/modules/project'
+import { bfsWalk } from 'simple-mind-map/src/utils'
+
+import type { NodeItemType } from './type'
+import { v4 } from 'uuid'
+
+const containerRef = ref<HTMLElement | null>(null)
+const { width, height } = useElementSize(containerRef)
+const mindMap = ref<MindMap | null>(null)
+const projectStore = useProjectStore()
+
+// 监听容器尺寸变化
+useResizeObserver(containerRef, () => {
+  if (mindMap.value && width.value && height.value) {
+    mindMap.value.resize()
+  }
+})
+
+const mindMapData = ref<NodeItemType>()
+
+watch(
+  () => projectStore.activeWidgets,
+  async (list) => {
+    const widget = list?.at(-1)
+    // 当前选择的最后一个元素
+    if (widget) {
+      const data: NodeItemType = {
+        id: widget.id,
+        name: widget.name,
+        nodeType: 'root',
+        children: widget.events,
+        config: null
+      }
+      // 添加增加节点
+      bfsWalk(data, (child) => {
+        if (child.children) {
+          child.children.push(getAddNode(child.id, child.nodeType))
+        }
+      })
+      mindMapData.value = data
+      await nextTick()
+      mindMap.value?.setData(mindMapData.value)
+    }
+  }
+)
+
+// 刷新节点
+const refresh = () => {
+  mindMap.value?.setData(mindMapData.value)
+}
+
+// 获取添加节点
+const getAddNode = (parentId: string, parentType: NodeItemType['parentType']): NodeItemType => {
+  return {
+    id: v4(),
+    name: 'add_btn',
+    nodeType: 'add',
+    parentId,
+    parentType
+  }
+}
+
+// 添加节点
+const handleAddNode = (parentId: string, parentType: string, keys: string[]) => {
+  const nodes: NodeItemType[] = []
+  switch (parentType) {
+    case 'root':
+      keys.forEach((key) => {
+        const id = v4()
+        nodes.push({
+          id,
+          name: key,
+          nodeType: 'event',
+          config: {},
+          children: [getAddNode(id, 'event')]
+        })
+      })
+      break
+    case 'event':
+      keys.forEach((key) => {
+        const id = v4()
+        nodes.push({
+          id,
+          name: key,
+          nodeType: 'target',
+          config: {},
+          children: [getAddNode(id, 'target')]
+        })
+      })
+      break
+    case 'target':
+      keys.forEach((key) => {
+        const id = v4()
+        nodes.push({
+          id,
+          name: key,
+          nodeType: 'action',
+          config: {},
+          children: []
+        })
+      })
+      break
+  }
+  bfsWalk(mindMapData.value, (node) => {
+    if (parentId === node.id) {
+      // 往倒数第二个位置插入
+      node.children.splice(node.children.length - 1, 0, ...nodes)
+    }
+  })
+  // 刷新画布
+  refresh()
+}
+
+watchOnce(
+  () => [width.value, height.value],
+  ([w, h]) => {
+    if (w && h) {
+      // 初始化思维导图
+      // 配置参考:https://wanglin2.github.io/mind-map-docs/api/constructor/constructor-options.html
+      mindMap.value = new MindMap({
+        el: containerRef.value,
+        themeConfig: defaultTheme,
+        initRootNodePosition: ['5%', 'center'],
+        fitPadding: 12,
+        data: mindMapData.value,
+        readonly: true,
+        isUseCustomNodeContent: true,
+        customCreateNodeContent: (node: any) => {
+          const sourceData = node.nodeData
+          const App = defineComponent({
+            render() {
+              return <NodeItem node={sourceData as NodeItemType} onAdd={handleAddNode} />
+            }
+          })
+
+          // return你的自定义DOM节点
+          let div = document.createElement('div')
+          const app = createApp(App)
+          div.style = 'user-select: none;'
+          app.mount(div)
+
+          return div
+        }
+      } as any)
+      // 监听数据变化
+      mindMap.value.on('data_change_detail', (data: any) => {
+        console.log('date change detail', data)
+      })
+    }
+  }
+)
+</script>

+ 254 - 0
src/renderer/src/views/designer/workspace/composite/eventEdit/theme.ts

@@ -0,0 +1,254 @@
+//  默认主题
+export default {
+  // 节点内边距
+  paddingX: 15,
+  paddingY: 5,
+  // 图片显示的最大宽度
+  imgMaxWidth: 200,
+  // 图片显示的最大高度
+  imgMaxHeight: 100,
+  // icon的大小
+  iconSize: 20,
+  // 连线的粗细
+  lineWidth: 1,
+  // 连线的颜色
+  lineColor: '#3e9af8',
+  // 连线样式
+  lineDasharray: 'none',
+  // 连线是否开启流动效果,仅在虚线时有效(需要注册LineFlow插件)
+  lineFlow: false,
+  // 流动效果一个周期的时间,单位:s
+  lineFlowDuration: 1,
+  // 流动方向是否是从父节点到子节点
+  lineFlowForward: true,
+  // 连线风格
+  lineStyle: 'straight', // 曲线(curve)【仅支持logicalStructure、mindMap、verticalTimeline三种结构】、直线(straight)、直连(direct)【仅支持logicalStructure、mindMap、organizationStructure、verticalTimeline四种结构】
+  // 曲线连接时,根节点和其他节点的连接线样式保持统一,默认根节点为 ( 型,其他节点为 { 型,设为true后,都为 { 型。仅支持logicalStructure、mindMap两种结构
+  rootLineKeepSameInCurve: true,
+  // 曲线连接时,根节点和其他节点的连线起始位置保持统一,默认根节点的连线起始位置在节点中心,其他节点在节点右侧(或左侧),如果该配置设为true,那么根节点的连线起始位置也会在节点右侧(或左侧)
+  rootLineStartPositionKeepSameInCurve: false,
+  // 直线连接(straight)时,连线的圆角大小,设置为0代表没有圆角,仅支持logicalStructure、mindMap、verticalTimeline三种结构
+  lineRadius: 5,
+  // 连线是否显示标记,目前只支持箭头
+  showLineMarker: false,
+  // 概要连线的粗细
+  generalizationLineWidth: 1,
+  // 概要连线的颜色
+  generalizationLineColor: '#549688',
+  // 概要曲线距节点的距离
+  generalizationLineMargin: 0,
+  // 概要节点距节点的距离
+  generalizationNodeMargin: 20,
+  // 关联线默认状态的粗细
+  associativeLineWidth: 2,
+  // 关联线默认状态的颜色
+  associativeLineColor: 'rgb(51, 51, 51)',
+  // 关联线激活状态的粗细
+  associativeLineActiveWidth: 8,
+  // 关联线激活状态的颜色
+  associativeLineActiveColor: 'rgba(2, 167, 240, 1)',
+  // 关联线样式
+  associativeLineDasharray: '6,4',
+  // 关联线文字颜色
+  associativeLineTextColor: 'rgb(51, 51, 51)',
+  // 关联线文字大小
+  associativeLineTextFontSize: 14,
+  // 关联线文字行高
+  associativeLineTextLineHeight: 1.2,
+  // 关联线文字字体
+  associativeLineTextFontFamily: '微软雅黑, Microsoft YaHei',
+  // 背景颜色
+  backgroundColor: 'transparent',
+  // 背景图片
+  backgroundImage: 'none',
+  // 背景重复
+  backgroundRepeat: 'no-repeat',
+  // 设置背景图像的起始位置
+  backgroundPosition: 'center center',
+  // 设置背景图片大小
+  backgroundSize: 'cover',
+  // 节点使用只有底边横线的样式,仅支持logicalStructure、mindMap、catalogOrganization、organizationStructure四种结构
+  nodeUseLineStyle: false,
+  // 根节点样式
+  root: {
+    shape: 'rectangle',
+    fillColor: 'transparent',
+    fontFamily: '微软雅黑, Microsoft YaHei',
+    color: '#fff',
+    fontSize: 16,
+    fontWeight: 'bold',
+    fontStyle: 'normal',
+    borderColor: 'transparent',
+    borderWidth: 0,
+    borderDasharray: 'none',
+    borderRadius: 0,
+    textDecoration: 'none',
+    gradientStyle: false,
+    startColor: '#549688',
+    endColor: '#fff',
+    startDir: [0, 0],
+    endDir: [1, 0],
+    // 连线标记的位置,start(头部)、end(尾部),该配置在showLineMarker配置为true时生效
+    lineMarkerDir: 'end',
+    // 节点鼠标hover和激活时显示的矩形边框的颜色,主题里不设置,默认会取hoverRectColor实例化选项的值
+    hoverRectColor: '',
+    // 点鼠标hover和激活时显示的矩形边框的圆角大小
+    hoverRectRadius: 5,
+    // 文本对齐
+    textAlign: 'left', // right、center、justify、left
+    // 图片放置位置,相对于整个文本内容
+    imgPlacement: 'top', // left、right、bottom、top
+    // 标签放置位置
+    tagPlacement: 'right' // right(文字右侧)、bottom(文本内容下方)
+    // 下列样式也支持给节点设置,用于覆盖最外层的设置
+    // paddingX,
+    // paddingY,
+    // lineWidth,
+    // lineColor,
+    // lineDasharray,
+    // lineFlow,
+    // lineFlowDuration,
+    // lineFlowForward
+    // 关联线的所有样式
+  },
+  // 二级节点样式
+  second: {
+    shape: 'rectangle',
+    marginX: 100,
+    marginY: 40,
+    fillColor: 'transparent',
+    fontFamily: '微软雅黑, Microsoft YaHei',
+    color: '#565656',
+    fontSize: 16,
+    fontWeight: 'normal',
+    fontStyle: 'normal',
+    borderColor: 'transparent',
+    borderWidth: 1,
+    borderDasharray: 'none',
+    borderRadius: 0,
+    textDecoration: 'none',
+    gradientStyle: false,
+    startColor: '#549688',
+    endColor: '#fff',
+    startDir: [0, 0],
+    endDir: [1, 0],
+    lineMarkerDir: 'end',
+    hoverRectColor: '',
+    hoverRectRadius: 5,
+    textAlign: 'left',
+    imgPlacement: 'top',
+    tagPlacement: 'right'
+  },
+  // 三级及以下节点样式
+  node: {
+    shape: 'rectangle',
+    marginX: 50,
+    marginY: 0,
+    fillColor: 'transparent',
+    fontFamily: '微软雅黑, Microsoft YaHei',
+    color: '#6a6d6c',
+    fontSize: 14,
+    fontWeight: 'normal',
+    fontStyle: 'normal',
+    borderColor: 'transparent',
+    borderWidth: 0,
+    borderRadius: 0,
+    borderDasharray: 'none',
+    textDecoration: 'none',
+    gradientStyle: false,
+    startColor: '#549688',
+    endColor: '#fff',
+    startDir: [0, 0],
+    endDir: [1, 0],
+    lineMarkerDir: 'end',
+    hoverRectColor: '',
+    hoverRectRadius: 5,
+    textAlign: 'left',
+    imgPlacement: 'top',
+    tagPlacement: 'right'
+  },
+  // 概要节点样式
+  generalization: {
+    shape: 'rectangle',
+    marginX: 100,
+    marginY: 40,
+    fillColor: '#fff',
+    fontFamily: '微软雅黑, Microsoft YaHei',
+    color: '#565656',
+    fontSize: 16,
+    fontWeight: 'normal',
+    fontStyle: 'normal',
+    borderColor: '#549688',
+    borderWidth: 1,
+    borderDasharray: 'none',
+    borderRadius: 5,
+    textDecoration: 'none',
+    gradientStyle: false,
+    startColor: '#549688',
+    endColor: '#fff',
+    startDir: [0, 0],
+    endDir: [1, 0],
+    hoverRectColor: '',
+    hoverRectRadius: 5,
+    textAlign: 'left',
+    imgPlacement: 'top',
+    tagPlacement: 'right'
+  }
+}
+
+// 检测主题配置是否是节点大小无关的
+const nodeSizeIndependenceList = [
+  'lineWidth',
+  'lineColor',
+  'lineDasharray',
+  'lineStyle',
+  'generalizationLineWidth',
+  'generalizationLineColor',
+  'associativeLineWidth',
+  'associativeLineColor',
+  'associativeLineActiveWidth',
+  'associativeLineActiveColor',
+  'associativeLineTextColor',
+  'associativeLineTextFontSize',
+  'associativeLineTextLineHeight',
+  'associativeLineTextFontFamily',
+  'backgroundColor',
+  'backgroundImage',
+  'backgroundRepeat',
+  'backgroundPosition',
+  'backgroundSize',
+  'rootLineKeepSameInCurve',
+  'rootLineStartPositionKeepSameInCurve',
+  'showLineMarker',
+  'lineRadius',
+  'hoverRectColor',
+  'hoverRectRadius',
+  'lineFlow',
+  'lineFlowDuration',
+  'lineFlowForward',
+  'textAlign'
+]
+export const checkIsNodeSizeIndependenceConfig = (config) => {
+  let keys = Object.keys(config)
+  for (let i = 0; i < keys.length; i++) {
+    if (
+      !nodeSizeIndependenceList.find((item) => {
+        return item === keys[i]
+      })
+    ) {
+      return false
+    }
+  }
+  return true
+}
+
+// 连线的样式
+export const lineStyleProps = [
+  'lineColor',
+  'lineDasharray',
+  'lineWidth',
+  'lineMarkerDir',
+  'lineFlow',
+  'lineFlowDuration',
+  'lineFlowForward'
+]

+ 34 - 0
src/renderer/src/views/designer/workspace/composite/eventEdit/type.d.ts

@@ -0,0 +1,34 @@
+export type NodeItemType = {
+  /**
+   * @description: 节点id
+   */
+  id: string
+  /**
+   * @description: 节点名称
+   */
+  name: string
+  /**
+   * @description: 节点类型
+   */
+  nodeType: 'root' | 'event' | 'target' | 'action' | 'add'
+  /**
+   * @description: 节点配置
+   */
+  config?: any
+  /**
+   * @description: 节点子节点
+   */
+  children?: NodeItemType[]
+  /**
+   * @description: 节点父节点
+   */
+  parentId?: string
+  /**
+   * @description: 父节点类型
+   */
+  parentType?: 'root' | 'event' | 'target'
+  /**
+   * @description: 是否页面
+   */
+  isPage?: boolean
+}

+ 34 - 0
src/renderer/src/views/designer/workspace/composite/index.vue

@@ -0,0 +1,34 @@
+<template>
+  <el-tabs v-model="active" class="w-full h-full">
+    <el-tab-pane label="日志" name="log">
+      <Log />
+    </el-tab-pane>
+    <el-tab-pane label="事件" name="event">
+      <EventEdit />
+    </el-tab-pane>
+  </el-tabs>
+</template>
+
+<script setup lang="ts">
+import { ref } from 'vue'
+import Log from './Log.vue'
+import EventEdit from './eventEdit/index.vue'
+
+const active = ref('log')
+</script>
+
+<style scoped>
+::v-deep(.el-tabs__nav-scroll) {
+  padding-left: 12px;
+}
+::v-deep(.el-tab-pane) {
+  height: 100%;
+  width: 100%;
+}
+::v-deep(.el-tabs__content) {
+  padding: 0;
+}
+::v-deep(.el-tabs__header) {
+  margin-bottom: 0;
+}
+</style>

+ 73 - 11
src/renderer/src/views/designer/workspace/index.vue

@@ -1,32 +1,81 @@
 <template>
   <div class="w-full h-full">
-    <SplitterGroup :direction="appStore.screenLayout">
+    <SplitterGroup direction="vertical">
       <SplitterPanel>
-        <Stage key="1" :data="projectStore.project?.screens[0]" />
+        <el-tabs
+          v-model="active"
+          type="border-card"
+          class="w-full h-full"
+          @tab-remove="handleTabRemove"
+        >
+          <el-tab-pane label="界面设计" name="design" :closable="false">
+            <SplitterGroup :direction="appStore.screenLayout">
+              <SplitterPanel>
+                <Stage key="1" :data="projectStore.project?.screens[0]" />
+              </SplitterPanel>
+              <SplitterResizeHandle
+                class="bg-border"
+                :class="appStore.screenLayout === 'vertical' ? 'h-2px' : 'w-2px'"
+                v-if="projectStore.project?.meta.screenType === 'dual'"
+              />
+              <SplitterPanel v-if="projectStore.project?.meta.screenType === 'dual'">
+                <Stage key="2" :data="projectStore.project?.screens[1]" />
+              </SplitterPanel>
+            </SplitterGroup>
+          </el-tab-pane>
+          <el-tab-pane
+            v-for="item in tabPaneList"
+            :key="item.name"
+            :label="item.label"
+            :name="item.name"
+            closable
+          >
+            <div class="w-full">
+              <component :is="item.component" />
+            </div>
+          </el-tab-pane>
+        </el-tabs>
       </SplitterPanel>
-      <SplitterResizeHandle
-        class="bg-border"
-        :class="appStore.screenLayout === 'vertical' ? 'h-2px' : 'w-2px'"
-        v-if="projectStore.project?.meta.screenType === 'dual'"
-      />
-      <SplitterPanel v-if="projectStore.project?.meta.screenType === 'dual'">
-        <Stage key="2" :data="projectStore.project?.screens[1]" />
+      <SplitterResizeHandle v-show="appStore.showComposite" class="bg-border h-2px" />
+      <SplitterPanel v-show="appStore.showComposite" :default-size="30">
+        <div class="w-full h-full">
+          <Composite />
+        </div>
       </SplitterPanel>
     </SplitterGroup>
   </div>
 </template>
 
 <script setup lang="ts">
+import type { TabPaneName } from 'element-plus'
+
 import { ref, onMounted } from 'vue'
-import Stage from './stage/index.vue'
 import { SplitterGroup, SplitterPanel, SplitterResizeHandle } from 'reka-ui'
 import { useProjectStore } from '@/store/modules/project'
 import { useAppStore } from '@/store/modules/app'
 
+import MonacoEditor from '@/components/MonacoEditor/index.vue'
+import Stage from './stage/index.vue'
+import Composite from './composite/index.vue'
+
 const projectStore = useProjectStore()
 const appStore = useAppStore()
 
 const content = ref('')
+const active = ref<TabPaneName>('design')
+// tab pane列表
+const tabPaneList = ref([
+  {
+    label: 'aa.h',
+    name: '111',
+    component: MonacoEditor
+  },
+  {
+    label: 'bb.h',
+    name: '222',
+    component: MonacoEditor
+  }
+])
 onMounted(() => {
   window.electron.ipcRenderer.send('connect-pipe')
 
@@ -39,6 +88,19 @@ onMounted(() => {
     console.log('pipe connected')
   })
 })
+
+// 关闭tab
+const handleTabRemove = (tabName: TabPaneName) => {
+  tabPaneList.value = tabPaneList.value.filter((item) => item.name !== tabName)
+}
 </script>
 
-<style scoped></style>
+<style scoped>
+::v-deep(.el-tab-pane) {
+  height: 100%;
+  width: 100%;
+}
+::v-deep(.el-tabs__content) {
+  padding: 0;
+}
+</style>

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

@@ -7,7 +7,7 @@
     @mouseup="handleMouseUp"
   >
     <div class="workspace flex flex-col">
-      <div class="h-32px bg-bg-secondary stage-title">
+      <div class="h-32px bg-bg-secondary stage-title border-y-1px border-y-solid border-border">
         <div class="px-12px leading-32px text-text-primary font-bold">屏幕</div>
       </div>
       <div class="workspace-top">
@@ -86,7 +86,7 @@
 import type { StageState } from './type'
 import type { Screen } from '@/types/screen'
 
-import { ref, reactive, defineProps, watch, nextTick, computed } from 'vue'
+import { ref, reactive, watch, nextTick, computed } from 'vue'
 import Scaleplate from './Scaleplate.vue'
 import DesignerCanvas from './DesignerCanvas.vue'
 import { throttle } from 'lodash'

+ 5 - 0
tsconfig.web.json

@@ -9,6 +9,11 @@
   "compilerOptions": {
     "types": ["element-plus/global"],
     "composite": true,
+    "jsxImportSource": "vue",
+    "jsx": "preserve",
+
+    "experimentalDecorators": true,
+
     "baseUrl": ".",
     "paths": {
       "@/*": [