Selaa lähdekoodia

feat: 添加贝塞尔动画事件配置

jiaxing.liao 3 viikkoa sitten
vanhempi
commit
51ee0d817f

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

@@ -1,5 +1,5 @@
 <template>
-  <div class="min-h-32px bg-#4EB2BF rounded-4px flex items-start pr-4px">
+  <div ref="containerRef" class="min-h-32px bg-#4EB2BF rounded-4px flex items-start pr-4px">
     <div
       class="w-160px text-#fff grid place-items-center shrink-0 py-8px text-center"
       v-if="!actionSchemas.length"
@@ -48,9 +48,10 @@
 <script setup lang="ts">
 import type { NodeItemType } from './type'
 
-import { computed, ref, watch } from 'vue'
+import { computed, nextTick, ref, watch } from 'vue'
 import { klona } from 'klona'
 import { LuTrash2 } from 'vue-icons-plus/lu'
+import { useResizeObserver } from '@vueuse/core'
 
 import { useProjectStore } from '@/store/modules/project'
 import CusFormItem from '@/views/designer/config/property/CusFormItem.vue'
@@ -69,9 +70,15 @@ const props = defineProps<{
 }>()
 
 const projectStore = useProjectStore()
+const containerRef = ref<HTMLElement | null>(null)
 const formData = ref<{ payload: any }>({
   payload: klona(props.node.value)
 })
+const lastNodeSize = ref({
+  width: 0,
+  height: 0
+})
+let rerenderTimer: ReturnType<typeof setTimeout> | null = null
 
 const actionMeta = computed(() => props.node.config?.data)
 const isCustomCode = computed(() => actionMeta.value?.kind === 'custom_code')
@@ -107,7 +114,12 @@ const methodValue = computed({
 })
 
 const handleReRender = () => {
-  setTimeout(() => {
+  if (rerenderTimer) {
+    clearTimeout(rerenderTimer)
+  }
+
+  rerenderTimer = setTimeout(() => {
+    rerenderTimer = null
     const changed = props.mindMapNode?.reRender?.([], {
       force: true
     })
@@ -118,6 +130,20 @@ const handleReRender = () => {
   }, 0)
 }
 
+useResizeObserver(containerRef, async (entries) => {
+  const entry = entries[0]
+  if (!entry || !actionSchemas.value.length) return
+
+  const width = Math.round(entry.contentRect.width)
+  const height = Math.round(entry.contentRect.height)
+
+  if (width === lastNodeSize.value.width && height === lastNodeSize.value.height) return
+
+  lastNodeSize.value = { width, height }
+  await nextTick()
+  handleReRender()
+})
+
 watch(
   () => props.node.value,
   (val) => {

+ 405 - 5
src/renderer/src/views/designer/workspace/composite/eventEdit/config.ts

@@ -53,6 +53,21 @@ const pageLoadAnimationOptions = [
   { label: '退出底部', value: 'out_bottom' }
 ]
 
+const bezierAnimationModeOptions = [
+  { label: '时间控制', value: 'time' },
+  { label: '值控制', value: 'value' }
+]
+
+const bezierAnimationEasingOptions = [
+  { label: 'Linear', value: 'linear' },
+  { label: 'Ease In', value: 'ease-in' },
+  { label: 'Ease Out', value: 'ease-out' },
+  { label: 'Ease In Out', value: 'ease-in-out' },
+  { label: 'Bounce', value: 'bounce' },
+  { label: 'Back', value: 'back' },
+  { label: 'Cubic In Out', value: 'cubic-in-out' }
+]
+
 const eventOption = (
   label: string,
   eventCode: string,
@@ -107,7 +122,11 @@ const commonEventOptions = [
   eventOption('Hit Test', 'LV_EVENT_HIT_TEST'),
   eventOption('Insert', 'LV_EVENT_INSERT'),
   eventOption('Ready', 'LV_EVENT_READY'),
-  eventOption('Cancel', 'LV_EVENT_CANCEL')
+  eventOption('Cancel', 'LV_EVENT_CANCEL'),
+  eventOption('Load Start', 'LV_EVENT_SCREEN_LOAD_START'),
+  eventOption('Loaded', 'LV_EVENT_SCREEN_LOADED'),
+  eventOption('Unload Start', 'LV_EVENT_SCREEN_UNLOAD_START'),
+  eventOption('Unloaded', 'LV_EVENT_SCREEN_UNLOADED')
 ]
 
 /**
@@ -1023,7 +1042,7 @@ function flattenEventActionSchemas(
           actionKey: item.field || `group.${nextPathTokens.join('.')}`,
           label: getSchemaLabel(item),
           groupKey: 'property',
-          groupLabel: 'Property',
+          groupLabel: '属性',
           groupOrder: 1,
           schemas: [cloneEventActionSchema(item)],
           defaultValue: createSchemaDefaultValue(defaultRoot, item)
@@ -1113,6 +1132,383 @@ function getStyleActionDescriptors(targetData?: WidgetEventTargetData): SchemaAc
   })
 }
 
+function getTargetWidgetSchemaData(project?: IProject, targetData?: WidgetEventTargetData) {
+  if (!project || !targetData) return
+
+  for (const screen of project.screens || []) {
+    for (const page of screen.pages || []) {
+      if (targetData.kind === 'page' && page.id === (targetData.targetId || targetData.pageId)) {
+        return page
+      }
+
+      if (targetData.kind !== 'widget') continue
+
+      let matched: Record<string, any> | undefined
+      bfsWalk(page, (child) => {
+        if (child.id === targetData.targetId) {
+          matched = child
+        }
+      })
+
+      if (matched) return matched
+    }
+  }
+
+  return undefined
+}
+
+function clampBezierScaleSize(value: unknown) {
+  const numeric = Number(value)
+  if (Number.isFinite(numeric) && numeric >= 1) return numeric
+  return 1
+}
+
+function getBezierAnimationDefaultValue(
+  targetData?: WidgetEventTargetData,
+  project?: IProject
+) {
+  const model = getTargetModel(targetData)
+  const targetSchemaData = getTargetWidgetSchemaData(project, targetData) as Record<string, any> | undefined
+
+  const defaultWidth = clampBezierScaleSize(
+    targetSchemaData?.props?.width ?? model?.defaultSchema?.props?.width
+  )
+  const defaultHeight = clampBezierScaleSize(
+    targetSchemaData?.props?.height ?? model?.defaultSchema?.props?.height
+  )
+
+  return {
+    mode: 'time',
+    time: {
+      duration: 500,
+      easing: 'linear',
+      repeatCount: -1,
+      playback: {
+        enabled: false,
+        duration: 500,
+        delay: 500
+      },
+      autoPlay: false,
+      reverse: false,
+      scale: {
+        enabled: false,
+        targetWidth: defaultWidth,
+        targetHeight: defaultHeight,
+        duration: 500,
+        delay: 500
+      }
+    },
+    value: {
+      range: {
+        min: 0,
+        max: 100
+      },
+      current: 50
+    },
+    path: {
+      resourceId: ''
+    }
+  }
+}
+
+function normalizeBezierValueRange(formData?: Record<string, any>) {
+  if (!formData) return
+
+  const min = Number(get(formData, 'payload.value.range.min'))
+  const max = Number(get(formData, 'payload.value.range.max'))
+  const current = Number(get(formData, 'payload.value.current'))
+
+  const normalizedMin = Number.isFinite(min) ? min : 0
+  const normalizedMax = Number.isFinite(max) ? max : 100
+  const nextMax = normalizedMax < normalizedMin ? normalizedMin : normalizedMax
+  const nextCurrent = Math.min(
+    Math.max(Number.isFinite(current) ? current : normalizedMin, normalizedMin),
+    nextMax
+  )
+
+  set(formData, 'payload.value.range.min', normalizedMin)
+  set(formData, 'payload.value.range.max', nextMax)
+  set(formData, 'payload.value.current', nextCurrent)
+}
+
+function getBezierAnimationActionDescriptors(
+  targetData?: WidgetEventTargetData,
+  project?: IProject
+): SchemaActionDescriptor[] {
+  if (targetData?.kind !== 'widget') return []
+
+  const bezierOptions =
+    project?.resources.bezierAnimations.map((item) => ({
+      label: item.name,
+      value: item.id,
+      disabled: (item.segments?.length || 0) > 100
+    })) || []
+
+  return [
+    {
+      actionKey: 'animation.bezier',
+      label: '贝塞尔动画',
+      groupKey: 'animation',
+      groupLabel: '动画',
+      groupOrder: 3,
+      defaultValue: getBezierAnimationDefaultValue(targetData, project),
+      schemas: [
+        {
+          label: '模式',
+          field: 'payload.mode',
+          labelWidth: '120px',
+          valueType: 'select',
+          defaultValue: 'time',
+          componentProps: {
+            options: bezierAnimationModeOptions
+          }
+        },
+        {
+          valueType: 'dependency',
+          name: ['payload.mode'],
+          dependency: (values) => {
+            if (values['payload.mode'] === 'value') {
+              return [
+                {
+                  label: '最小',
+                  field: 'payload.value.range.min',
+                  labelWidth: '120px',
+                  valueType: 'number',
+                  defaultValue: 0,
+                  componentProps: {
+                    min: -100000,
+                    max: 100000,
+                    onValueChange: (_val: number, formData: Record<string, any>) => {
+                      normalizeBezierValueRange(formData)
+                    }
+                  }
+                },
+                {
+                  label: '最大',
+                  field: 'payload.value.range.max',
+                  labelWidth: '120px',
+                  valueType: 'number',
+                  defaultValue: 100,
+                  componentProps: {
+                    min: -100000,
+                    max: 100000,
+                    onValueChange: (_val: number, formData: Record<string, any>) => {
+                      normalizeBezierValueRange(formData)
+                    }
+                  }
+                },
+                {
+                  valueType: 'dependency',
+                  name: ['payload.value.range.min', 'payload.value.range.max'],
+                  dependency: (rangeValues) => {
+                    const min = Number(rangeValues['payload.value.range.min'])
+                    const max = Number(rangeValues['payload.value.range.max'])
+                    const normalizedMin = Number.isFinite(min) ? min : -100000
+                    const normalizedMax = Number.isFinite(max)
+                      ? Math.max(max, normalizedMin)
+                      : 100000
+
+                    return {
+                      label: '当前值',
+                      field: 'payload.value.current',
+                      labelWidth: '120px',
+                      valueType: 'number',
+                      defaultValue: 50,
+                      componentProps: {
+                        min: normalizedMin,
+                        max: normalizedMax,
+                        onValueChange: (_val: number, formData: Record<string, any>) => {
+                          normalizeBezierValueRange(formData)
+                        }
+                      }
+                    } satisfies ComponentSchema
+                  }
+                },
+                {
+                  label: '贝塞尔段',
+                  field: 'payload.path.resourceId',
+                  labelWidth: '120px',
+                  valueType: 'select',
+                  defaultValue: '',
+                  componentProps: {
+                    options: bezierOptions,
+                    clearable: true,
+                    placeholder: '请选择资源中的贝塞尔段'
+                  }
+                }
+              ] satisfies ComponentSchema[]
+            }
+
+            return [
+              {
+                label: '时间',
+                field: 'payload.time.duration',
+                labelWidth: '120px',
+                valueType: 'number',
+                defaultValue: 500,
+                componentProps: {
+                  min: 0,
+                  max: 100000
+                },
+                slots: {
+                  suffix: 'ms'
+                }
+              },
+              {
+                label: '缓动效果',
+                field: 'payload.time.easing',
+                labelWidth: '120px',
+                valueType: 'select',
+                defaultValue: 'linear',
+                componentProps: {
+                  options: bezierAnimationEasingOptions
+                }
+              },
+              {
+                label: '重复次数',
+                field: 'payload.time.repeatCount',
+                labelWidth: '120px',
+                valueType: 'number',
+                defaultValue: -1,
+                componentProps: {
+                  min: -1,
+                  max: 100000
+                }
+              },
+              {
+                label: '回放',
+                field: 'payload.time.playback.enabled',
+                labelWidth: '120px',
+                valueType: 'switch',
+                defaultValue: false
+              },
+              {
+                valueType: 'dependency',
+                name: ['payload.time.playback.enabled'],
+                dependency: (playbackValues) => {
+                  if (!playbackValues['payload.time.playback.enabled']) return []
+
+                  return [
+                    {
+                      label: '回放时间',
+                      field: 'payload.time.playback.duration',
+                      labelWidth: '120px',
+                      valueType: 'number',
+                      defaultValue: 500,
+                      componentProps: {
+                        min: 0,
+                        max: 100000
+                      },
+                      slots: {
+                        suffix: 'ms'
+                      }
+                    },
+                    {
+                      label: '回放延时时间',
+                      field: 'payload.time.playback.delay',
+                      labelWidth: '120px',
+                      valueType: 'number',
+                      defaultValue: 500,
+                      componentProps: {
+                        min: 0,
+                        max: 100000
+                      },
+                      slots: {
+                        suffix: 'ms'
+                      }
+                    }
+                  ] satisfies ComponentSchema[]
+                }
+              },
+              {
+                label: '自动播放',
+                field: 'payload.time.autoPlay',
+                labelWidth: '120px',
+                valueType: 'switch',
+                defaultValue: false
+              },
+              {
+                label: '倒放',
+                field: 'payload.time.reverse',
+                labelWidth: '120px',
+                valueType: 'switch',
+                defaultValue: false
+              },
+              {
+                label: '缩放',
+                field: 'payload.time.scale.enabled',
+                labelWidth: '120px',
+                valueType: 'switch',
+                defaultValue: false
+              },
+              {
+                valueType: 'dependency',
+                name: ['payload.time.scale.enabled'],
+                dependency: (scaleValues) => {
+                  if (!scaleValues['payload.time.scale.enabled']) return []
+
+                  return [
+                    {
+                      label: '目标宽度',
+                      field: 'payload.time.scale.targetWidth',
+                      labelWidth: '120px',
+                      valueType: 'number',
+                      defaultValue: 1,
+                      componentProps: {
+                        min: 1,
+                        max: 10000
+                      }
+                    },
+                    {
+                      label: '目标高度',
+                      field: 'payload.time.scale.targetHeight',
+                      labelWidth: '120px',
+                      valueType: 'number',
+                      defaultValue: 1,
+                      componentProps: {
+                        min: 1,
+                        max: 10000
+                      }
+                    },
+                    {
+                      label: '缩放时间',
+                      field: 'payload.time.scale.duration',
+                      labelWidth: '120px',
+                      valueType: 'number',
+                      defaultValue: 500,
+                      componentProps: {
+                        min: 0,
+                        max: 100000
+                      },
+                      slots: {
+                        suffix: 'ms'
+                      }
+                    },
+                    {
+                      label: '缩放延时时间',
+                      field: 'payload.time.scale.delay',
+                      labelWidth: '120px',
+                      valueType: 'number',
+                      defaultValue: 500,
+                      componentProps: {
+                        min: 0,
+                        max: 100000
+                      },
+                      slots: {
+                        suffix: 'ms'
+                      }
+                    }
+                  ] satisfies ComponentSchema[]
+                }
+              }
+            ] satisfies ComponentSchema[]
+          }
+        }
+      ]
+    }
+  ]
+}
+
 function getTargetModel(targetData?: WidgetEventTargetData) {
   if (!targetData) return
   if (targetData.kind === 'page' || targetData.kind === 'load_page') return LvglWidgets.page
@@ -1120,7 +1516,10 @@ function getTargetModel(targetData?: WidgetEventTargetData) {
   return LvglWidgets[targetData.targetType]
 }
 
-function getSchemaActionDescriptors(targetData?: WidgetEventTargetData): SchemaActionDescriptor[] {
+function getSchemaActionDescriptors(
+  targetData?: WidgetEventTargetData,
+  project?: IProject
+): SchemaActionDescriptor[] {
   const model = getTargetModel(targetData)
   if (!model) return []
 
@@ -1133,7 +1532,8 @@ function getSchemaActionDescriptors(targetData?: WidgetEventTargetData): SchemaA
       (model.config.coreProps || []) as EventSettableSchema[],
       model.defaultSchema
     ),
-    ...getStyleActionDescriptors(targetData)
+    ...getStyleActionDescriptors(targetData),
+    ...getBezierAnimationActionDescriptors(targetData, project)
   ]
 }
 
@@ -1323,7 +1723,7 @@ function getBuiltinActionDescriptors(
     case 'custom_code':
       return []
     default:
-      return getSchemaActionDescriptors(targetData)
+      return getSchemaActionDescriptors(targetData, project)
   }
 }