فهرست منبع

fix: 修改控件通用问题

jiaxing.liao 1 ماه پیش
والد
کامیت
c3e6a43007

+ 64 - 9
src/renderer/src/components/ColorModal/index.vue

@@ -1,14 +1,14 @@
 <template>
   <el-dialog draggable append-to-body v-model="show" title="背景颜色" width="440px">
+    <div class="flex flex-col items-center">
+      <el-segmented v-if="useType === 'both'" v-model="type" :options="typeOptions" />
+      <div v-if="type !== 'pure'" class="w-full flex flex-col items-center mt-16px">
+        <h4 class="m-0 mb-12px">预览</h4>
+        <div class="w-full h-60px rounded-4px mb-10px" :style="previewGradient"></div>
+      </div>
+    </div>
     <el-scrollbar height="400px">
       <div class="w-full flex flex-col items-center">
-        <el-segmented v-if="useType === 'both'" v-model="type" :options="typeOptions" />
-
-        <div v-if="type !== 'pure'" class="w-full flex flex-col items-center">
-          <h4 class="m-0">预览</h4>
-          <div class="w-80% h-60px rounded-4px mb-10px" :style="previewGradient"></div>
-        </div>
-
         <!-- 纯色 -->
         <div v-if="type === 'pure'" class="mt-20px">
           <ColorPicker
@@ -161,6 +161,28 @@
                   </input-number>
                 </div>
               </el-form-item>
+              <el-form-item label="椭圆范围">
+                <div class="w-full flex items-center gap-8px">
+                  <input-number
+                    v-model="focalExtentX"
+                    :min="0"
+                    :max="width"
+                    size="small"
+                    style="flex: 1"
+                  >
+                    <template #prefix>X</template>
+                  </input-number>
+                  <input-number
+                    v-model="focalExtentY"
+                    :min="0"
+                    :max="height"
+                    size="small"
+                    style="flex: 1"
+                  >
+                    <template #prefix>Y</template>
+                  </input-number>
+                </div>
+              </el-form-item>
             </template>
 
             <!-- 锥形渐变:中心点 + 角度 -->
@@ -366,6 +388,7 @@ watch(
 )
 
 watch(type, (val) => {
+  ensureGradient()
   if (!internalGradient.value) return
   internalGradient.value.gradientType = val === 'gradient' ? 'gradient' : 'advanced'
   // 保留前2个渐变点
@@ -589,6 +612,33 @@ const centerY = computed({
   }
 })
 
+// 径向渐变:结束点
+const focalExtentX = computed({
+  get() {
+    ensureGradient()
+    return internalGradient.value?.focal_extent?.x ?? 50
+  },
+  set(val: number) {
+    ensureGradient()
+    if (!internalGradient.value) return
+    if (!internalGradient.value.focal_extent) internalGradient.value.focal_extent = { x: 50, y: 50 }
+    internalGradient.value.focal_extent.x = Math.max(0, val)
+  }
+})
+
+const focalExtentY = computed({
+  get() {
+    ensureGradient()
+    return internalGradient.value?.focal_extent?.y ?? 50
+  },
+  set(val: number) {
+    ensureGradient()
+    if (!internalGradient.value) return
+    if (!internalGradient.value.focal_extent) internalGradient.value.focal_extent = { x: 50, y: 50 }
+    internalGradient.value.focal_extent.y = Math.max(0, val)
+  }
+})
+
 // 锥形渐变:角度 0~3600
 const coneStartAngle = computed({
   get() {
@@ -666,6 +716,7 @@ const removePoint = (index: number) => {
   internalGradient.value.points.splice(index, 1)
 }
 
+// 高级渐变类型切换
 const onChangeAdvancedType = (val) => {
   ensureGradient()
   if (!internalGradient.value) return
@@ -676,8 +727,12 @@ const onChangeAdvancedType = (val) => {
     }
   } else {
     internalGradient.value.center = {
-      x: (props.width || 100) / 2,
-      y: (props.height || 100) / 2
+      x: Math.round((props.width || 100) / 2),
+      y: Math.round((props.height || 100) / 2)
+    }
+    internalGradient.value.focal_extent = {
+      x: Math.round((props.width || 100) / 2),
+      y: Math.round((props.height || 100) / 2)
     }
   }
 }

+ 21 - 5
src/renderer/src/components/InputNumber/index.vue

@@ -1,6 +1,6 @@
 <template>
   <el-input-number
-    :model-value="modelValue"
+    :model-value="displayValue"
     controls-position="right"
     v-bind="$attrs"
     @focus="onFocus"
@@ -17,10 +17,18 @@
 </template>
 
 <script setup lang="ts">
-import { useAttrs, ref } from 'vue'
+import { computed, useAttrs, ref } from 'vue'
 import { isNumber } from 'lodash-es'
 
 const modelValue = defineModel<number>('modelValue')
+const props = withDefaults(
+  defineProps<{
+    allowDecimal?: boolean
+  }>(),
+  {
+    allowDecimal: false
+  }
+)
 
 defineOptions({
   name: 'InputNumber'
@@ -28,6 +36,13 @@ defineOptions({
 
 const attrs = useAttrs()
 const beforeVal = ref<number>()
+const getValue = (value?: number | null) => {
+  if (value === undefined || value === null) {
+    return value
+  }
+  return props.allowDecimal ? value : Math.trunc(value)
+}
+const displayValue = computed(() => getValue(modelValue.value))
 
 const onFocus = () => {
   beforeVal.value = modelValue.value
@@ -44,13 +59,14 @@ const onChange = (value?: number) => {
   if (value === undefined || value === null) {
     return
   }
+  const normalizedValue = getValue(value)
   const { min, max } = attrs
-  if (isNumber(min) && value < min) {
+  if (isNumber(min) && normalizedValue! < min) {
     modelValue.value = min as number
-  } else if (isNumber(max) && value > max) {
+  } else if (isNumber(max) && normalizedValue! > max) {
     modelValue.value = max as number
   } else {
-    modelValue.value = value
+    modelValue.value = normalizedValue as number
   }
 }
 </script>

+ 6 - 6
src/renderer/src/lvgl-widgets/button/index.ts

@@ -197,6 +197,12 @@ export default {
     ],
     // 核心属性
     coreProps: [
+      {
+        label: '静态文本',
+        field: 'props.isStaticText',
+        valueType: 'switch',
+        labelWidth: '100px'
+      },
       {
         label: '文本',
         field: 'props.text',
@@ -235,12 +241,6 @@ export default {
             4: 'clip'
           }
         }
-      },
-      {
-        label: '静态文本',
-        field: 'props.isStaticText',
-        valueType: 'switch',
-        labelWidth: '100px'
       }
     ],
     // 组件样式

+ 6 - 6
src/renderer/src/lvgl-widgets/checkbox/index.ts

@@ -163,6 +163,12 @@ export default {
       }
     ],
     coreProps: [
+      {
+        label: '静态文本',
+        field: 'props.isStaticText',
+        valueType: 'switch',
+        labelWidth: '100px'
+      },
       {
         label: '文本',
         field: 'props.text',
@@ -172,12 +178,6 @@ export default {
           type: 'text'
         },
         canUseEventSet: true
-      },
-      {
-        label: '静态文本',
-        field: 'props.isStaticText',
-        valueType: 'switch',
-        labelWidth: '100px'
       }
     ],
     // 组件样式

+ 8 - 7
src/renderer/src/lvgl-widgets/dropdown/index.tsx

@@ -216,6 +216,12 @@ export default {
       }
     ],
     coreProps: [
+      {
+        label: '静态文本',
+        field: 'props.isStaticText',
+        valueType: 'switch',
+        labelWidth: '100px'
+      },
       {
         label: '展开方向',
         field: 'props.direction',
@@ -244,12 +250,6 @@ export default {
         render: (val) => {
           return <Config values={val} />
         }
-      },
-      {
-        label: '静态文本',
-        field: 'props.isStaticText',
-        valueType: 'switch',
-        labelWidth: '100px'
       }
     ],
     // 组件样式
@@ -373,7 +373,8 @@ export default {
                       field: 'background',
                       valueType: 'background',
                       componentProps: {
-                        onlyColor: true
+                        onlyColor: true,
+                        useType: part?.name === 'scrollbar' ? 'pure' : 'both'
                       }
                     },
                     {

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

@@ -304,10 +304,12 @@ export const useWidgetStyle = (param: StyleParam) => {
 
         // 获取背景图片src及颜色
         if (key === 'background' && resolvedValue?.image?.imgId) {
-          styleMap.value[`${partItem.name}Style`].imageSrc = getImageSrc(resolvedValue?.image?.imgId)
+          styleMap.value[`${partItem.name}Style`].imageSrc = getImageSrc(
+            resolvedValue?.image?.imgId
+          )
           styleMap.value[`${partItem.name}Style`].imageStyle = {
             backgroundColor: resolvedValue?.image?.recolor,
-            opacity: (resolvedValue?.image?.alpha || 255) / 255
+            opacity: (resolvedValue?.image?.alpha ?? 255) / 255
           }
         }
         // 图片样式

+ 6 - 6
src/renderer/src/lvgl-widgets/label/index.ts

@@ -201,6 +201,12 @@ export default {
       }
     ],
     coreProps: [
+      {
+        label: '静态文本',
+        field: 'props.isStaticText',
+        valueType: 'switch',
+        labelWidth: '100px'
+      },
       {
         label: '文本',
         field: 'props.text',
@@ -256,12 +262,6 @@ export default {
             }
           }
         ]
-      },
-      {
-        label: '静态文本',
-        field: 'props.isStaticText',
-        valueType: 'switch',
-        labelWidth: '100px'
       }
     ],
     // 组件样式

+ 2 - 1
src/renderer/src/lvgl-widgets/list/index.tsx

@@ -308,7 +308,8 @@ export default {
                     field: 'background',
                     valueType: 'background',
                     componentProps: {
-                      onlyColor: true
+                      onlyColor: true,
+                      useType: 'pure'
                     }
                   },
                   {

+ 6 - 6
src/renderer/src/lvgl-widgets/span-group/index.tsx

@@ -194,6 +194,12 @@ export default {
       }
     ],
     coreProps: [
+      {
+        label: '静态文本',
+        field: 'props.isStaticText',
+        valueType: 'switch',
+        labelWidth: '100px'
+      },
       {
         label: '模式',
         field: 'props.mode',
@@ -206,12 +212,6 @@ export default {
           ]
         }
       },
-      {
-        label: '静态文本',
-        field: 'props.isStaticText',
-        valueType: 'switch',
-        labelWidth: '100px'
-      },
       {
         label: '内容列表',
         field: 'props.items',

+ 4 - 1
src/renderer/src/lvgl-widgets/textarea/index.ts

@@ -308,7 +308,10 @@ export default {
                 {
                   label: '背景',
                   field: 'background',
-                  valueType: 'background'
+                  valueType: 'background',
+                  componentProps: {
+                    useType: 'pure'
+                  }
                 },
                 {
                   label: '边框',

+ 2 - 1
src/renderer/src/lvgl-widgets/tileview/index.tsx

@@ -264,7 +264,8 @@ export default {
                   field: 'background',
                   valueType: 'background',
                   componentProps: {
-                    onlyColor: true
+                    onlyColor: true,
+                    useType: part?.name === 'scrollbar' ? 'pure' : 'both'
                   }
                 },
                 {

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

@@ -170,6 +170,10 @@ export type GradientColor = {
     x: number
     y: number
   }
+  focal_extent?: {
+    x: number
+    y: number
+  }
   // 起始角度
   angle?: {
     start: number

+ 26 - 8
src/renderer/src/utils/color.ts

@@ -92,8 +92,7 @@ export function generateCssGradient(
   width?: number,
   height?: number
 ): string {
-  const { type, points, direction, angle, center } = config
-
+  const { type, points, direction, angle, center, gradientType, start, end, focal_extent } = config
   // 1. 处理颜色点部分
   const stops = (points || [])
     // 优先按照 LVGL 渐变位置(0~255)排序
@@ -112,7 +111,13 @@ export function generateCssGradient(
   // 2. 根据类型组装前缀和参数
   if (type === 'linear') {
     let dirParam = '180deg' // 默认从上到下
-    if (angle?.start !== undefined) {
+    if (gradientType === 'advanced' && start && end) {
+      const dx = end.x - start.x
+      const dy = end.y - start.y
+      if (dx !== 0 || dy !== 0) {
+        dirParam = `${(Math.atan2(dy, dx) * 180) / Math.PI + 90}deg`
+      }
+    } else if (angle?.start !== undefined) {
       dirParam = `${angle.start}deg`
     } else if (direction === 'horizontal') {
       dirParam = 'to right'
@@ -123,16 +128,29 @@ export function generateCssGradient(
   }
 
   if (type === 'radial') {
-    const cx = center?.x && width ? (center.x / width) * 100 : 50
-    const cy = center?.y && height ? (center.y / height) * 100 : 50
+    const cx = typeof center?.x === 'number' && width ? (center.x / width) * 100 : 50
+    const cy = typeof center?.y === 'number' && height ? (center.y / height) * 100 : 50
+
+    if (gradientType === 'advanced') {
+      const radiusX =
+        typeof focal_extent?.x === 'number'
+          ? Math.max(1, focal_extent.x)
+          : Math.max(1, Math.round((width || 100) / 2))
+      const radiusY =
+        typeof focal_extent?.y === 'number'
+          ? Math.max(1, focal_extent.y)
+          : Math.max(1, Math.round((height || 100) / 2))
+      return `radial-gradient(ellipse ${radiusX}px ${radiusY}px at ${cx}% ${cy}%, ${stops})`
+    }
+
     return `radial-gradient(circle at ${cx}% ${cy}%, ${stops})`
   }
 
   if (type === 'conical') {
     const startAngle = angle?.start ?? 0
-    const cx = center?.x && width ? (center.x / width) * 100 : 50
-    const cy = center?.y && height ? (center.y / height) * 100 : 50
-    return `conic-gradient(from ${startAngle}deg at ${cx}% ${cy}%, ${stops})`
+    const cx = typeof center?.x === 'number' && width ? (center.x / width) * 100 : 50
+    const cy = typeof center?.y === 'number' && height ? (center.y / height) * 100 : 50
+    return `conic-gradient(from ${startAngle + 90}deg at ${cx}% ${cy}%, ${stops})`
   }
 
   return ''

+ 8 - 4
src/renderer/src/views/designer/config/AnimationConfig.vue

@@ -61,33 +61,37 @@
                 <div class="flex flex-wrap gap-10px mb-10px pb-20px">
                   <input-number
                     v-model="animationItem.start"
+                    allow-decimal
                     controls-position="right"
                     placeholder="开始值"
-                    :step="0.1"
+                    :step="1"
                     :min="0"
                     style="flex: 0 0 calc(50% - 5px)"
                   />
                   <input-number
                     v-model="animationItem.end"
+                    allow-decimal
                     controls-position="right"
                     placeholder="结束值"
-                    :step="0.1"
+                    :step="1"
                     :min="0"
                     style="flex: 0 0 calc(50% - 5px)"
                   />
                   <input-number
                     v-model="animationItem.duration"
+                    allow-decimal
                     controls-position="right"
                     placeholder="动画时间"
-                    :step="0.1"
+                    :step="1"
                     :min="0"
                     style="flex: 0 0 calc(50% - 5px)"
                   />
                   <input-number
                     v-model="animationItem.delay"
+                    allow-decimal
                     controls-position="right"
                     placeholder="延迟时间"
-                    :step="0.1"
+                    :step="1"
                     :min="0"
                     style="flex: 0 0 calc(50% - 5px)"
                   />

+ 14 - 1
src/renderer/src/views/designer/config/property/components/StyleBorder.vue

@@ -61,7 +61,7 @@
           controls-position="right"
           style="width: 100%"
           :min="0"
-          :max="max"
+          :max="radiusMax"
         >
           <template #prefix>
             <span>圆角</span>
@@ -197,6 +197,19 @@ const max = computed(() => {
   return Math.min(width, height) / 2
 })
 
+// 最大值为控件宽高最小值的一半
+const radiusMax = computed(() => {
+  const activeWidget = projectStore.activeWidget
+  if (!activeWidget) return
+  const width = activeWidget.props.width || 0
+  const height = activeWidget.props.height || 0
+
+  if (!width || !height) {
+    return
+  }
+  return Math.min(width, height)
+})
+
 const handleBorder = (val: string) => {
   if (side.value?.includes(val)) {
     side.value = side.value.filter((item) => item !== val)

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

@@ -135,7 +135,7 @@ import { DEFAULT_THEME_KEY } from '@/constants'
 import { LuSliders } from 'vue-icons-plus/lu'
 import { IoColorPaletteOutline } from 'vue-icons-plus/io'
 import { klona } from 'klona'
-import { assign, get } from 'lodash-es'
+import { assign, get, has, set } from 'lodash-es'
 
 const projectStore = useProjectStore()
 const activeKeys = ref<string[]>(['props', 'style'])
@@ -339,24 +339,43 @@ const onChangeStateStyle = (field: string, type: 'add' | 'delete') => {
   }
 }
 
+const getStyleFields = (arr: Record<string, any>[]) => {
+  const keys = (arr || []).map((config) => {
+    if (config.valueType === 'dependency') {
+      return getStyleFields(config.dependency({ part: part.value }))
+    } else {
+      return config.field
+    }
+  })
+  return keys
+}
+
 /**
  * 开启/关闭控件部件状态样式
  * @param type
  */
 const onChangeStyleByState = (type: 'select' | 'cancel') => {
-  console.log('onChangeStyleByState', type)
   if (type === 'select') {
     const { defaultStyle, stateStyle } = getWidgetDefaultStyle()
-    // 找多对应样式时添加 状态的样式合并到默认样式
-    const result = klona({
-      ...assign({}, defaultStyle, stateStyle),
-      part: {
-        name: part.value.name,
-        state: part.value.state
-      },
-      theme: editTheme.value
-    })
-    projectStore.activeWidget?.style?.push(result)
+    const mergedStyle = assign({}, defaultStyle, stateStyle)
+    if (styleFormData.value) {
+      const fields = getStyleFields(formConfig.value.styles || []).flat(Infinity)
+      fields.forEach((field) => {
+        if (field !== 'part' && !has(styleFormData.value, field)) {
+          set(styleFormData.value!, field, get(mergedStyle, field))
+        }
+      })
+    } else {
+      const result = klona({
+        ...mergedStyle,
+        part: {
+          name: part.value.name,
+          state: part.value.state
+        },
+        theme: editTheme.value
+      })
+      projectStore.activeWidget?.style?.push(result)
+    }
   } else {
     // 删除样式
     deleteStyle()