Quellcode durchsuchen

feat: 添加变量绑定处理

jiaxing.liao vor 1 Monat
Ursprung
Commit
ee2d067f42

+ 2 - 19
src/renderer/src/lvgl-widgets/hooks/useWidgetStyle.ts

@@ -4,7 +4,7 @@ import { assign } from 'lodash-es'
 import { computed, ref, watch } from 'vue'
 import { generateCssGradient } from '@/utils'
 import { DEFAULT_THEME_KEY } from '@/constants'
-import { findVariableById, getAllVariables, isVariableBound } from '@/utils/variableBinding'
+import { getAllVariables, resolveVariableBoundValue } from '@/utils/variableBinding'
 
 import type { IStyleConfig, PartItem } from '../type'
 import type { CSSProperties } from 'vue'
@@ -251,23 +251,6 @@ export const useWidgetStyle = (param: StyleParam) => {
       projectStore.project?.variables || [],
       projectStore.activePage?.variables
     )
-    const resolveVariableValue = (val: any): any => {
-      if (isVariableBound(val)) {
-        const variable = findVariableById(val.varId, allVariables)
-        return variable ? variable.value : val.originValue
-      }
-      if (Array.isArray(val)) {
-        return val.map((item) => resolveVariableValue(item))
-      }
-      if (val && typeof val === 'object') {
-        const result = {}
-        Object.keys(val).forEach((k) => {
-          result[k] = resolveVariableValue(val[k])
-        })
-        return result
-      }
-      return val
-    }
     parts.forEach((partItem) => {
       styleMap.value[`${partItem.name}Style`] = {}
       // 从控件配置的样式列表查找对应样式
@@ -304,7 +287,7 @@ export const useWidgetStyle = (param: StyleParam) => {
 
       // 合并到预设样式
       style = assign({}, presetStyle || {}, defaultStyle || {}, style || {})
-      const resolvedStyle = resolveVariableValue(style)
+      const resolvedStyle = resolveVariableBoundValue(style, allVariables)
 
       // 遍历style 获取样式
       Object.keys(resolvedStyle || {}).forEach((key) => {

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

@@ -45,6 +45,7 @@ import Barcode from './barcode/index'
 
 import Page from './page'
 import { IComponentModelConfig } from './type'
+import { enhanceWidgetVariableConfig } from './variableConfig'
 
 export const ComponentArray = [
   Page,
@@ -96,7 +97,7 @@ export const ComponentArray = [
   Barcode,
 
   BaseMeter
-] as IComponentModelConfig[]
+].map((item) => enhanceWidgetVariableConfig(item)) as IComponentModelConfig[]
 
 const componentMap: { [key: string]: IComponentModelConfig } = ComponentArray.reduce((acc, cur) => {
   acc[cur.key] = cur

+ 6 - 0
src/renderer/src/lvgl-widgets/type.d.ts

@@ -1,4 +1,5 @@
 import { CSSProperties } from 'vue'
+import type { VariableType } from '@/types/variables'
 
 type PartItem = {
   name: string
@@ -39,6 +40,11 @@ export interface ComponentSchema {
   slots?: { [slotName: string]: ComponentSchema[] | string }
   // 节点类型,必选
   valueType: string
+  // 变量绑定配置
+  variableConfig?: {
+    type: VariableType
+    enumMap?: Record<number | string, string>
+  }
   // 配置器隐藏
   hideInForm?: boolean
   // 渲染表单项

+ 281 - 0
src/renderer/src/lvgl-widgets/variableConfig.ts

@@ -0,0 +1,281 @@
+import type { ComponentSchema, IComponentModelConfig } from './type'
+import type { VariableType } from '@/types/variables'
+
+type VariableConfig = {
+  type: VariableType
+  enumMap?: Record<number | string, string>
+}
+
+type WidgetVariableConfigMap = Record<string, Record<string, VariableConfig>>
+
+const longModeEnumMap = { 0: 'wrap', 1: 'dot', 2: 'scroll', 3: 'circular', 4: 'clip' }
+const dropdownDirectionEnumMap = { 1: 'left', 2: 'top', 4: 'bottom', 8: 'right' }
+const barModeEnumMap = { 0: 'normal', 1: 'symmetrical', 2: 'range' }
+const tabviewPositionEnumMap = { 1: 'left', 2: 'top', 4: 'bottom', 8: 'right' }
+const arcModeEnumMap = { 0: 'normal', 1: 'symmetrical', 2: 'reverse' }
+const scrollbarEnumMap = { 0: 'off', 1: 'on', 2: 'active', 3: 'auto' }
+const scaleModeEnumMap = {
+  0: 'horizontal_top',
+  1: 'horizontal_bottom',
+  2: 'vertical_left',
+  4: 'vertical_right'
+}
+const rollerModeEnumMap = { 0: 'normal', 1: 'infinite' }
+const keyboardModeEnumMap = { 0: 'lower', 1: 'upper', 2: 'special', 3: 'number' }
+const videoSourceEnumMap = { 0: 'file', 1: 'signal' }
+const barcodeDirectionEnumMap = { 3: 'horizontal', 12: 'vertical' }
+const meterModeEnumMap = { 8: 'round_inner', 16: 'round_outer' }
+
+const commonPositionSizeFields: Record<string, VariableConfig> = {
+  'props.x': { type: 'int32_t' },
+  'props.y': { type: 'int32_t' },
+  'props.width': { type: 'int32_t' },
+  'props.height': { type: 'int32_t' }
+}
+
+const commonWidgetVariableConfig: Record<string, VariableConfig> = {
+  ...commonPositionSizeFields,
+  'props.scrollbar': { type: 'enum', enumMap: scrollbarEnumMap }
+}
+
+const widgetVariableConfigMap: WidgetVariableConfigMap = {
+  lv_button: {
+    ...commonPositionSizeFields,
+    'props.text': { type: 'char' },
+    'props.longMode': { type: 'enum', enumMap: longModeEnumMap }
+  },
+  lv_imagebutton: {
+    ...commonPositionSizeFields,
+    'props.text': { type: 'char' }
+  },
+  lv_label: {
+    ...commonPositionSizeFields,
+    'props.text': { type: 'char' },
+    'props.longMode': { type: 'enum', enumMap: longModeEnumMap },
+    'props.lightStart': { type: 'uint32_t' },
+    'props.lightEnd': { type: 'uint32_t' }
+  },
+  lv_span: {
+    ...commonPositionSizeFields,
+    'props.mode': { type: 'enum', enumMap: { 0: 'fixed', 1: 'expand', 2: 'break' } }
+  },
+  lv_textarea: {
+    ...commonPositionSizeFields,
+    'props.text': { type: 'char' },
+    'props.placeholder': { type: 'char' },
+    'props.maxLength': { type: 'uint32_t' },
+    'props.allowString': { type: 'char' },
+    'props.passwordReplaceText': { type: 'char' },
+    'props.passwordMode': { type: 'bool' },
+    'props.nowrap': { type: 'bool' }
+  },
+  lv_dropdown: {
+    ...commonPositionSizeFields,
+    'props.direction': { type: 'enum', enumMap: dropdownDirectionEnumMap }
+  },
+  lv_checkbox: {
+    'props.x': { type: 'int32_t' },
+    'props.y': { type: 'int32_t' },
+    'props.text': { type: 'char' }
+  },
+  lv_bar: {
+    ...commonPositionSizeFields,
+    'props.min': { type: 'int32_t' },
+    'props.max': { type: 'int32_t' },
+    'props.value': { type: 'int32_t' },
+    'props.animationTime': { type: 'uint32_t' },
+    'props.animation': { type: 'bool' },
+    'props.mode': { type: 'enum', enumMap: barModeEnumMap },
+    'props.startValue': { type: 'int32_t' }
+  },
+  lv_slider: {
+    ...commonPositionSizeFields,
+    'props.min': { type: 'int32_t' },
+    'props.max': { type: 'int32_t' },
+    'props.value': { type: 'int32_t' },
+    'props.mode': { type: 'enum', enumMap: barModeEnumMap },
+    'props.startValue': { type: 'int32_t' }
+  },
+  lv_tabview: {
+    ...commonPositionSizeFields,
+    'props.size': { type: 'int32_t' },
+    'props.position': { type: 'enum', enumMap: tabviewPositionEnumMap }
+  },
+  lv_msgbox: {
+    ...commonPositionSizeFields,
+    'props.title': { type: 'char' },
+    'props.content': { type: 'char' },
+    'props.btnWidth': { type: 'int32_t' },
+    'props.btnHeight': { type: 'int32_t' }
+  },
+  lv_win: {
+    ...commonPositionSizeFields,
+    'props.title': { type: 'char' },
+    'props.titleHeight': { type: 'int32_t' },
+    'props.text': { type: 'char' }
+  },
+  lv_line: {
+    ...commonPositionSizeFields
+  },
+  lv_arc: {
+    ...commonPositionSizeFields,
+    'props.mode': { type: 'enum', enumMap: arcModeEnumMap },
+    'props.rangeStart': { type: 'int32_t' },
+    'props.rangeEnd': { type: 'int32_t' },
+    'props.value': { type: 'int32_t' },
+    'props.angleStart': { type: 'int16_t' },
+    'props.angleEnd': { type: 'int16_t' },
+    'props.rotate': { type: 'int32_t' },
+    'props.rotateOffset': { type: 'bool' },
+    'props.rotateOffsetValue': { type: 'int32_t' },
+    'props.changeRate': { type: 'uint32_t' }
+  },
+  lv_scale: {
+    ...commonPositionSizeFields,
+    'props.mode': { type: 'enum', enumMap: scaleModeEnumMap },
+    'props.tick': { type: 'uint32_t' },
+    'props.mainTick': { type: 'uint32_t' },
+    'props.rangeStart': { type: 'int32_t' },
+    'props.rangeEnd': { type: 'int32_t' },
+    'props.enableLabels': { type: 'bool' },
+    'props.labels': { type: 'char' }
+  },
+  lv_led: {
+    ...commonPositionSizeFields,
+    'props.led_color': { type: 'int32_t' },
+    'props.led_set_bright': { type: 'uint8_t' }
+  },
+  lv_canvas: {
+    ...commonPositionSizeFields,
+    'props.background_color': { type: 'int32_t' },
+    'props.background_alpha': { type: 'uint8_t' }
+  },
+  lv_spinner: {
+    ...commonPositionSizeFields,
+    'props.length': { type: 'uint32_t' },
+    'props.time': { type: 'uint32_t' }
+  },
+  lv_roller: {
+    ...commonPositionSizeFields,
+    'props.direction': { type: 'enum', enumMap: rollerModeEnumMap }
+  },
+  lv_spinbox: {
+    ...commonPositionSizeFields,
+    'props.rangeMin': { type: 'int32_t' },
+    'props.rangeMax': { type: 'int32_t' },
+    'props.step': { type: 'int32_t' },
+    'props.integerDigits': { type: 'uint32_t' },
+    'props.decimalPlaces': { type: 'uint32_t' },
+    'props.value': { type: 'int32_t' },
+    'props.rollOver': { type: 'bool' }
+  },
+  lv_keyboard: {
+    ...commonPositionSizeFields,
+    'props.mode': { type: 'enum', enumMap: keyboardModeEnumMap },
+    'props.showPopovers': { type: 'bool' }
+  },
+  lv_animimg: {
+    ...commonPositionSizeFields,
+    'props.time': { type: 'uint32_t' },
+    'props.repeatCount': { type: 'int32_t' },
+    'props.playback': { type: 'bool' },
+    'props.playbackTime': { type: 'uint32_t' },
+    'props.playbackDelay': { type: 'uint32_t' },
+    'props.autoPlay': { type: 'bool' },
+    'props.reverse': { type: 'bool' }
+  },
+  lv_calendar: {
+    ...commonPositionSizeFields,
+    'props.currentDate': { type: 'char' },
+    'props.displayDate': { type: 'char' },
+    'props.showLunar': { type: 'bool' }
+  },
+  dagital_clock: {
+    ...commonPositionSizeFields,
+    'props.timeText': { type: 'char' },
+    'props.showSecond': { type: 'bool' },
+    'props.showAmPm': { type: 'bool' }
+  },
+  video: {
+    ...commonPositionSizeFields,
+    'props.source.type': { type: 'enum', enumMap: videoSourceEnumMap },
+    'props.source.file.sd_path': { type: 'char' },
+    'props.source.file.autoplay': { type: 'bool' },
+    'props.source.file.loop': { type: 'bool' }
+  },
+  qrcode: {
+    'props.x': { type: 'int32_t' },
+    'props.y': { type: 'int32_t' },
+    'props.lightColor': { type: 'int32_t' },
+    'props.darkColor': { type: 'int32_t' },
+    'props.size': { type: 'int32_t' },
+    'props.text': { type: 'char' }
+  },
+  barcode: {
+    'props.x': { type: 'int32_t' },
+    'props.y': { type: 'int32_t' },
+    'props.height': { type: 'int32_t' },
+    'props.lightColor': { type: 'int32_t' },
+    'props.darkColor': { type: 'int32_t' },
+    'props.scale': { type: 'uint16_t' },
+    'props.direction': { type: 'enum', enumMap: barcodeDirectionEnumMap },
+    'props.text': { type: 'char' }
+  },
+  base_meter: {
+    ...commonPositionSizeFields,
+    'props.mode': { type: 'enum', enumMap: meterModeEnumMap },
+    'props.tick': { type: 'uint32_t' },
+    'props.mainTick': { type: 'uint32_t' },
+    'props.rangeStart': { type: 'int32_t' },
+    'props.rangeEnd': { type: 'int32_t' },
+    'props.angleRange': { type: 'int32_t' },
+    'props.rotationAngle': { type: 'int32_t' },
+    'props.enableText': { type: 'bool' },
+    'props.labels': { type: 'char' }
+  }
+}
+
+const withVariableConfig = (
+  schema: ComponentSchema,
+  configMap: Record<string, VariableConfig>
+): ComponentSchema => {
+  const nextSchema: ComponentSchema = {
+    ...schema
+  }
+
+  if (schema.field && configMap[schema.field] && !schema.variableConfig) {
+    nextSchema.variableConfig = configMap[schema.field]
+  }
+
+  if (schema.children?.length) {
+    nextSchema.children = schema.children.map((child) => withVariableConfig(child, configMap))
+  }
+
+  if (schema.valueType === 'dependency' && typeof schema.dependency === 'function') {
+    const dependency = schema.dependency
+    nextSchema.dependency = (...args: any[]) => {
+      const result = dependency(...args)
+      const items = Array.isArray(result) ? result : result ? [result] : []
+      return items.map((item) => withVariableConfig(item, configMap))
+    }
+  }
+
+  return nextSchema
+}
+
+export const enhanceWidgetVariableConfig = (widget: IComponentModelConfig): IComponentModelConfig => {
+  const configMap = {
+    ...commonWidgetVariableConfig,
+    ...(widgetVariableConfigMap[widget.key] || {})
+  }
+
+  return {
+    ...widget,
+    config: {
+      ...widget.config,
+      props: widget.config.props?.map((item) => withVariableConfig(item, configMap)),
+      coreProps: widget.config.coreProps?.map((item) => withVariableConfig(item, configMap)),
+      styles: widget.config.styles?.map((item) => withVariableConfig(item, configMap))
+    }
+  }
+}

+ 2 - 0
src/renderer/src/types/variables.d.ts

@@ -38,4 +38,6 @@ export type VariableBinding = {
   originValue: any
   // 来源类型
   source: 'page' | 'global'
+  // 枚举映射
+  enumMap?: Record<number | string, string>
 }

+ 45 - 3
src/renderer/src/utils/variableBinding.ts

@@ -19,7 +19,7 @@ export function getPropertyDisplayValue(propValue: any, allVariables: VariableGr
   }
 
   const variable = findVariableById(propValue.varId, allVariables)
-  return variable ? variable.value : propValue.originValue
+  return resolveBoundValue(propValue, variable?.value)
 }
 
 /**
@@ -28,12 +28,14 @@ export function getPropertyDisplayValue(propValue: any, allVariables: VariableGr
 export function bindVariableToProperty(
   propValue: any,
   varId: string,
-  source: 'page' | 'global'
+  source: 'page' | 'global',
+  enumMap?: Record<number | string, string>
 ): VariableBinding {
   return {
     varId,
     originValue: propValue,
-    source
+    source,
+    enumMap
   }
 }
 
@@ -55,6 +57,46 @@ export function findVariableById(varId: string, allVariables: VariableGroup[]):
   return null
 }
 
+/**
+ * 解析绑定变量的最终值
+ * 变量值优先;当变量值为 null/undefined 时回退到原始值。
+ * 枚举变量存储为 key,这里还原为控件实际使用的 value。
+ */
+export function resolveBoundValue(boundValue: VariableBinding, variableValue?: any): any {
+  const rawValue = variableValue ?? boundValue.originValue
+  if (boundValue.enumMap && rawValue !== null && rawValue !== undefined) {
+    const enumValue = boundValue.enumMap[rawValue]
+    if (enumValue !== undefined) {
+      return enumValue
+    }
+  }
+  return rawValue
+}
+
+/**
+ * 递归解析对象中的变量绑定值
+ */
+export function resolveVariableBoundValue(value: any, allVariables: VariableGroup[]): any {
+  if (isVariableBound(value)) {
+    const variable = findVariableById(value.varId, allVariables)
+    return resolveBoundValue(value, variable?.value)
+  }
+
+  if (Array.isArray(value)) {
+    return value.map((item) => resolveVariableBoundValue(item, allVariables))
+  }
+
+  if (value && typeof value === 'object') {
+    const result = {}
+    Object.keys(value).forEach((key) => {
+      result[key] = resolveVariableBoundValue(value[key], allVariables)
+    })
+    return result
+  }
+
+  return value
+}
+
 /**
  * 获取所有变量(全局 + 当前页面)
  */

+ 10 - 8
src/renderer/src/views/designer/config/property/components/StylePart.vue

@@ -62,18 +62,20 @@ const props = defineProps<{
 }>()
 
 const emit = defineEmits<{
-  (e: 'change', val: 'add' | 'delete'): void
+  (e: 'change', val: 'select' | 'cancel'): void
 }>()
 
 // 部件列表
 const partOptions = computed(() => {
   const fn = props.componentProps?.filterPartOptions
   return (
-    parts?.value?.map((item) => {
-      return typeof fn === 'function'
-        ? fn(item, props.widgetData)
-        : { label: item.name, value: item.name }
-    })?.filter(Boolean) || []
+    parts?.value
+      ?.map((item) => {
+        return typeof fn === 'function'
+          ? fn(item, props.widgetData)
+          : { label: item.name, value: item.name }
+      })
+      ?.filter(Boolean) || []
   )
 })
 
@@ -113,7 +115,7 @@ const expandStyle = computed({
 
 const onChangeOpen = (val) => {
   openTooltipRef.value?.hide()
-  emit('change', val ? 'add' : 'delete')
+  emit('change', val ? 'select' : 'cancel')
 }
 
 const onChangeExpand = () => {
@@ -150,7 +152,7 @@ const openAllStyle = computed({
     return !!valueLength && !hasNotValue
   },
   set(val: boolean) {
-    emit('change', val ? 'add' : 'delete')
+    emit('change', val ? 'select' : 'cancel')
   }
 })
 

+ 14 - 2
src/renderer/src/views/designer/config/property/components/VariableBindWrapper.vue

@@ -159,7 +159,14 @@ const addPageVariables = () => {
       }
       console.log(newVar)
       projectStore.activePage?.variables?.push(newVar)
-      modelValue.value = { varId: newVar.id, originValue: val, source: 'page' }
+      modelValue.value = {
+        varId: newVar.id,
+        originValue: val,
+        source: 'page',
+        ...(props.variableConfig?.type === 'enum' && props.variableConfig.enumMap
+          ? { enumMap: props.variableConfig.enumMap }
+          : {})
+      }
     })
     .catch(() => {
       ElMessage({
@@ -181,7 +188,12 @@ const currentBoundVarId = computed(() => {
  * @param varId
  */
 const handleVariableBindConfirm = (varId: string, source: 'page' | 'global') => {
-  const boundValue = bindVariableToProperty(modelValue.value, varId, source)
+  const boundValue = bindVariableToProperty(
+    modelValue.value,
+    varId,
+    source,
+    props.variableConfig?.type === 'enum' ? props.variableConfig.enumMap : undefined
+  )
   modelValue.value = boundValue
   showVariableSelector.value = false
 }

+ 27 - 12
src/renderer/src/views/designer/config/property/index.vue

@@ -210,6 +210,9 @@ const handleClickMask = (e: MouseEvent) => {
   }
 }
 
+/**
+ * 修改部分主题的样式
+ */
 const setPartByEditThemeStyle = () => {
   const widgetStyles = projectStore.activeWidget?.style || []
   const currentPartThemeStyle = widgetStyles.find(
@@ -286,6 +289,22 @@ const getWidgetDefaultStyle = () => {
   })
 }
 
+/**
+ * 删除样式
+ */
+const deleteStyle = () => {
+  const index =
+    projectStore.activeWidget?.style.findIndex(
+      (item) =>
+        item.part.name === part.value.name &&
+        item.part.state === part.value.state &&
+        getStyleTheme(item) === editTheme.value
+    ) ?? -1
+  if (index !== -1) {
+    projectStore.activeWidget?.style.splice(index, 1)
+  }
+}
+
 /**
  * 设置控件状态部分样式
  * @param field 样式字段名
@@ -312,6 +331,10 @@ const onChangeStateStyle = (field: string, type: 'add' | 'delete') => {
   } else {
     if (styleFormData.value?.[field]) {
       delete styleFormData.value[field]
+      // 如果样式全部删除,只有part和theme那么这一条样式也删掉
+      if (Object.keys(styleFormData.value).length === 2) {
+        deleteStyle()
+      }
     }
   }
 }
@@ -320,8 +343,9 @@ const onChangeStateStyle = (field: string, type: 'add' | 'delete') => {
  * 开启/关闭控件部件状态样式
  * @param type
  */
-const onChangeStyleByState = (type: 'add' | 'delete') => {
-  if (type === 'add') {
+const onChangeStyleByState = (type: 'select' | 'cancel') => {
+  console.log('onChangeStyleByState', type)
+  if (type === 'select') {
     const { defaultStyle, stateStyle } = getWidgetDefaultStyle()
     // 找多对应样式时添加 状态的样式合并到默认样式
     const result = klona({
@@ -335,16 +359,7 @@ const onChangeStyleByState = (type: 'add' | 'delete') => {
     projectStore.activeWidget?.style?.push(result)
   } else {
     // 删除样式
-    const index =
-      projectStore.activeWidget?.style.findIndex(
-        (item) =>
-          item.part.name === part.value.name &&
-          item.part.state === part.value.state &&
-          getStyleTheme(item) === editTheme.value
-      ) ?? -1
-    if (index !== -1) {
-      projectStore.activeWidget?.style.splice(index, 1)
-    }
+    deleteStyle()
   }
 }
 </script>

+ 50 - 37
src/renderer/src/views/designer/workspace/stage/Node.vue

@@ -9,7 +9,7 @@
     :widget-type="schema.type"
     @click="handleSelect"
     @contextmenu.stop="handleContextmenu"
-    :ignore-drag="ignoreDrag && !schema.props?.flags?.includes('LV_OBJ_FLAG_IGNORE_LAYOUT')"
+    :ignore-drag="ignoreDrag && !resolvedSchemaProps?.flags?.includes('LV_OBJ_FLAG_IGNORE_LAYOUT')"
   >
     <component
       v-bind="widgetProps"
@@ -54,6 +54,7 @@ import { useProjectStore } from '@/store/modules/project'
 import { useAppStore } from '@/store/modules/app'
 import { useActionStore } from '@/store/modules/action'
 import { get, has, isEmpty } from 'lodash-es'
+import { getAllVariables, resolveVariableBoundValue } from '@/utils/variableBinding'
 
 import ContextMenu from './ContextMenu.vue'
 import { getAddWidgetIndex, isDescendant } from '@/utils'
@@ -83,11 +84,19 @@ const nodeRef = ref<HTMLDivElement>()
 const zIndex = ref(props.zIndex)
 const dropStyle = ref<CSSProperties>({})
 
+const resolvedSchemaProps = computed(() => {
+  const allVariables = getAllVariables(
+    projectStore.project?.variables || [],
+    projectStore.activePage?.variables
+  )
+  return resolveVariableBoundValue(props.schema.props || {}, allVariables)
+})
+
 const children = computed(() => {
   const { schema } = props
 
   return has(schema.props, 'activeIndex')
-    ? get(schema.children, schema.props.activeIndex)?.children || []
+    ? get(schema.children, resolvedSchemaProps.value.activeIndex)?.children || []
     : schema.children
 })
 
@@ -95,7 +104,7 @@ const widgetProps = computed(() => {
   const { schema } = props
 
   return {
-    ...(schema?.props || {}),
+    ...resolvedSchemaProps.value,
     part: schema?.part,
     state: schema?.state,
     styles: schema?.style,
@@ -105,8 +114,7 @@ const widgetProps = computed(() => {
 })
 
 const ignoreChildrenDrag = computed(() => {
-  const { schema } = props
-  return schema.props?.layout === 'flex'
+  return resolvedSchemaProps.value?.layout === 'flex'
 })
 
 const shouldIgnoreLibraryDrop = computed(() => {
@@ -116,18 +124,19 @@ const shouldIgnoreLibraryDrop = computed(() => {
 
 const getStyle = computed((): CSSProperties => {
   const { style = {}, schema } = props
+  const resolvedProps = resolvedSchemaProps.value
 
   const other: CSSProperties = {}
   if (pageState?.showBorder && schema.type !== 'page') {
     other.border = '1px dashed #000'
-    other.transform = `translate(${schema.props.x - 1}px, ${schema.props.y - 1}px)`
+    other.transform = `translate(${resolvedProps.x - 1}px, ${resolvedProps.y - 1}px)`
   }
 
   let rotate = ''
-  if (schema.props?.rotation) {
-    rotate = `rotate(${schema.props.rotation}deg)`
-    if (schema.props?.pivot) {
-      other.transformOrigin = `${schema.props.pivot.x}px ${schema.props.pivot.y}px`
+  if (resolvedProps?.rotation) {
+    rotate = `rotate(${resolvedProps.rotation}deg)`
+    if (resolvedProps?.pivot) {
+      other.transformOrigin = `${resolvedProps.pivot.x}px ${resolvedProps.pivot.y}px`
     }
 
     if (other.transform) {
@@ -136,13 +145,13 @@ const getStyle = computed((): CSSProperties => {
   }
 
   let scale = 1
-  if (schema.props?.openScale) {
-    scale = (schema.props?.scale ?? 256) / 256
+  if (resolvedProps?.openScale) {
+    scale = (resolvedProps?.scale ?? 256) / 256
   }
 
   const parentStyle = klona(style)
 
-  if (schema.props.flags?.includes('LV_OBJ_FLAG_IGNORE_LAYOUT')) {
+  if (resolvedProps.flags?.includes('LV_OBJ_FLAG_IGNORE_LAYOUT')) {
     delete parentStyle.position
     delete parentStyle.transform
   }
@@ -151,9 +160,9 @@ const getStyle = computed((): CSSProperties => {
     position: 'absolute',
     left: 0,
     top: 0,
-    transform: `translate(${schema.props.x}px, ${schema.props.y}px) ${rotate}`,
-    width: `${schema.props?.width * scale}px`,
-    height: `${schema.props?.height * scale}px`,
+    transform: `translate(${resolvedProps.x}px, ${resolvedProps.y}px) ${rotate}`,
+    width: `${resolvedProps?.width * scale}px`,
+    height: `${resolvedProps?.height * scale}px`,
     zIndex: zIndex.value,
     pointerEvents: shouldIgnoreLibraryDrop.value ? 'none' : undefined,
     ...parentStyle,
@@ -165,28 +174,28 @@ const layoutStyle = ref<CSSProperties>({})
 watch(
   () => [
     props.schema.type,
-    props.schema.props?.layout,
-    props.schema.props?.flex?.direction,
-    props.schema.props?.flex?.mainAxisAlign,
-    props.schema.props?.flex?.crossAxisAlign,
-    props.schema.props?.flex?.trackAxisAlign,
-    props.schema.props?.flex?.rowGap,
-    props.schema.props?.flex?.columnGap,
-    props.schema.props?.width,
-    props.schema.props?.height,
-    props.schema.props?.position,
-    props.schema.props?.sider,
-    props.schema.props?.titleMode,
-    props.schema.props?.inPage
+    resolvedSchemaProps.value?.layout,
+    resolvedSchemaProps.value?.flex?.direction,
+    resolvedSchemaProps.value?.flex?.mainAxisAlign,
+    resolvedSchemaProps.value?.flex?.crossAxisAlign,
+    resolvedSchemaProps.value?.flex?.trackAxisAlign,
+    resolvedSchemaProps.value?.flex?.rowGap,
+    resolvedSchemaProps.value?.flex?.columnGap,
+    resolvedSchemaProps.value?.width,
+    resolvedSchemaProps.value?.height,
+    resolvedSchemaProps.value?.position,
+    resolvedSchemaProps.value?.sider,
+    resolvedSchemaProps.value?.titleMode,
+    resolvedSchemaProps.value?.inPage
   ],
   async () => {
     await nextTick()
-    const { schema } = props
+    const resolvedProps = resolvedSchemaProps.value
     let style: CSSProperties = {}
     layoutStyle.value = {}
 
-    if (schema.props?.layout === 'flex' && schema.props?.flex) {
-      const { flex } = schema.props
+    if (resolvedProps?.layout === 'flex' && resolvedProps?.flex) {
+      const { flex } = resolvedProps
       const directionMap = {
         row: 'row',
         column: 'column',
@@ -214,7 +223,10 @@ watch(
     if (typeof getChildStyle === 'function') {
       style = {
         ...style,
-        ...getChildStyle(props.schema)
+        ...getChildStyle({
+          ...props.schema,
+          props: resolvedProps
+        })
       }
     }
     layoutStyle.value = style
@@ -225,8 +237,7 @@ watch(
 )
 
 const childStyle = computed((): CSSProperties => {
-  const { schema } = props
-  if (schema.props?.layout === 'flex') {
+  if (resolvedSchemaProps.value?.layout === 'flex') {
     return {
       transform: 'none',
       position: 'relative',
@@ -246,8 +257,10 @@ const hasLockedWidgetTree = (widget?: BaseWidget | Page): boolean => {
 const getDropPosition = (event?: DragEvent) => {
   const element = nodeRef.value
   const rect = element?.getBoundingClientRect()
-  const logicalWidth = props.schema.props?.width ?? element?.offsetWidth ?? element?.clientWidth
-  const logicalHeight = props.schema.props?.height ?? element?.offsetHeight ?? element?.clientHeight
+  const logicalWidth =
+    resolvedSchemaProps.value?.width ?? element?.offsetWidth ?? element?.clientWidth
+  const logicalHeight =
+    resolvedSchemaProps.value?.height ?? element?.offsetHeight ?? element?.clientHeight
   if (!rect || !logicalWidth || !logicalHeight || !event) {
     return { x: 0, y: 0 }
   }