Quellcode durchsuchen

feat: 贝塞尔属性配置

jiaxing.liao vor 3 Wochen
Ursprung
Commit
cf0439372f

+ 72 - 1
src/renderer/src/lvgl-widgets/variableConfig.ts

@@ -1,5 +1,6 @@
 import type { ComponentSchema, IComponentModelConfig } from './type'
 import type { VariableType } from '@/types/variables'
+import { klona } from 'klona'
 
 type VariableConfig = {
   type: VariableType
@@ -38,6 +39,54 @@ const commonWidgetVariableConfig: Record<string, VariableConfig> = {
   'props.scrollbar': { type: 'enum', enumMap: scrollbarEnumMap }
 }
 
+const createBezierDefaultConfig = () => ({
+  enabled: false,
+  mode: 'time',
+  time: {
+    duration: 500,
+    easing: 'linear',
+    repeatCount: -1,
+    playback: {
+      enabled: false,
+      duration: 500,
+      delay: 500
+    },
+    autoPlay: false,
+    reverse: false,
+    scale: {
+      enabled: false,
+      targetWidth: undefined,
+      targetHeight: undefined,
+      duration: 500,
+      delay: 500
+    }
+  },
+  value: {
+    range: {
+      min: 0,
+      max: 100
+    },
+    current: 50
+  },
+  path: {
+    source: 'resource',
+    resourceId: '',
+    segments: []
+  },
+  showTrack: true
+})
+
+const bezierConfigSchema: ComponentSchema = {
+  label: '贝塞尔动画',
+  field: 'props.bezierConfig',
+  valueType: 'bezier',
+  componentProps: {}
+}
+
+const hasBezierConfigSchema = (props?: ComponentSchema[]) => {
+  return props?.some((item) => item.field === 'props.bezierConfig')
+}
+
 const widgetVariableConfigMap: WidgetVariableConfigMap = {
   lv_button: {
     ...commonPositionSizeFields,
@@ -269,11 +318,33 @@ export const enhanceWidgetVariableConfig = (widget: IComponentModelConfig): ICom
     ...(widgetVariableConfigMap[widget.key] || {})
   }
 
+  // 给所有非 page 控件统一补上贝塞尔动画配置,避免每个组件重复维护。
+  const nextDefaultSchema = {
+    ...widget.defaultSchema,
+    props:
+      widget.key === 'page'
+        ? widget.defaultSchema.props
+        : {
+            ...(widget.defaultSchema.props || {}),
+            ...(widget.defaultSchema.props?.bezierConfig
+              ? {}
+              : {
+                  bezierConfig: klona(createBezierDefaultConfig())
+                })
+          }
+  }
+
+  const nextProps = widget.config.props ? [...widget.config.props] : []
+  if (widget.key !== 'page' && !hasBezierConfigSchema(nextProps)) {
+    nextProps.push(bezierConfigSchema)
+  }
+
   return {
     ...widget,
+    defaultSchema: nextDefaultSchema,
     config: {
       ...widget.config,
-      props: widget.config.props?.map((item) => withVariableConfig(item, configMap)),
+      props: nextProps.map((item) => withVariableConfig(item, configMap)),
       coreProps: widget.config.coreProps?.map((item) => withVariableConfig(item, configMap)),
       styles: widget.config.styles?.map((item) => withVariableConfig(item, configMap))
     }

+ 38 - 2
src/renderer/src/views/designer/config/property/CusFormItem.vue

@@ -161,7 +161,15 @@
       :model-value="componentProps?.defaultCollapsed ? [] : [key]"
       @change="(val) => emit('changeCollapse', val)"
     >
-      <el-collapse-item :title="schema.label" :name="key" style="margin-bottom: 12px">
+      <el-collapse-item
+        v-if="
+          schema.valueType !== 'bezier' ||
+          (schema.valueType === 'bezier' && !shouldHideBezierConfig)
+        "
+        :title="schema.label"
+        :name="key"
+        style="margin-bottom: 12px"
+      >
         <el-card body-class="p-4px! pt-12px!" class="mb-8px!">
           <el-row :gutter="12" class="px-4px">
             <CusFormItem
@@ -179,8 +187,9 @@
 
             <!-- 贝塞尔动画配置 -->
             <BezierConfig
-              v-if="schema.valueType === 'bezier'"
+              v-if="schema.valueType === 'bezier' && !shouldHideBezierConfig"
               v-model="value"
+              :form-data="formData"
               v-bind="componentProps"
             />
           </el-row>
@@ -553,6 +562,33 @@ const dependencyFormItems = computed(() => {
   }
   return []
 })
+
+const findParentWidget = (
+  widgets: Record<string, any>[] = [],
+  targetId?: string,
+  parent?: Record<string, any>
+) => {
+  if (!targetId) return undefined
+
+  for (const widget of widgets) {
+    if (widget.id === targetId) return parent
+    const found = findParentWidget(widget.children || [], targetId, widget)
+    if (found) return found
+  }
+
+  return undefined
+}
+
+const parentWidget = computed(() => {
+  const widgetId = props.formData?.id
+  if (!widgetId || !projectStore.activePage?.children?.length) return undefined
+  return findParentWidget(projectStore.activePage.children, widgetId)
+})
+
+const shouldHideBezierConfig = computed(() => {
+  if (props.schema.valueType !== 'bezier') return false
+  return parentWidget.value?.type === 'lv_obj' && parentWidget.value?.props?.layout === 'flex'
+})
 </script>
 
 <style lang="less" scoped>

+ 18 - 9
src/renderer/src/views/designer/config/property/components/BezierConfig.vue

@@ -248,13 +248,9 @@ export type BezierAnimationConfig = {
 
 const props = withDefaults(
   defineProps<{
-    defaultWidth?: number
-    defaultHeight?: number
+    formData: Record<string, any>
   }>(),
-  {
-    defaultWidth: 100,
-    defaultHeight: 100
-  }
+  {}
 )
 
 const modelValue = defineModel<BezierAnimationConfig>('modelValue')
@@ -299,8 +295,8 @@ const createDefaultConfig = (): BezierAnimationConfig => ({
     reverse: false,
     scale: {
       enabled: false,
-      targetWidth: props.defaultWidth,
-      targetHeight: props.defaultHeight,
+      targetWidth: props.formData?.props?.width || 0,
+      targetHeight: props.formData?.props?.height || 0,
       duration: 500,
       delay: 500
     }
@@ -322,7 +318,8 @@ const createDefaultConfig = (): BezierAnimationConfig => ({
 
 const mergeConfig = (value?: Partial<BezierAnimationConfig>): BezierAnimationConfig => {
   const defaults = createDefaultConfig()
-  return {
+  const { props: compProps } = props.formData || {}
+  const result = {
     ...defaults,
     ...value,
     time: {
@@ -353,6 +350,18 @@ const mergeConfig = (value?: Partial<BezierAnimationConfig>): BezierAnimationCon
         : defaults.path.segments
     }
   }
+
+  // 初次加载使用控件宽高
+  if (
+    (compProps.width || compProps.height) &&
+    !result.time.scale.targetWidth &&
+    !result.time.scale.targetHeight
+  ) {
+    result.time.scale.targetWidth = compProps.width
+    result.time.scale.targetHeight = compProps.height
+  }
+
+  return result
 }
 
 const config = computed<BezierAnimationConfig>(() => modelValue.value || createDefaultConfig())

+ 48 - 0
src/renderer/src/views/designer/workspace/stage/BezierTrackLayer.vue

@@ -0,0 +1,48 @@
+<template>
+  <svg
+    class="bezier-track-layer"
+    :viewBox="`0 0 ${width} ${height}`"
+    :width="width"
+    :height="height"
+  >
+    <g v-for="track in tracks" :key="track.id" :transform="`translate(${track.rect.x}, ${track.rect.y})`">
+      <path
+        v-for="(segment, index) in track.segments"
+        :key="index"
+        :d="getSegmentPath(segment)"
+        fill="none"
+        stroke="#a0a0a0"
+        stroke-width="2"
+        stroke-dasharray="5 5"
+        stroke-linecap="round"
+      />
+    </g>
+  </svg>
+</template>
+
+<script setup lang="ts">
+import type { BezierSegment } from '@/types/resource'
+import type { BezierTrack } from './useBezierTrack'
+
+defineProps<{
+  width: number
+  height: number
+  tracks: BezierTrack[]
+}>()
+
+const getSegmentPath = (segment: BezierSegment) => {
+  return `M ${segment.start.x} ${segment.start.y} C ${segment.control1.x} ${segment.control1.y}, ${segment.control2.x} ${segment.control2.y}, ${segment.end.x} ${segment.end.y}`
+}
+</script>
+
+<style scoped>
+.bezier-track-layer {
+  position: absolute;
+  inset: 0;
+  width: 100%;
+  height: 100%;
+  overflow: visible;
+  pointer-events: none;
+  z-index: 2;
+}
+</style>

+ 9 - 1
src/renderer/src/views/designer/workspace/stage/DesignerCanvas.vue

@@ -27,6 +27,11 @@
             position: 'static'
           }"
         />
+        <BezierTrackLayer
+          :width="state.width"
+          :height="state.height"
+          :tracks="bezierTracks"
+        />
         <Moveable :root-container="canvasRef!" :page-id="page?.id" />
       </div>
     </div>
@@ -38,7 +43,7 @@ import type { StageState } from './type'
 import type { Page } from '@/types/page'
 import type { Screen } from '@/types/screen'
 
-import { ref, onMounted, computed, nextTick } from 'vue'
+import { ref, onMounted, computed, nextTick, toRef } from 'vue'
 import { useScroll, useElementSize } from '@vueuse/core'
 import { useProjectStore } from '@/store/modules/project'
 import { useCanvasMove } from './useCanvasMove'
@@ -46,6 +51,8 @@ import { throttle } from 'lodash-es'
 
 import Nodes from './Node.vue'
 import Moveable from './Moveable.vue'
+import BezierTrackLayer from './BezierTrackLayer.vue'
+import { useBezierTrack } from './useBezierTrack'
 
 const props = defineProps<{
   state: StageState
@@ -59,6 +66,7 @@ const stageWrapperRef = ref<HTMLDivElement>()
 const canvasRef = ref<HTMLDivElement>()
 const { width: clientWidth, height: clientHeight } = useElementSize(stageWrapperRef)
 const projectStore = useProjectStore()
+const { tracks: bezierTracks } = useBezierTrack(toRef(props, 'page'))
 
 useCanvasMove(stageWrapperRef)
 

+ 82 - 0
src/renderer/src/views/designer/workspace/stage/useBezierTrack.ts

@@ -0,0 +1,82 @@
+import type { BaseWidget } from '@/types/baseWidget'
+import type { Page } from '@/types/page'
+import type { BezierSegment } from '@/types/resource'
+
+import { computed, unref, type MaybeRef } from 'vue'
+import { get } from 'lodash-es'
+import { useProjectStore } from '@/store/modules/project'
+import { getAllVariables, resolveVariableBoundValue } from '@/utils/variableBinding'
+
+export type BezierTrack = {
+  id: string
+  segments: BezierSegment[]
+  rect: {
+    x: number
+    y: number
+    width: number
+    height: number
+  }
+}
+
+const normalizeSegments = (segments?: BezierSegment[]) => {
+  return Array.isArray(segments) ? segments : []
+}
+
+export const useBezierTrack = (page?: MaybeRef<Page | undefined>) => {
+  const projectStore = useProjectStore()
+
+  const tracks = computed<BezierTrack[]>(() => {
+    const result: BezierTrack[] = []
+    const currentPage = unref(page)
+    if (!currentPage?.children?.length) return result
+
+    const allVariables = getAllVariables(
+      projectStore.project?.variables || [],
+      projectStore.activePage?.variables
+    )
+
+    const walkWidgets = (widgets: BaseWidget[], offset = { x: 0, y: 0 }) => {
+      widgets.forEach((widget) => {
+        const resolvedProps = resolveVariableBoundValue(widget.props || {}, allVariables)
+        const widgetX = offset.x + Number(resolvedProps?.x ?? 0)
+        const widgetY = offset.y + Number(resolvedProps?.y ?? 0)
+        const bezierConfig = get(resolvedProps, 'bezierConfig')
+
+        if (bezierConfig?.enabled && bezierConfig.showTrack) {
+          const segments =
+            bezierConfig.path?.source === 'resource'
+              ? projectStore.project?.resources.bezierAnimations.find(
+                  (item) => item.id === bezierConfig.path.resourceId
+                )?.segments
+              : bezierConfig.path?.segments
+
+          const normalizedSegments = normalizeSegments(segments)
+          if (normalizedSegments.length) {
+            result.push({
+              id: widget.id,
+              segments: normalizedSegments,
+              rect: {
+                x: widgetX,
+                y: widgetY,
+                width: Number(resolvedProps?.width ?? 0),
+                height: Number(resolvedProps?.height ?? 0)
+              }
+            })
+          }
+        }
+
+        if (widget.children?.length) {
+          walkWidgets(widget.children, { x: widgetX, y: widgetY })
+        }
+      })
+    }
+
+    walkWidgets(currentPage.children)
+
+    return result
+  })
+
+  return {
+    tracks
+  }
+}