Bläddra i källkod

feat: 添加事件配置

jiaxing.liao 2 veckor sedan
förälder
incheckning
2a6a16032d

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

@@ -23,7 +23,6 @@ declare module 'vue' {
     ElCol: typeof import('element-plus/es')['ElCol']
     ElCollapse: typeof import('element-plus/es')['ElCollapse']
     ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
-    ElColorPicker: typeof import('element-plus/es')['ElColorPicker']
     ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
     ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
     ElDialog: typeof import('element-plus/es')['ElDialog']
@@ -90,7 +89,6 @@ declare global {
   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 ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
   const ElDialog: typeof import('element-plus/es')['ElDialog']

+ 16 - 8
src/renderer/src/lvgl-widgets/button/index.ts

@@ -129,7 +129,8 @@ export default {
             componentProps: {
               span: 12
             },
-            slots: { prefix: 'X' }
+            slots: { prefix: 'X' },
+            canUseEventSet: true
           },
           {
             field: 'props.y',
@@ -137,7 +138,8 @@ export default {
             componentProps: {
               span: 12
             },
-            slots: { prefix: 'Y' }
+            slots: { prefix: 'Y' },
+            canUseEventSet: true
           },
           {
             field: 'props.width',
@@ -145,7 +147,8 @@ export default {
             componentProps: {
               span: 12
             },
-            slots: { prefix: 'W' }
+            slots: { prefix: 'W' },
+            canUseEventSet: true
           },
           {
             field: 'props.height',
@@ -153,7 +156,8 @@ export default {
             componentProps: {
               span: 12
             },
-            slots: { prefix: 'H' }
+            slots: { prefix: 'H' },
+            canUseEventSet: true
           }
         ]
       },
@@ -164,7 +168,8 @@ export default {
         componentProps: {
           options: flagOptions,
           defaultCollapsed: true
-        }
+        },
+        canUseEventSet: true
       },
       {
         label: '状态',
@@ -173,7 +178,8 @@ export default {
         componentProps: {
           options: stateOptions,
           defaultCollapsed: true
-        }
+        },
+        canUseEventSet: true
       }
     ],
     // 核心属性
@@ -186,7 +192,8 @@ export default {
           useSymbol: true,
           rows: 3,
           supportLangues: true
-        }
+        },
+        canUseEventSet: true
       },
       {
         label: '模式',
@@ -200,7 +207,8 @@ export default {
             { label: 'Scroll', value: 'scroll' },
             { label: 'Wrap', value: 'wrap' }
           ]
-        }
+        },
+        canUseEventSet: true
       }
     ],
     // 组件样式

+ 0 - 2
src/renderer/src/lvgl-widgets/index.ts

@@ -103,6 +103,4 @@ const componentMap: { [key: string]: IComponentModelConfig } = ComponentArray.re
   return acc
 }, {})
 
-componentMap.lv_animing = componentMap.lv_animimg
-
 export default componentMap

+ 4 - 0
src/renderer/src/views/designer/config/property/CusFormItem.vue

@@ -118,6 +118,9 @@
         style="width: 100%"
         v-bind="componentProps"
       />
+
+      <!-- 字体样式 -->
+      <FamilySelect v-if="schema.valueType === 'family'" v-model="value" />
     </el-form-item>
 
     <!-- 分组 -->
@@ -247,6 +250,7 @@ import ImageSelect from './components/ImageSelect.vue'
 import SymbolSelect from './components/SymbolSelect.vue'
 import { ColorPicker, MonacoEditor } from '@/components'
 import FileSelect from './components/FileSelect.vue'
+import FamilySelect from './components/FamilySelect.vue'
 
 import StyleBackground from './components/StyleBackground.vue'
 import StyleBorder from './components/StyleBorder.vue'

+ 37 - 0
src/renderer/src/views/designer/config/property/components/FamilySelect.vue

@@ -0,0 +1,37 @@
+<template>
+  <el-select v-model="modelValue">
+    <template #label="{ label }">
+      <div :style="{ fontFamily: `'${label}'` }">{{ label }}</div>
+    </template>
+    <el-option
+      v-for="item in fontOptions"
+      :key="item.value"
+      :value="item.value"
+      :label="item.label"
+    >
+      <div :style="{ fontFamily: `'${item.label}'` }">
+        {{ item.label }}
+      </div>
+    </el-option>
+  </el-select>
+</template>
+
+<script setup lang="ts">
+import { computed } from 'vue'
+import { useProjectStore } from '@/store/modules/project'
+const projectStore = useProjectStore()
+// 字体选项
+const fontOptions = computed(() => {
+  const list = (projectStore.project?.resources.fonts || []).map((font) => {
+    return {
+      label: font.fileName,
+      value: font.id
+    }
+  })
+  return [{ label: 'montserratMedium', value: 'montserratMedium' }, ...list]
+})
+
+const modelValue = defineModel<string>('modelValue')
+</script>
+
+<style scoped></style>

+ 3 - 2
src/renderer/src/views/designer/workspace/composite/eventEdit/ActionNode.vue

@@ -16,8 +16,8 @@
         </el-select>
       </div>
 
-      <div v-else-if="actionSchemas?.length" class="pr-8px w-300px">
-        <el-form :model="formData">
+      <div v-else-if="actionSchemas.length" class="pr-8px w-300px">
+        <el-form :model="formData" label-position="top">
           <CusFormItem
             v-for="item in actionSchemas"
             :key="item.field || item.label"
@@ -47,6 +47,7 @@ import type { NodeItemType } from './type'
 import { computed, ref, watch } from 'vue'
 import { klona } from 'klona'
 import { LuTrash2 } from 'vue-icons-plus/lu'
+
 import { useProjectStore } from '@/store/modules/project'
 import CusFormItem from '@/views/designer/config/property/CusFormItem.vue'
 import { resolveActionDescriptor } from './config'

+ 74 - 10
src/renderer/src/views/designer/workspace/composite/eventEdit/NodeItem.vue

@@ -105,6 +105,9 @@ const props = defineProps<{
 
 const projectStore = useProjectStore()
 
+const getRootChildren = () =>
+  ((projectStore.activeWidget?.events || []) as unknown as NodeItemType[]).filter(Boolean)
+
 const addBtnStyle = computed(() => {
   const node = props.node
   return {
@@ -114,7 +117,7 @@ const addBtnStyle = computed(() => {
 })
 
 const findParentChildren = (parentId: string) => {
-  const rootChildren = (projectStore.activeWidget?.events || []) as unknown as NodeItemType[]
+  const rootChildren = getRootChildren()
 
   if (props.node.parentType === 'root') {
     return rootChildren
@@ -134,6 +137,34 @@ const findParentChildren = (parentId: string) => {
   return []
 }
 
+const findParentNode = (parentId: string) => {
+  for (const eventNode of getRootChildren()) {
+    if (eventNode.id === parentId) return eventNode
+
+    const targetNode = (eventNode.children || []).find((item) => item.id === parentId)
+    if (targetNode) return targetNode
+  }
+
+  return
+}
+
+const findNodeById = (nodeId?: string) => {
+  if (!nodeId) return
+
+  for (const eventNode of getRootChildren()) {
+    if (eventNode.id === nodeId) return eventNode
+
+    for (const targetNode of eventNode.children || []) {
+      if (targetNode.id === nodeId) return targetNode
+
+      const actionNode = (targetNode.children || []).find((item) => item.id === nodeId)
+      if (actionNode) return actionNode
+    }
+  }
+
+  return
+}
+
 const getSiblingValues = (parentType: 'root' | 'event' | 'target') => {
   if (parentType === 'target') return []
 
@@ -150,7 +181,33 @@ const withDisabled = (options: OptionType[], selectedValues: string[]) =>
     disabled: selectedValues.includes(item.value)
   }))
 
-const getAddOptions = (type: string) => {
+const getSelectedActionValues = () => {
+  const parentNode =
+    props.node.nodeType === 'target'
+      ? props.node
+      : props.node.nodeType === 'add' && props.node.parentType === 'target'
+        ? findParentNode(props.node.parentId!)
+        : undefined
+
+  return (parentNode?.children || [])
+    .filter((item) => item.nodeType === 'action')
+    .map((item) => item.config?.data?.actionData?.actionKey || item.value)
+    .filter(Boolean) as string[]
+}
+
+const getTargetDataForActionOptions = () => {
+  if (props.node.nodeType === 'target') {
+    return props.node.config?.data as WidgetEventTargetData | undefined
+  }
+
+  if (props.node.nodeType === 'add' && props.node.parentType === 'target') {
+    return findNodeById(props.node.parentId)?.config?.data as WidgetEventTargetData | undefined
+  }
+
+  return
+}
+
+const getAddOptions = (type: NonNullable<NodeItemType['parentType']>) => {
   const widget = projectStore.activeWidget
 
   switch (type) {
@@ -169,11 +226,14 @@ const getAddOptions = (type: string) => {
       }
 
     case 'target': {
-      const targetData = props.node.config?.data as WidgetEventTargetData | undefined
+      const targetData = getTargetDataForActionOptions()
       return {
         title: '动作',
-        multiple: false,
-        options: getActionOptions(targetData, projectStore.project, projectStore.activePage?.id)
+        multiple: true,
+        options: withDisabled(
+          getActionOptions(targetData, projectStore.project, projectStore.activePage?.id),
+          getSelectedActionValues()
+        )
       }
     }
 
@@ -207,20 +267,24 @@ const handleChangeTarget = (val?: OptionType[]) => {
 
   const targetData = item.data as WidgetEventTargetData
   const builtinActionData = getBuiltinActionData(targetData)
-  const actionOptions = getActionOptions(targetData, projectStore.project, projectStore.activePage?.id)
+  const actionOptions = getActionOptions(
+    targetData,
+    projectStore.project,
+    projectStore.activePage?.id
+  )
   const actionOption = builtinActionData
     ? actionOptions.find((option) => option.value === builtinActionData.actionKey)
     : undefined
 
-  const currentFirstChild = props.node.children?.[0]
+  const currentActionChild = props.node.children?.find((child) => child.nodeType === 'action')
   const nextChildren: NodeItemType[] =
     targetData.kind === 'custom_code'
       ? [
           {
-            id: currentFirstChild?.id || v4(),
+            id: currentActionChild?.id || v4(),
             name: '自定义代码',
             nodeType: 'action',
-            value: currentFirstChild?.value ?? '',
+            value: currentActionChild?.value ?? '',
             config: {
               data: {
                 kind: 'custom_code',
@@ -232,7 +296,7 @@ const handleChangeTarget = (val?: OptionType[]) => {
       : actionOption
         ? [
             {
-              id: currentFirstChild?.id || v4(),
+              id: currentActionChild?.id || v4(),
               name: actionOption.label,
               nodeType: 'action',
               value: klona(actionOption.defaultValue),

+ 56 - 15
src/renderer/src/views/designer/workspace/composite/eventEdit/SelectPopover.vue

@@ -27,23 +27,31 @@
       </div>
 
       <el-scrollbar max-height="360px">
-        <div class="w-full my-12px flex flex-wrap gap-x-12px gap-y-8px">
-          <div
-            v-for="item in filteredOptions"
-            :key="item.value"
-            style="width: calc(50% - 8px)"
-            class="shrink-0"
-          >
-            <div
-              class="w-full min-h-20px border-dashed border-1px border-border grid place-items-center py-4px text-center"
-              :class="getItemClass(item)"
-              @click="handleSelect(item)"
-            >
-              {{ item.label }}
+        <div class="w-full my-12px">
+          <template v-for="group in groupedOptions" :key="group.key">
+            <div class="w-full mb-8px text-12px text-text-secondary font-600">
+              {{ group.label }}
             </div>
-          </div>
 
-          <div v-if="!filteredOptions.length" class="w-full h-120px grid place-items-center">
+            <div class="w-full mb-12px flex flex-wrap gap-x-12px gap-y-8px">
+              <div
+                v-for="item in group.options"
+                :key="item.value"
+                style="width: calc(50% - 8px)"
+                class="shrink-0"
+              >
+                <div
+                  class="w-full min-h-20px border-dashed border-1px border-border grid place-items-center py-4px text-center"
+                  :class="getItemClass(item)"
+                  @click="handleSelect(item)"
+                >
+                  {{ item.label }}
+                </div>
+              </div>
+            </div>
+          </template>
+
+          <div v-if="!groupedOptions.length" class="w-full h-120px grid place-items-center">
             暂无数据
           </div>
         </div>
@@ -128,6 +136,39 @@ const filteredOptions = computed(() => {
   return options.filter((item) => item.label.toLowerCase().includes(keyword))
 })
 
+const groupedOptions = computed(() => {
+  const groups = new Map<
+    string,
+    {
+      key: string
+      label: string
+      order: number
+      options: OptionType[]
+    }
+  >()
+
+  filteredOptions.value.forEach((item) => {
+    const key = item.groupKey || 'default'
+    const label = item.groupLabel || ''
+    const order = item.groupOrder ?? Number.MAX_SAFE_INTEGER
+    const current = groups.get(key)
+
+    if (current) {
+      current.options.push(item)
+      return
+    }
+
+    groups.set(key, {
+      key,
+      label,
+      order,
+      options: [item]
+    })
+  })
+
+  return [...groups.values()].sort((a, b) => a.order - b.order)
+})
+
 const getItemClass = (item: OptionType) => {
   if (item.disabled) {
     return 'cursor-not-allowed bg-bg-secondary text-text-secondary opacity-50'

+ 781 - 59
src/renderer/src/views/designer/workspace/composite/eventEdit/config.ts

@@ -7,8 +7,10 @@ import type {
 } from '@/types/event'
 import type { OptionType } from './type'
 
+import { get } from 'lodash-es'
 import { klona } from 'klona'
 import { bfsWalk } from 'simple-mind-map/src/utils'
+
 import LvglWidgets from '@/lvgl-widgets'
 import { flagOptions, stateOptions } from '@/constants'
 
@@ -17,25 +19,38 @@ type SchemaActionDescriptor = {
   defaultValue: any
   label: string
   schemas: ComponentSchema[]
+  groupKey?: string
+  groupLabel?: string
+  groupOrder?: number
+}
+
+type EventSettableSchema = ComponentSchema & {
+  canUseEventSet?: boolean
+  dependency?: (values: Record<string, any>) => ComponentSchema[] | ComponentSchema
+  name?: string[]
+}
+
+type EventStyleSchema = ComponentSchema & {
+  key: string
 }
 
 const pageLoadAnimationOptions = [
-  { label: 'None', value: 'none' },
-  { label: 'Over Left', value: 'over_left' },
-  { label: 'Over Right', value: 'over_right' },
-  { label: 'Over Top', value: 'over_top' },
-  { label: 'Over Bottom', value: 'over_bottom' },
-  { label: 'Move Left', value: 'move_left' },
-  { label: 'Move Right', value: 'move_right' },
-  { label: 'Move Top', value: 'move_top' },
-  { label: 'Move Bottom', value: 'move_bottom' },
-  { label: 'Fade In', value: 'fade_in' },
-  { label: 'Fade On', value: 'fade_on' },
-  { label: 'Fade Out', value: 'fade_out' },
-  { label: 'Out Left', value: 'out_left' },
-  { label: 'Out Right', value: 'out_right' },
-  { label: 'Out Top', value: 'out_top' },
-  { label: 'Out Bottom', value: 'out_bottom' }
+  { label: '', value: 'none' },
+  { label: '覆盖左侧', value: 'over_left' },
+  { label: '覆盖右侧', value: 'over_right' },
+  { label: '覆盖顶部', value: 'over_top' },
+  { label: '覆盖底部', value: 'over_bottom' },
+  { label: '左移', value: 'move_left' },
+  { label: '右移', value: 'move_right' },
+  { label: '上移', value: 'move_top' },
+  { label: '下移', value: 'move_bottom' },
+  { label: '淡入', value: 'fade_in' },
+  { label: '淡开', value: 'fade_on' },
+  { label: '淡出', value: 'fade_out' },
+  { label: '退出左侧', value: 'out_left' },
+  { label: '退出右侧', value: 'out_right' },
+  { label: '退出顶部', value: 'out_top' },
+  { label: '退出底部', value: 'out_bottom' }
 ]
 
 const eventOption = (
@@ -95,6 +110,656 @@ const commonEventOptions = [
   eventOption('Cancel', 'LV_EVENT_CANCEL')
 ]
 
+/**
+ * 事件编辑统一样式配置
+ * 仅在控件自身存在 config.styles 时启用
+ */
+const styleSchemas: EventStyleSchema[] = [
+  {
+    key: 'background.color',
+    label: '背景颜色',
+    field: 'background.color',
+    valueType: 'color',
+    defaultValue: '#000000ff'
+  },
+  {
+    key: 'background.alpha',
+    label: '背景透明度',
+    field: 'background.alpha',
+    valueType: 'color',
+    defaultValue: '#000000ff'
+  },
+  {
+    key: 'background.gradient.direction',
+    label: '背景渐变方向',
+    field: 'background.alpha',
+    valueType: 'select',
+    defaultValue: 'none',
+    componentProps: {
+      options: [
+        { label: 'None', value: 'none' },
+        { label: 'Horizontal', value: 'horizontal' },
+        { label: 'Vertical', value: 'vertical' }
+      ]
+    }
+  },
+  {
+    key: 'background.gradient.color',
+    label: '背景渐变颜色',
+    field: 'background.gradient.color',
+    valueType: 'color',
+    defaultValue: '#000000ff'
+  },
+  {
+    key: 'background.gradient.alpha',
+    label: '背景渐变透明度',
+    field: 'background.gradient.alpha',
+    valueType: 'number',
+    defaultValue: 255,
+    componentProps: {
+      min: 0,
+      max: 255
+    }
+  },
+  {
+    key: 'background.gradient.start',
+    label: '背景渐变起始点',
+    field: 'background.gradient.start',
+    valueType: 'number',
+    defaultValue: 0,
+    componentProps: {
+      min: 0,
+      max: 255
+    }
+  },
+  {
+    key: 'background.gradient.end',
+    label: '背景渐变结束点',
+    field: 'background.gradient.end',
+    valueType: 'number',
+    defaultValue: 255,
+    componentProps: {
+      min: 0,
+      max: 255
+    }
+  },
+  {
+    key: 'background.image.imgId',
+    label: '背景图',
+    field: 'background.image.imgId',
+    valueType: 'image'
+  },
+  {
+    key: 'background.image.recolor',
+    label: '背景图片遮罩',
+    field: 'background.image.recolor',
+    valueType: 'color',
+    defaultValue: '#ffffff00'
+  },
+  {
+    key: 'background.image.alpha',
+    label: '背景透明度',
+    field: 'background.image.alpha',
+    valueType: 'number',
+    defaultValue: 255,
+    componentProps: {
+      min: 0,
+      max: 255
+    }
+  },
+  {
+    key: 'text.color',
+    label: '字体颜色',
+    field: 'text.color',
+    valueType: 'color',
+    defaultValue: '#000000ff'
+  },
+  {
+    key: 'text.alpha',
+    label: '字体透明度',
+    field: 'text.alpha',
+    valueType: 'number',
+    defaultValue: 255,
+    componentProps: {
+      min: 0,
+      max: 255
+    }
+  },
+  {
+    key: 'text.family',
+    label: '字体',
+    field: 'text.family',
+    valueType: 'family',
+    defaultValue: 'montserratMedium'
+  },
+  {
+    key: 'text.align',
+    label: '字体对齐',
+    field: 'text.align',
+    valueType: 'select',
+    defaultValue: 'left',
+    componentProps: {
+      options: [
+        { label: '左对齐', value: 'left' },
+        { label: '居中对齐', value: 'center' },
+        { label: '右对齐', value: 'right' }
+      ]
+    }
+  },
+  {
+    key: 'text.decoration',
+    label: '字体装饰',
+    field: 'text.decoration',
+    valueType: 'select',
+    defaultValue: 'none',
+    componentProps: {
+      options: [
+        { label: 'None', value: 'none' },
+        { label: 'Underline', value: 'underline' },
+        { label: 'Strikethrough', value: 'line-through' },
+        { label: 'Underline&Strikethrough', value: 'underline line-through' }
+      ]
+    }
+  },
+  {
+    key: 'spacer.letterSpacing',
+    label: '字间距',
+    field: 'spacer.letterSpacing',
+    valueType: 'number',
+    defaultValue: 0,
+    componentProps: {
+      min: -10000,
+      max: 10000
+    }
+  },
+  {
+    key: 'spacer.lineHeight',
+    label: '行间距',
+    field: 'spacer.lineHeight',
+    valueType: 'number',
+    defaultValue: 0,
+    componentProps: {
+      min: -10000,
+      max: 10000
+    }
+  },
+  {
+    key: 'border.color',
+    label: '边框颜色',
+    field: 'border.color',
+    valueType: 'color',
+    defaultValue: '#000000ff'
+  },
+  {
+    key: 'border.alpha',
+    label: '边框透明度',
+    field: 'border.alpha',
+    valueType: 'number',
+    defaultValue: 255,
+    componentProps: {
+      min: 0,
+      max: 255
+    }
+  },
+  {
+    key: 'border.width',
+    label: '边框宽度',
+    field: 'border.width',
+    valueType: 'number',
+    defaultValue: 0,
+    componentProps: {
+      min: 0,
+      max: 10000
+    }
+  },
+  {
+    key: 'border.radius',
+    label: '边框圆角',
+    field: 'border.radius',
+    valueType: 'number',
+    defaultValue: 0,
+    componentProps: {
+      min: 0,
+      max: 10000
+    }
+  },
+  {
+    key: 'border.side',
+    label: '边框选择',
+    field: 'border.side',
+    valueType: 'checkbox',
+    defaultValue: ['all'],
+    componentProps: {
+      options: [
+        { label: '所有边框', value: 'all' },
+        { label: '左边框', value: 'left' },
+        { label: '上边框', value: 'top' },
+        { label: '下边框', value: 'bottom' },
+        { label: '右边框', value: 'right' }
+      ]
+    }
+  },
+  {
+    key: 'outline.color',
+    label: '轮廓颜色',
+    field: 'outline.color',
+    valueType: 'color',
+    defaultValue: '#000000ff'
+  },
+  {
+    key: 'outline.alpha',
+    label: '轮廓透明度',
+    field: 'outline.alpha',
+    valueType: 'number',
+    defaultValue: 255,
+    componentProps: {
+      min: 0,
+      max: 255
+    }
+  },
+  {
+    key: 'outline.width',
+    label: '轮廓宽度',
+    field: 'outline.width',
+    valueType: 'number',
+    defaultValue: 0,
+    componentProps: {
+      min: 0,
+      max: 10000
+    }
+  },
+  {
+    key: 'outline.pad',
+    label: '轮廓间距',
+    field: 'outline.pad',
+    valueType: 'number',
+    defaultValue: 0,
+    componentProps: {
+      min: 0,
+      max: 10000
+    }
+  },
+  {
+    key: 'padding.top',
+    label: '内上边距',
+    field: 'padding.top',
+    valueType: 'number',
+    defaultValue: 0,
+    componentProps: {
+      min: 0,
+      max: 10000
+    }
+  },
+  {
+    key: 'padding.bottom',
+    label: '内下边距',
+    field: 'padding.bottom',
+    valueType: 'number',
+    defaultValue: 0,
+    componentProps: {
+      min: 0,
+      max: 10000
+    }
+  },
+  {
+    key: 'padding.left',
+    label: '内左边距',
+    field: 'padding.left',
+    valueType: 'number',
+    defaultValue: 0,
+    componentProps: {
+      min: 0,
+      max: 10000
+    }
+  },
+  {
+    key: 'padding.right',
+    label: '内右边距',
+    field: 'padding.right',
+    valueType: 'number',
+    defaultValue: 0,
+    componentProps: {
+      min: 0,
+      max: 10000
+    }
+  },
+  {
+    key: 'padding.row',
+    label: '内行边距',
+    field: 'padding.row',
+    valueType: 'number',
+    defaultValue: 0,
+    componentProps: {
+      min: 0,
+      max: 10000
+    }
+  },
+  {
+    key: 'padding.column',
+    label: '内列边距',
+    field: 'padding.column',
+    valueType: 'number',
+    defaultValue: 0,
+    componentProps: {
+      min: 0,
+      max: 10000
+    }
+  },
+  {
+    key: 'margin.top',
+    label: '外上边距',
+    field: 'margin.top',
+    valueType: 'number',
+    defaultValue: 0,
+    componentProps: {
+      min: 0,
+      max: 10000
+    }
+  },
+  {
+    key: 'margin.bottom',
+    label: '外下边距',
+    field: 'margin.bottom',
+    valueType: 'number',
+    defaultValue: 0,
+    componentProps: {
+      min: 0,
+      max: 10000
+    }
+  },
+  {
+    key: 'margin.left',
+    label: '外左边距',
+    field: 'margin.left',
+    valueType: 'number',
+    defaultValue: 0,
+    componentProps: {
+      min: 0,
+      max: 10000
+    }
+  },
+  {
+    key: 'margin.right',
+    label: '外右边距',
+    field: 'margin.right',
+    valueType: 'number',
+    defaultValue: 0,
+    componentProps: {
+      min: 0,
+      max: 10000
+    }
+  },
+  {
+    key: 'shadow.color',
+    label: '阴影颜色',
+    field: 'shadow.color',
+    valueType: 'color',
+    defaultValue: '#000000ff'
+  },
+  {
+    key: 'shadow.alpha',
+    label: '阴影透明度',
+    field: 'shadow.alpha',
+    valueType: 'number',
+    defaultValue: 255,
+    componentProps: {
+      min: 0,
+      max: 255
+    }
+  },
+  {
+    key: 'shadow.offsetX',
+    label: '阴影偏移X',
+    field: 'shadow.offsetX',
+    valueType: 'number',
+    defaultValue: 0,
+    componentProps: {
+      min: -10000,
+      max: 10000
+    }
+  },
+  {
+    key: 'shadow.offsetY',
+    label: '阴影偏移Y',
+    field: 'shadow.offsetY',
+    valueType: 'number',
+    defaultValue: 0,
+    componentProps: {
+      min: -10000,
+      max: 10000
+    }
+  },
+  {
+    key: 'shadow.spread',
+    label: '阴影扩散',
+    field: 'shadow.spread',
+    valueType: 'number',
+    defaultValue: 0,
+    componentProps: {
+      min: 0,
+      max: 10000
+    }
+  },
+  {
+    key: 'shadow.width',
+    label: '阴影宽度',
+    field: 'shadow.width',
+    valueType: 'number',
+    defaultValue: 0,
+    componentProps: {
+      min: 0,
+      max: 10000
+    }
+  },
+  {
+    key: 'transform.width',
+    label: '宽变换',
+    field: 'transform.width',
+    valueType: 'number',
+    defaultValue: 0,
+    componentProps: {
+      min: -10000,
+      max: 10000
+    }
+  },
+  {
+    key: 'transform.height',
+    label: '高变换',
+    field: 'transform.height',
+    valueType: 'number',
+    defaultValue: 0,
+    componentProps: {
+      min: -10000,
+      max: 10000
+    }
+  },
+  {
+    key: 'transform.translateX',
+    label: 'X变换',
+    field: 'transform.translateX',
+    valueType: 'number',
+    defaultValue: 0,
+    componentProps: {
+      min: -10000,
+      max: 10000
+    }
+  },
+  {
+    key: 'transform.translateY',
+    label: 'Y变换',
+    field: 'transform.translateY',
+    valueType: 'number',
+    defaultValue: 0,
+    componentProps: {
+      min: -10000,
+      max: 10000
+    }
+  },
+  {
+    key: 'transform.originX',
+    label: '中心X',
+    field: 'transform.originX',
+    valueType: 'number',
+    defaultValue: 0,
+    componentProps: {
+      min: -10000,
+      max: 10000
+    }
+  },
+  {
+    key: 'transform.originY',
+    label: '中心Y',
+    field: 'transform.originY',
+    valueType: 'number',
+    defaultValue: 0,
+    componentProps: {
+      min: -10000,
+      max: 10000
+    }
+  },
+  {
+    key: 'transform.rotate',
+    label: '旋转角度',
+    field: 'transform.rotate',
+    valueType: 'number',
+    defaultValue: 0,
+    componentProps: {
+      min: -3600,
+      max: 3600
+    }
+  },
+  {
+    key: 'transform.scale',
+    label: '缩放',
+    field: 'transform.scale',
+    valueType: 'number',
+    defaultValue: 256,
+    componentProps: {
+      min: 0,
+      max: 10000
+    }
+  },
+  {
+    key: 'imageStyle.alpha',
+    label: '图片透明度',
+    field: 'imageStyle.alpha',
+    valueType: 'number',
+    defaultValue: 255,
+    componentProps: {
+      min: 0,
+      max: 255
+    }
+  },
+  {
+    key: 'imageStyle.recolor',
+    label: '图片遮罩',
+    field: 'imageStyle.recolor',
+    valueType: 'color',
+    defaultValue: '#ffffff00'
+  },
+  {
+    key: 'line.color',
+    label: '直线颜色',
+    field: 'line.color',
+    valueType: 'color',
+    defaultValue: '#000000ff'
+  },
+  {
+    key: 'line.width',
+    label: '直线宽度',
+    field: 'line.width',
+    valueType: 'number',
+    defaultValue: 0,
+    componentProps: {
+      min: 0,
+      max: 10000
+    }
+  },
+  {
+    key: 'line.radius',
+    label: '直线圆角',
+    field: 'line.radius',
+    valueType: 'switch',
+    defaultValue: false
+  },
+  {
+    key: 'line.dashWidth',
+    label: '虚线宽度',
+    field: 'line.dashWidth',
+    valueType: 'number',
+    defaultValue: 0,
+    componentProps: {
+      min: 0,
+      max: 10000
+    }
+  },
+  {
+    key: 'line.dashGap',
+    label: '虚线间隔',
+    field: 'line.dashGap',
+    valueType: 'number',
+    defaultValue: 0,
+    componentProps: {
+      min: 0,
+      max: 10000
+    }
+  },
+  {
+    key: 'curve.color',
+    label: '曲线颜色',
+    field: 'curve.color',
+    valueType: 'color',
+    defaultValue: '#000000ff'
+  },
+  {
+    key: 'curve.image',
+    label: '曲线图片',
+    field: 'curve.image',
+    valueType: 'image',
+    defaultValue: ''
+  },
+  {
+    key: 'curve.width',
+    label: '曲线宽度',
+    field: 'curve.width',
+    valueType: 'number',
+    defaultValue: 0,
+    componentProps: {
+      min: 0,
+      max: 10000
+    }
+  },
+  {
+    key: 'curve.radius',
+    label: '曲线圆角',
+    field: 'curve.radius',
+    valueType: 'switch',
+    defaultValue: false
+  },
+  {
+    key: 'other.length',
+    label: '长度',
+    field: 'other.length',
+    valueType: 'number',
+    defaultValue: 0,
+    componentProps: {
+      min: 0,
+      max: 10000
+    }
+  },
+  {
+    key: 'animation.time',
+    label: '动画时间',
+    field: 'animation.time',
+    valueType: 'number',
+    defaultValue: 0,
+    componentProps: {
+      min: 0,
+      max: 100000
+    }
+  }
+]
+
 export const widgetEventOptions = [...commonEventOptions]
 
 export const pageEventOptions = [
@@ -105,20 +770,6 @@ export const pageEventOptions = [
   eventOption('Unloaded', 'LV_EVENT_SCREEN_UNLOADED')
 ]
 
-const builtinVisibleAction: SchemaActionDescriptor = {
-  actionKey: 'builtin.visible',
-  label: '显示',
-  defaultValue: true,
-  schemas: [
-    {
-      label: '显示',
-      field: 'payload',
-      labelWidth: '120px',
-      valueType: 'switch'
-    }
-  ]
-}
-
 function getSchemaLabel(schema: ComponentSchema): string {
   if (schema.label) return schema.label
 
@@ -182,55 +833,109 @@ function createDefaultValue(schema: ComponentSchema): any {
   }
 }
 
-function cloneActionSchema(schema: ComponentSchema): ComponentSchema {
+function createSchemaDefaultValue(defaultRoot: any, schema: ComponentSchema) {
+  if (schema.field) {
+    const currentValue = get(defaultRoot, schema.field)
+    if (currentValue !== undefined) return klona(currentValue)
+  }
+
+  return createDefaultValue(schema)
+}
+
+function cloneActionSchema(schema: ComponentSchema, targetField = 'payload'): ComponentSchema {
   const cloned = klona(schema)
-  cloned.field = 'payload'
+  cloned.field = targetField
   return cloned
 }
 
-function flattenComponentSchemas(items: ComponentSchema[] = []): SchemaActionDescriptor[] {
+function buildDependencyValues(names: string[] = [], defaultRoot: any) {
+  return names.reduce<Record<string, any>>((result, key) => {
+    result[key] = get(defaultRoot, key)
+    return result
+  }, {})
+}
+
+function flattenEventSettableSchemas(
+  items: EventSettableSchema[] = [],
+  defaultRoot: any
+): SchemaActionDescriptor[] {
   const result: SchemaActionDescriptor[] = []
 
   items.forEach((item) => {
     if (!item) return
 
     if (item.valueType === 'group') {
-      if (!isSchemaActionable(item)) {
-        result.push(...flattenComponentSchemas(item.children || []))
-        return
-      }
+      result.push(
+        ...flattenEventSettableSchemas((item.children || []) as EventSettableSchema[], defaultRoot)
+      )
+      return
+    }
 
-      result.push({
-        actionKey: item.field!,
-        label: getSchemaLabel(item),
-        schemas: [cloneActionSchema(item)],
-        defaultValue: createDefaultValue(item)
-      })
+    if (item.valueType === 'dependency' && typeof item.dependency === 'function') {
+      const dependencySchemas = item.dependency(buildDependencyValues(item.name, defaultRoot))
+      const schemaList = Array.isArray(dependencySchemas) ? dependencySchemas : [dependencySchemas]
+      result.push(...flattenEventSettableSchemas(schemaList as EventSettableSchema[], defaultRoot))
       return
     }
 
+    if (item.canUseEventSet !== true) return
     if (!isSchemaActionable(item)) return
 
     result.push({
       actionKey: item.field!,
       label: getSchemaLabel(item),
+      groupKey: 'property',
+      groupLabel: '属性',
+      groupOrder: 1,
       schemas: [cloneActionSchema(item)],
-      defaultValue: createDefaultValue(item)
+      defaultValue: createSchemaDefaultValue(defaultRoot, item)
     })
   })
 
   return result
 }
 
-function getStyleActionDescriptors(items: ComponentSchema[] = []): SchemaActionDescriptor[] {
-  return items
-    .filter((item) => !!item && isSchemaActionable(item))
-    .map((item) => ({
-      actionKey: `style.${item.field}`,
-      label: item.label || item.field || '样式',
-      schemas: [cloneActionSchema(item)],
-      defaultValue: createDefaultValue(item)
-    }))
+function createStyleStateSchema(): ComponentSchema {
+  return {
+    label: '状态',
+    field: 'payload.state',
+    labelWidth: '120px',
+    valueType: 'select',
+    defaultValue: 'LV_STATE_DEFAULT',
+    componentProps: {
+      options: stateOptions,
+      clearable: true,
+      placeholder: '请选择状态'
+    }
+  }
+}
+
+function getStyleActionDescriptors(targetData?: WidgetEventTargetData): SchemaActionDescriptor[] {
+  const model = getTargetModel(targetData)
+  const styleDefaults = model?.defaultSchema?.styles?.[0] || {}
+  const widgetStyleSchemas = model?.config?.styles || []
+
+  if (!widgetStyleSchemas.length) return []
+
+  return styleSchemas.map((item) => {
+    const defaultValue =
+      get(styleDefaults, item.field!) !== undefined
+        ? createSchemaDefaultValue(styleDefaults, item)
+        : klona(item.defaultValue)
+
+    return {
+      actionKey: `style.${item.key}`,
+      label: item.label || item.key || '样式',
+      groupKey: 'style',
+      groupLabel: '样式',
+      groupOrder: 2,
+      schemas: [createStyleStateSchema(), cloneActionSchema(item, 'payload.style')],
+      defaultValue: {
+        state: '',
+        style: defaultValue
+      }
+    }
+  })
 }
 
 function getTargetModel(targetData?: WidgetEventTargetData) {
@@ -245,10 +950,15 @@ function getSchemaActionDescriptors(targetData?: WidgetEventTargetData): SchemaA
   if (!model) return []
 
   return [
-    builtinVisibleAction,
-    ...flattenComponentSchemas(model.config.props || []),
-    ...flattenComponentSchemas(model.config.coreProps || []),
-    ...getStyleActionDescriptors(model.config.styles || [])
+    ...flattenEventSettableSchemas(
+      (model.config.props || []) as EventSettableSchema[],
+      model.defaultSchema
+    ),
+    ...flattenEventSettableSchemas(
+      (model.config.coreProps || []) as EventSettableSchema[],
+      model.defaultSchema
+    ),
+    ...getStyleActionDescriptors(targetData)
   ]
 }
 
@@ -269,6 +979,9 @@ function getLoadPageActionDescriptor(
   return {
     actionKey: 'builtin.load_page',
     label: '加载页面',
+    groupKey: 'builtin',
+    groupLabel: '动作',
+    groupOrder: 1,
     defaultValue: {
       pageId: pageOptions[0]?.value || '',
       animationType: 'none',
@@ -340,6 +1053,9 @@ function getLanguageSwitchActionDescriptor(project?: IProject): SchemaActionDesc
   return {
     actionKey: 'builtin.language_switch',
     label: '语言切换',
+    groupKey: 'builtin',
+    groupLabel: '动作',
+    groupOrder: 1,
     defaultValue: {
       languageKey: languageOptions[0]?.value || '',
       refreshPage: true
@@ -381,6 +1097,9 @@ function getThemeSwitchActionDescriptor(project?: IProject): SchemaActionDescrip
   return {
     actionKey: 'builtin.theme_switch',
     label: '主题切换',
+    groupKey: 'builtin',
+    groupLabel: '动作',
+    groupOrder: 1,
     defaultValue: {
       screen1Theme: preferredTheme,
       ...(isDualScreen ? { screen2Theme: preferredTheme } : {})
@@ -498,7 +1217,7 @@ export function getTargetOptions(project?: IProject): OptionType[] {
   project?.screens.forEach((screen) => {
     screen.pages.forEach((page) => {
       options.push({
-        label: `[${screen.name}] [Page] ${page.name}`,
+        label: `[${screen.name}] ${page.name}`,
         value: `page:${page.id}`,
         data: {
           kind: 'page',
@@ -540,6 +1259,9 @@ export function getActionOptions(
   return descriptors.map((item) => ({
     label: item.label,
     value: item.actionKey,
+    groupKey: item.groupKey,
+    groupLabel: item.groupLabel,
+    groupOrder: item.groupOrder,
     data: {
       actionKey: item.actionKey
     } satisfies WidgetEventActionData,
@@ -566,5 +1288,5 @@ export const optionMap = {
     { label: '隐藏', value: false }
   ],
   flags: flagOptions,
-  states: stateOptions
+  states: [{ label: 'Default', value: 'LV_STATE_DEFAULT' }, ...stateOptions]
 }

+ 63 - 15
src/renderer/src/views/designer/workspace/composite/eventEdit/index.vue

@@ -84,13 +84,20 @@ const createBuiltinActionNode = (
   }
 }
 
-const createTargetChildren = (targetId: string, targetData: WidgetEventTargetData): NodeItemType[] => {
+const createTargetChildren = (
+  targetId: string,
+  targetData: WidgetEventTargetData
+): NodeItemType[] => {
   if (!isBuiltinTarget(targetData)) {
     return [getAddNode(targetId, 'target')]
   }
 
   const builtinActionData = getBuiltinActionData(targetData)
-  const actionOptions = getActionOptions(targetData, projectStore.project, projectStore.activePage?.id)
+  const actionOptions = getActionOptions(
+    targetData,
+    projectStore.project,
+    projectStore.activePage?.id
+  )
   const actionOption = builtinActionData
     ? actionOptions.find((item) => item.value === builtinActionData.actionKey)
     : undefined
@@ -181,12 +188,45 @@ const attachUiNodes = (node: NodeItemType): NodeItemType => {
   }
 }
 
+const sanitizeEventValue = (value: any, seen = new WeakSet<object>()) => {
+  if (value === null || value === undefined) return value
+  if (typeof value !== 'object') return value
+
+  if (seen.has(value as object)) {
+    return undefined
+  }
+
+  seen.add(value as object)
+
+  if (Array.isArray(value)) {
+    return value.map((item) => sanitizeEventValue(item, seen)).filter((item) => item !== undefined)
+  }
+
+  const result: Record<string, any> = {}
+
+  Object.entries(value as Record<string, any>).forEach(([key, item]) => {
+    if (key === '_node') return
+    if (typeof item === 'function') return
+
+    const nextValue = sanitizeEventValue(item, seen)
+    if (nextValue !== undefined) {
+      result[key] = nextValue
+    }
+  })
+
+  return result
+}
+
 const stripUiNodes = (node?: NodeItemType): NodeItemType | undefined => {
   if (!node || node.nodeType === 'add') return
 
-  const baseNode: NodeItemType = {
-    ...node
-  }
+  const baseNode: NodeItemType = sanitizeEventValue({
+    id: node.id,
+    name: node.name,
+    value: sanitizeEventValue(node.value),
+    nodeType: node.nodeType,
+    config: sanitizeEventValue(node.config)
+  })
 
   if (node.children) {
     baseNode.children = node.children
@@ -199,12 +239,12 @@ const stripUiNodes = (node?: NodeItemType): NodeItemType | undefined => {
   return baseNode
 }
 
-const updateStoreEvents = () => {
+const updateStoreEvents = (data?: NodeItemType) => {
   const widget = projectStore.activeWidget
-  const data = stripUiNodes(mindMapData.value)
+  const sourceData = data || stripUiNodes(mindMapData.value)
 
-  if (!widget || !data || widget.id !== data.id) return
-  widget.events = (data.children || []) as any
+  if (!widget || !sourceData || widget.id !== sourceData.id) return
+  widget.events = (sourceData.children || []) as any
 }
 
 const refresh = () => {
@@ -213,9 +253,10 @@ const refresh = () => {
   const cleanData = stripUiNodes(mindMapData.value)
   if (!cleanData) return
 
-  mindMapData.value = attachUiNodes(cleanData)
-  updateStoreEvents()
-  mindMap.value?.updateData(mindMapData.value)
+  const nextData = attachUiNodes(cleanData)
+  mindMapData.value = nextData
+  updateStoreEvents(cleanData)
+  mindMap.value?.setData(nextData)
 }
 
 const getSiblingUsedValues = (parentId: string, currentNodeId?: string) => {
@@ -265,9 +306,11 @@ const handleAddNode = (parentId: string, parentType: string, values: OptionType[
       break
 
     case 'target':
-      values.forEach((item) => {
-        insertNodes.push(createActionNode(parentId, item))
-      })
+      values
+        .filter((item) => !usedValues.has(item.value))
+        .forEach((item) => {
+          insertNodes.push(createActionNode(parentId, item))
+        })
       break
   }
 
@@ -299,6 +342,11 @@ const handleChangeNode = (node: NodeItemType) => {
     Object.assign(currentNode, node)
   })
 
+  if (node.nodeType === 'action') {
+    updateStoreEvents()
+    return
+  }
+
   refresh()
 }
 

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

@@ -13,6 +13,9 @@ export type OptionType = {
   label: string
   value: string
   disabled?: boolean
+  groupKey?: string
+  groupLabel?: string
+  groupOrder?: number
   data?: any
   defaultValue?: any
   [key: string]: any