Explorar el Código

feat: 添加arc控件

jiaxing.liao hace 2 semanas
padre
commit
dcf4a781ad

+ 2 - 1
src/renderer/src/locales/en_US.json

@@ -106,5 +106,6 @@
   "window": "Window",
   "menu": "Menu",
   "line": "Line",
-  "display": "Display"
+  "display": "Display",
+  "arc": "Arc"
 }

+ 2 - 1
src/renderer/src/locales/zh_CN.json

@@ -106,5 +106,6 @@
   "window": "窗口",
   "menu": "菜单",
   "line": "线条",
-  "display": "显示"
+  "display": "显示",
+  "arc": "圆弧"
 }

+ 182 - 0
src/renderer/src/lvgl-widgets/arc/Arc.vue

@@ -0,0 +1,182 @@
+<template>
+  <div
+    :style="{
+      ...styleMap?.mainStyle
+    }"
+    class="box-border overflow-hidden relative"
+  >
+    <svg
+      :viewBox="`0 0 ${width} ${height}`"
+      preserveAspectRatio="xMinYMin meet"
+      class="w-full h-full"
+      xmlns="http://www.w3.org/2000/svg"
+      :style="{ transform: `rotate(${props.rotate}deg)` }"
+    >
+      <!-- 背景条:绘制完整的起始到结束角度 -->
+      <path
+        :d="bgPath"
+        fill="none"
+        :stroke="styles?.mainStyle?.curve?.color || '#eeeeee'"
+        :stroke-width="styles?.mainStyle?.curve?.width || 1"
+        :stroke-linecap="styles?.mainStyle?.curve?.radius ? 'round' : 'butt'"
+      />
+
+      <!-- 进度值条 -->
+      <path
+        :d="valuePath"
+        fill="none"
+        :stroke="styles?.indicatorStyle?.curve?.color || '#eeeeee'"
+        :stroke-width="styles?.indicatorStyle?.curve?.width || 1"
+        :stroke-linecap="styles?.indicatorStyle?.curve?.radius ? 'round' : 'butt'"
+      />
+
+      <!-- 进度圆点 -->
+      <circle
+        v-if="dotPos"
+        :cx="dotPos.x"
+        :cy="dotPos.y"
+        :r="styles?.knobStyle?.padding?.left || 5"
+        :fill="styles?.knobStyle?.backgroundColor || '#2092f5'"
+      />
+    </svg>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { computed } from 'vue'
+import { useWidgetStyle } from '../hooks/useWidgetStyle'
+
+const props = defineProps<{
+  width: number
+  height: number
+  styles: any
+  state?: string
+  part?: string
+  mode: 'normal' | 'symmetrical' | 'reverse'
+  rangeStart: number
+  rangeEnd: number
+  angleStart: number
+  angleEnd: number
+  value: number
+  rotate: number
+}>()
+
+const styleMap = useWidgetStyle({
+  widget: 'lv_arc',
+  props
+})
+
+const cx = computed(() => {
+  const { width, height } = props
+  const min = Math.min(width, height)
+  return min / 2
+})
+
+/**
+ * 极坐标转直角坐标
+ * SVG 角度习惯:0度在右侧(3点钟),需调整使0度在上方(12点钟)
+ */
+function polarToCartesian(
+  centerX: number,
+  centerY: number,
+  radius: number,
+  angleInDegrees: number
+) {
+  // LVGL 习惯通常 0 度在右侧,如果需要 0 度在上方,这里减去 90
+  // 这里我们遵循标准:angleStart 为输入值
+  const radians = ((angleInDegrees - 0) * Math.PI) / 180.0
+  return {
+    x: centerX + radius * Math.cos(radians),
+    y: centerY + radius * Math.sin(radians)
+  }
+}
+
+/**
+ * 生成 SVG 弧线路径指令
+ */
+function describeArc(x: number, y: number, radius: number, startAngle: number, endAngle: number) {
+  // 如果起始角度大于结束角度,交换位置(取决于业务逻辑,这里默认顺时针绘制)
+  const isReversed = startAngle > endAngle
+  const start = polarToCartesian(x, y, radius, isReversed ? endAngle : startAngle)
+  const end = polarToCartesian(x, y, radius, isReversed ? startAngle : endAngle)
+
+  const diff = Math.abs(endAngle - startAngle)
+  const largeArcFlag = diff <= 180 ? '0' : '1'
+  // sweep-flag: 1 为顺时针
+  const sweepFlag = isReversed ? '0' : '1'
+
+  return [
+    'M',
+    start.x,
+    start.y,
+    'A',
+    radius,
+    radius,
+    0,
+    largeArcFlag,
+    sweepFlag,
+    end.x,
+    end.y
+  ].join(' ')
+}
+
+// 1. 背景路径
+const bgPath = computed(() => {
+  const { width, height } = props
+  const min = Math.min(width, height)
+  const r = min / 2
+  return describeArc(r, r, r - 10, props.angleStart, props.angleEnd)
+})
+
+// 2. 进度计算逻辑
+const progressData = computed(() => {
+  const { value, rangeStart, rangeEnd, angleStart, angleEnd, mode } = props
+
+  // 基础百分比计算 (0-1)
+  let ratio = (value - rangeStart) / (rangeEnd - rangeStart)
+  ratio = Math.max(0, Math.min(1, ratio)) // 限制在 0-1
+
+  let startA = angleStart
+  let endA = angleEnd
+
+  if (mode === 'normal') {
+    endA = angleStart + (angleEnd - angleStart) * ratio
+  } else if (mode === 'reverse') {
+    // 从终点往回走
+    startA = angleEnd - (angleEnd - angleStart) * ratio
+    endA = angleEnd
+  } else if (mode === 'symmetrical') {
+    // 对称模式:通常 rangeStart 到 rangeEnd 的中间值是起点
+    const midAngle = (angleStart + angleEnd) / 2
+    const midValue = (rangeStart + rangeEnd) / 2
+
+    if (value >= midValue) {
+      startA = midAngle
+      const halfRatio = (value - midValue) / (rangeEnd - midValue)
+      endA = midAngle + (angleEnd - midAngle) * halfRatio
+    } else {
+      endA = midAngle
+      const halfRatio = (midValue - value) / (midValue - rangeStart)
+      startA = midAngle - (midAngle - angleStart) * halfRatio
+    }
+  }
+
+  return { startA, endA }
+})
+
+// 3. 值路径
+const valuePath = computed(() => {
+  const { startA, endA } = progressData.value
+  if (startA === endA) return ''
+  return describeArc(cx.value, cx.value, cx.value, startA, endA)
+})
+
+// 4. 圆点位置
+const dotPos = computed(() => {
+  // const { endA } = progressData.value
+  // 如果是 reverse 模式,圆点可能应该在 startA(取决于视觉习惯)
+  // 这里默认跟随进度的“活动端”
+  const angle = props.mode === 'reverse' ? progressData.value.startA : progressData.value.endA
+  return polarToCartesian(cx.value, cx.value, cx.value, angle)
+})
+</script>

+ 330 - 0
src/renderer/src/lvgl-widgets/arc/index.ts

@@ -0,0 +1,330 @@
+import Arc from './Arc.vue'
+import icon from '../assets/icon/icon_21circle.svg'
+import type { IComponentModelConfig } from '../type'
+import i18n from '@/locales'
+import { flagOptions } from '@/constants'
+import defaultStyle from './style.json'
+
+export default {
+  label: i18n.global.t('arc'),
+  icon,
+  component: Arc,
+  key: 'lv_arc',
+  group: i18n.global.t('display'),
+  sort: 1,
+  defaultStyle,
+  parts: [
+    {
+      name: 'main',
+      stateList: ['default', 'focused', 'disabled']
+    },
+    {
+      name: 'indicator',
+      stateList: ['default', 'focused', 'disabled']
+    },
+    {
+      name: 'knob',
+      stateList: ['default', 'focused', 'disabled']
+    }
+  ],
+  defaultSchema: {
+    name: 'arc',
+    props: {
+      x: 0,
+      y: 0,
+      width: 120,
+      height: 120,
+      addFlags: [],
+      removeFlags: [],
+      mode: 'normal',
+      rangeStart: 0,
+      rangeEnd: 100,
+      angleStart: 135,
+      angleEnd: 45,
+      value: 70,
+      rotate: 0
+    },
+    styles: [
+      {
+        part: {
+          name: 'main',
+          state: 'default'
+        },
+        background: {
+          color: '#ffffffff'
+        },
+        border: {
+          color: '#2092f5ff',
+          width: 0,
+          radius: 6,
+          side: ['all']
+        },
+        padding: {
+          top: 20,
+          right: 20,
+          bottom: 20,
+          left: 20
+        },
+        curve: {
+          color: '#eeeeeeff',
+          width: 12,
+          radius: true
+        },
+        shadow: {
+          color: '#2092f5ff',
+          x: 0,
+          y: 0,
+          spread: 0,
+          width: 0
+        }
+      },
+      {
+        part: {
+          name: 'indicator',
+          state: 'default'
+        },
+        curve: {
+          color: '#2092f5ff',
+          width: 12,
+          radius: true
+        }
+      },
+      {
+        part: {
+          name: 'knob',
+          state: 'default'
+        },
+        background: {
+          color: '#2092f5ff'
+        },
+        padding: {
+          left: 8,
+          right: 8,
+          top: 8,
+          bottom: 8
+        }
+      }
+    ]
+  },
+  config: {
+    // 组件属性
+    props: [
+      {
+        label: '名称',
+        field: 'name',
+        valueType: 'text',
+        componentProps: {
+          placeholder: '请输入名称'
+        }
+      },
+      {
+        label: '位置/大小',
+        valueType: 'group',
+        children: [
+          {
+            label: '',
+            field: 'props.x',
+            valueType: 'number',
+            componentProps: {
+              span: 12
+            },
+            slots: { prefix: 'X' }
+          },
+          {
+            label: '',
+            field: 'props.y',
+            valueType: 'number',
+            componentProps: {
+              span: 12
+            },
+            slots: { prefix: 'Y' }
+          },
+          {
+            label: '',
+            field: 'props.width',
+            valueType: 'number',
+            componentProps: {
+              span: 12
+            },
+            slots: { prefix: 'W' }
+          },
+          {
+            label: '',
+            field: 'props.height',
+            valueType: 'number',
+            componentProps: {
+              span: 12
+            },
+            slots: { prefix: 'H' }
+          }
+        ]
+      },
+      {
+        label: '添加标识',
+        field: 'props.addFlags',
+        valueType: 'checkbox',
+        componentProps: {
+          options: flagOptions,
+          defaultCollapsed: true
+        }
+      },
+      {
+        label: '删除标识',
+        field: 'props.removeFlags',
+        valueType: 'checkbox',
+        componentProps: {
+          options: flagOptions,
+          defaultCollapsed: true
+        }
+      }
+    ],
+    coreProps: [
+      {
+        label: '模式',
+        field: 'props.mode',
+        valueType: 'select',
+        componentProps: {
+          options: [
+            { label: 'normal', value: 'normal' },
+            { label: 'symmetrical', value: 'symmetrical' },
+            { label: 'reverse', value: 'reverse' }
+          ]
+        }
+      },
+      {
+        label: '范围',
+        valueType: 'group',
+        children: [
+          {
+            field: 'props.rangeStart',
+            valueType: 'number',
+            componentProps: {
+              min: -100000,
+              max: 100000
+            },
+            slots: {
+              prefix: 'S'
+            }
+          },
+          {
+            field: 'props.rangeEnd',
+            valueType: 'number',
+            componentProps: {
+              min: -100000,
+              max: 100000
+            },
+            slots: {
+              prefix: 'E'
+            }
+          }
+        ]
+      },
+      {
+        label: '角度',
+        valueType: 'group',
+        children: [
+          {
+            field: 'props.angleStart',
+            valueType: 'number',
+            componentProps: {
+              min: 0,
+              max: 360
+            },
+            slots: {
+              prefix: 'S'
+            }
+          },
+          {
+            field: 'props.angleEnd',
+            valueType: 'number',
+            componentProps: {
+              min: 0,
+              max: 360
+            },
+            slots: {
+              prefix: 'E'
+            }
+          }
+        ]
+      },
+      {
+        label: '值',
+        field: 'props.value',
+        valueType: 'number'
+      },
+      {
+        label: '旋转',
+        field: 'props.rotate',
+        valueType: 'number'
+      }
+    ],
+    // 组件样式
+    styles: [
+      {
+        label: '模块状态',
+        field: 'part',
+        valueType: 'part'
+      },
+      {
+        valueType: 'dependency',
+        name: ['part'],
+        dependency: ({ part }) => {
+          return part?.name === 'main'
+            ? [
+                {
+                  label: '背景',
+                  field: 'background',
+                  ValueType: 'background',
+                  componentProps: {
+                    onlyColor: true
+                  }
+                },
+                {
+                  label: '边框',
+                  field: 'border',
+                  valueType: 'border'
+                },
+                {
+                  label: '内边距',
+                  field: 'padding',
+                  valueType: 'padding'
+                },
+                {
+                  label: '曲线',
+                  field: 'curve',
+                  valueType: 'line'
+                },
+                {
+                  label: '阴影',
+                  field: 'shadow',
+                  valueType: 'shadow'
+                }
+              ]
+            : part?.name === 'indicator'
+              ? [
+                  {
+                    label: '曲线',
+                    field: 'curve',
+                    valueType: 'line'
+                  }
+                ]
+              : [
+                  {
+                    label: '背景',
+                    field: 'background',
+                    ValueType: 'background',
+                    componentProps: {
+                      onlyColor: true
+                    }
+                  },
+                  {
+                    label: '内边距',
+                    field: 'padding',
+                    valueType: 'padding'
+                  }
+                ]
+        }
+      }
+    ]
+  }
+} as IComponentModelConfig

+ 189 - 0
src/renderer/src/lvgl-widgets/arc/style.json

@@ -0,0 +1,189 @@
+{
+  "widget": "lv_arc",
+  "styleName": "defualt",
+  "part": [
+    {
+      "partName": "main",
+      "state": [
+        {
+          "state": "default",
+          "style": {
+            "background": {
+              "color": "#ffffffff"
+            },
+            "border": {
+              "color": "#2092f5ff",
+              "width": 0,
+              "radius": 6,
+              "side": ["all"]
+            },
+            "padding": {
+              "top": 20,
+              "right": 20,
+              "bottom": 20,
+              "left": 20
+            },
+            "curve": {
+              "color": "#eeeeeeff",
+              "width": 12,
+              "radius": true
+            },
+            "shadow": {
+              "color": "#2092f5ff",
+              "x": 0,
+              "y": 0,
+              "spread": 0,
+              "width": 0
+            }
+          }
+        },
+        {
+          "state": "focused",
+          "style": {
+            "background": {
+              "color": "#ffffffff"
+            },
+            "border": {
+              "color": "#2092f5ff",
+              "width": 0,
+              "radius": 6,
+              "side": ["all"]
+            },
+            "padding": {
+              "top": 20,
+              "right": 20,
+              "bottom": 20,
+              "left": 20
+            },
+            "curve": {
+              "color": "#eeeeeeff",
+              "width": 12,
+              "radius": true
+            },
+            "shadow": {
+              "color": "#2092f5ff",
+              "x": 0,
+              "y": 0,
+              "spread": 0,
+              "width": 0
+            }
+          }
+        },
+        {
+          "state": "disabled",
+          "style": {
+            "background": {
+              "color": "#ffffffff"
+            },
+            "border": {
+              "color": "#2092f5ff",
+              "width": 0,
+              "radius": 6,
+              "side": ["all"]
+            },
+            "padding": {
+              "top": 20,
+              "right": 20,
+              "bottom": 20,
+              "left": 20
+            },
+            "curve": {
+              "color": "#eeeeeeff",
+              "width": 12,
+              "radius": true
+            },
+            "shadow": {
+              "color": "#2092f5ff",
+              "x": 0,
+              "y": 0,
+              "spread": 0,
+              "width": 0
+            }
+          }
+        }
+      ]
+    },
+    {
+      "partName": "indicator",
+      "state": [
+        {
+          "state": "default",
+          "style": {
+            "curve": {
+              "color": "#2092f5ff",
+              "width": 12,
+              "radius": true
+            }
+          }
+        },
+        {
+          "state": "focused",
+          "style": {
+            "curve": {
+              "color": "#2092f5ff",
+              "width": 12,
+              "radius": true
+            }
+          }
+        },
+        {
+          "state": "disabled",
+          "style": {
+            "curve": {
+              "color": "#2092f5ff",
+              "width": 12,
+              "radius": true
+            }
+          }
+        }
+      ]
+    },
+    {
+      "partName": "knob",
+      "state": [
+        {
+          "state": "default",
+          "style": {
+            "background": {
+              "color": "#2092f5ff"
+            },
+            "padding": {
+              "left": 8,
+              "right": 8,
+              "top": 8,
+              "bottom": 8
+            }
+          }
+        },
+        {
+          "state": "focused",
+          "style": {
+            "background": {
+              "color": "#2092f5ff"
+            },
+            "padding": {
+              "left": 8,
+              "right": 8,
+              "top": 8,
+              "bottom": 8
+            }
+          }
+        },
+        {
+          "state": "disabled",
+          "style": {
+            "background": {
+              "color": "#2092f5ff"
+            },
+            "padding": {
+              "left": 8,
+              "right": 8,
+              "top": 8,
+              "bottom": 8
+            }
+          }
+        }
+      ]
+    }
+  ]
+}

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

@@ -23,6 +23,7 @@ type StyleMap = Record<
     imageSrc?: string
     imageColorStyle?: CSSProperties
     line?: { color: string; width: number; radius: boolean }
+    curve?: { color: string; width: number; radius: boolean }
   }
 >
 
@@ -181,6 +182,9 @@ export const useWidgetStyle = (param: StyleParam) => {
         if (key === 'line') {
           styleMap.value[`${partItem.name}Style`].line = style?.[key]
         }
+        if (key === 'curve') {
+          styleMap.value[`${partItem.name}Style`].curve = style?.[key]
+        }
       })
       // 处理行高 默认行高为1.2倍
       if (style?.spacer?.lineHeight && style?.text?.size) {

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

@@ -21,6 +21,7 @@ import Window from './window/index'
 import Menu from './menu/index'
 
 import Line from './line/index'
+// import Arc from './arc'
 
 import Page from './page'
 import { IComponentModelConfig } from './type'
@@ -51,6 +52,7 @@ export const ComponentArray = [
   Menu,
 
   Line
+  // Arc
 ]
 
 const componentMap: { [key: string]: IComponentModelConfig } = ComponentArray.reduce((acc, cur) => {