jiaxing.liao 3 дней назад
Родитель
Сommit
836c080cf3

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

@@ -133,5 +133,6 @@
   "chart": "Chart",
   "canvas": "Canvas",
   "spinner": "Spinner",
-  "roller": "Roller"
+  "roller": "Roller",
+  "spinbox": "Spinbox"
 }

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

@@ -133,5 +133,6 @@
   "chart": "图表",
   "canvas": "画布",
   "spinner": "加载器",
-  "roller": "滚轮"
+  "roller": "滚轮",
+  "spinbox": "微调框"
 }

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

@@ -27,6 +27,7 @@ import Scale from './scale/index'
 import Led from './led/index'
 import Chart from './chart/index'
 import Canvas from './canvas/index'
+import Spinbox from './spinbox'
 import Roller from './roller'
 
 import Page from './page'
@@ -64,7 +65,8 @@ export const ComponentArray = [
   Chart,
   Canvas,
   Spinner,
-  Roller
+  Roller,
+  Spinbox
 ] as IComponentModelConfig[]
 
 const componentMap: { [key: string]: IComponentModelConfig } = ComponentArray.reduce((acc, cur) => {

+ 168 - 0
src/renderer/src/lvgl-widgets/spinbox/Spinbox.vue

@@ -0,0 +1,168 @@
+<template>
+  <div
+    class="spinbox-root relative flex h-full w-full items-center box-border"
+    :style="{ gap: `${buttonGap}px` }"
+  >
+    <div
+      class="relative box-border flex shrink-0 items-center justify-center overflow-hidden"
+      :style="buttonStyleMap.left"
+    >
+      <ImageBg
+        v-if="styleMap?.leftButtonStyle?.imageSrc"
+        :src="styleMap.leftButtonStyle.imageSrc"
+        :imageStyle="styleMap.leftButtonStyle.imageStyle"
+      />
+      <span class="relative z-1 whitespace-pre!">
+        <LuMinus size="16px" />
+      </span>
+    </div>
+
+    <div class="relative min-w-0 flex-1 overflow-hidden box-border" :style="styleMap?.mainStyle">
+      <ImageBg
+        v-if="styleMap?.mainStyle?.imageSrc"
+        :src="styleMap.mainStyle.imageSrc"
+        :imageStyle="styleMap.mainStyle.imageStyle"
+      />
+      <div
+        class="relative z-1 flex h-full w-full items-center"
+        :style="{ justifyContent: textJustify }"
+      >
+        <div class="inline-flex max-w-full items-center whitespace-pre!">
+          <span
+            v-for="(char, index) in displayChars"
+            :key="`${index}-${char}`"
+            class="inline-block"
+            :style="index === cursorIndex ? styleMap?.cursorStyle : undefined"
+          >
+            {{ char }}
+          </span>
+        </div>
+      </div>
+    </div>
+
+    <div
+      class="relative box-border flex shrink-0 items-center justify-center overflow-hidden"
+      :style="buttonStyleMap.right"
+    >
+      <ImageBg
+        v-if="styleMap?.rightButtonStyle?.imageSrc"
+        :src="styleMap.rightButtonStyle.imageSrc"
+        :imageStyle="styleMap.rightButtonStyle.imageStyle"
+      />
+      <span class="relative z-1 whitespace-pre!">
+        <LuPlus size="16px" />
+      </span>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { computed } from 'vue'
+import { useWidgetStyle } from '../hooks/useWidgetStyle'
+import ImageBg from '../ImageBg.vue'
+import { LuPlus, LuMinus } from 'vue-icons-plus/lu'
+
+const DEFAULT_HEIGHT = 40
+
+const props = defineProps<{
+  width: number
+  height: number
+  styles: any[]
+  state?: string
+  part?: string
+  rangeMin: number
+  rangeMax: number
+  step: number
+  integerDigits: number
+  decimalPlaces: number
+  value: number
+  rollOver: boolean
+}>()
+
+const styleMap = useWidgetStyle({
+  widget: 'lv_spinbox',
+  props
+})
+
+const buttonGap = computed(() => 6)
+
+const buttonSize = computed(() => {
+  return Math.max(24, props.height || DEFAULT_HEIGHT)
+})
+
+const buttonStyleMap = computed(() => {
+  const size = `${buttonSize.value}px`
+  return {
+    left: {
+      ...styleMap.value?.leftButtonStyle,
+      width: size,
+      height: size
+    },
+    right: {
+      ...styleMap.value?.rightButtonStyle,
+      width: size,
+      height: size
+    }
+  }
+})
+
+const normalizedRange = computed(() => {
+  return {
+    min: Math.min(props.rangeMin, props.rangeMax),
+    max: Math.max(props.rangeMin, props.rangeMax)
+  }
+})
+
+const normalizedValue = computed(() => {
+  const { min, max } = normalizedRange.value
+  const current = props.value ?? 0
+
+  if (props.rollOver) {
+    if (current < min) return max
+    if (current > max) return min
+    return current
+  }
+
+  return Math.max(min, Math.min(max, current))
+})
+
+const formattedValue = computed(() => {
+  const decimals = Math.max(0, Math.min(10, props.decimalPlaces ?? 0))
+  const integerDigits = Math.max(1, Math.min(5, props.integerDigits ?? 1))
+  const value = normalizedValue.value
+  const sign = value >= 0 ? '+' : '-'
+  const absolute = Math.abs(value)
+  const [integerPartRaw, fractionPart = ''] = absolute.toFixed(decimals).split('.')
+  const integerPart = integerPartRaw.padStart(integerDigits, '0')
+
+  return decimals > 0 ? `${sign}${integerPart}.${fractionPart}` : `${sign}${integerPart}`
+})
+
+const displayChars = computed(() => formattedValue.value.split(''))
+
+const cursorIndex = computed(() => {
+  for (let index = displayChars.value.length - 1; index >= 0; index -= 1) {
+    if (/\d/.test(displayChars.value[index])) {
+      return index
+    }
+  }
+  return 0
+})
+
+const textJustify = computed(() => {
+  switch (styleMap.value?.mainStyle?.textAlign) {
+    case 'center':
+      return 'center'
+    case 'right':
+      return 'flex-end'
+    default:
+      return 'flex-start'
+  }
+})
+</script>
+
+<style scoped>
+.spinbox-root {
+  user-select: none;
+}
+</style>

+ 362 - 0
src/renderer/src/lvgl-widgets/spinbox/index.ts

@@ -0,0 +1,362 @@
+import Spinbox from './Spinbox.vue'
+import icon from '../assets/icon/icon_28spinbox.svg'
+import { flagOptions, stateOptions, stateList } from '@/constants'
+import type { IComponentModelConfig } from '../type'
+import i18n from '@/locales'
+import defaultStyle from './style.json'
+
+export default {
+  label: i18n.global.t('spinbox'),
+  icon,
+  component: Spinbox,
+  key: 'lv_spinbox',
+  group: i18n.global.t('display'),
+  sort: 1,
+  hasChildren: false,
+  defaultStyle,
+  parts: [
+    {
+      name: 'main',
+      stateList
+    },
+    {
+      name: 'cursor',
+      stateList
+    },
+    {
+      name: 'leftButton',
+      stateList
+    },
+    {
+      name: 'rightButton',
+      stateList
+    }
+  ],
+  defaultSchema: {
+    name: 'spinbox_1',
+    props: {
+      x: 0,
+      y: 0,
+      width: 160,
+      height: 40,
+      flags: [
+        'LV_OBJ_FLAG_CLICKABLE',
+        'LV_OBJ_FLAG_CLICK_FOCUSABLE',
+        'LV_OBJ_FLAG_SCROLLABLE',
+        'LV_OBJ_FLAG_SCROLL_ELASTIC',
+        'LV_OBJ_FLAG_SCROLL_MOMENTUM',
+        'LV_OBJ_FLAG_SCROLL_CHAIN_HOR',
+        'LV_OBJ_FLAG_SCROLL_CHAIN_VER',
+        'LV_OBJ_FLAG_SCROLL_CHAIN',
+        'LV_OBJ_FLAG_SCROLL_ON_FOCUS',
+        'LV_OBJ_FLAG_SCROLL_WITH_ARROW',
+        'LV_OBJ_FLAG_SNAPPABLE',
+        'LV_OBJ_FLAG_PRESS_LOCK',
+        'LV_OBJ_FLAG_GESTURE_BUBBLE'
+      ],
+      states: [],
+      rangeMin: -100,
+      rangeMax: 100,
+      step: 1,
+      integerDigits: 3,
+      decimalPlaces: 2,
+      value: 0,
+      rollOver: false
+    },
+    styles: [
+      {
+        part: {
+          name: 'main',
+          state: 'default'
+        },
+        background: {
+          color: '#ffffffff',
+          image: {
+            imgId: '',
+            recolor: '#ffffff00',
+            alpha: 255
+          }
+        },
+        text: {
+          color: '#212121ff',
+          size: 16,
+          family: 'xx',
+          align: 'left',
+          decoration: 'none'
+        },
+        spacer: {
+          letterSpacing: 0
+        },
+        border: {
+          color: '#e0e0e0ff',
+          width: 2,
+          radius: 10,
+          side: ['all']
+        },
+        padding: {
+          top: 11,
+          right: 11,
+          bottom: 11,
+          left: 11
+        },
+        shadow: {
+          color: '#000000ff',
+          offsetX: 0,
+          offsetY: 0,
+          spread: 0,
+          width: 0
+        }
+      }
+    ]
+  },
+  config: {
+    props: [
+      {
+        label: '名称',
+        field: 'name',
+        valueType: 'text',
+        componentProps: {
+          placeholder: '请输入名称',
+          type: 'text'
+        }
+      },
+      {
+        label: '位置/大小',
+        valueType: 'group',
+        children: [
+          {
+            label: '',
+            field: 'props.x',
+            valueType: 'number',
+            componentProps: {
+              span: 12,
+              min: -10000,
+              max: 10000
+            },
+            slots: { prefix: 'X' }
+          },
+          {
+            label: '',
+            field: 'props.y',
+            valueType: 'number',
+            componentProps: {
+              span: 12,
+              min: -10000,
+              max: 10000
+            },
+            slots: { prefix: 'Y' }
+          },
+          {
+            label: '',
+            field: 'props.width',
+            valueType: 'number',
+            componentProps: {
+              span: 12,
+              min: 1,
+              max: 10000
+            },
+            slots: { prefix: 'W' }
+          },
+          {
+            label: '',
+            field: 'props.height',
+            valueType: 'number',
+            componentProps: {
+              span: 12,
+              min: 1,
+              max: 10000
+            },
+            slots: { prefix: 'H' }
+          }
+        ]
+      },
+      {
+        label: '标识',
+        field: 'props.flags',
+        valueType: 'checkbox',
+        componentProps: {
+          options: flagOptions,
+          defaultCollapsed: true
+        }
+      },
+      {
+        label: '状态',
+        field: 'props.states',
+        valueType: 'checkbox',
+        componentProps: {
+          options: stateOptions,
+          defaultCollapsed: true
+        }
+      }
+    ],
+    coreProps: [
+      {
+        label: '范围',
+        valueType: 'group',
+        children: [
+          {
+            label: '',
+            field: 'props.rangeMin',
+            valueType: 'number',
+            componentProps: {
+              span: 12,
+              min: -99999,
+              max: 99999
+            },
+            slots: { prefix: 'min' }
+          },
+          {
+            label: '',
+            field: 'props.rangeMax',
+            valueType: 'number',
+            componentProps: {
+              span: 12,
+              min: -99999,
+              max: 99999
+            },
+            slots: { prefix: 'max' }
+          }
+        ]
+      },
+      {
+        label: '步长',
+        field: 'props.step',
+        valueType: 'number',
+        componentProps: {
+          min: 1,
+          max: 1000
+        }
+      },
+      {
+        label: '数位',
+        valueType: 'group',
+        children: [
+          {
+            label: '',
+            field: 'props.integerDigits',
+            valueType: 'number',
+            componentProps: {
+              span: 12,
+              min: 1,
+              max: 5
+            },
+            slots: { prefix: '' }
+          },
+          {
+            label: '',
+            field: 'props.decimalPlaces',
+            valueType: 'number',
+            componentProps: {
+              span: 12,
+              min: 0,
+              max: 10
+            },
+            slots: { prefix: '.' }
+          }
+        ]
+      },
+      {
+        label: '值',
+        field: 'props.value',
+        valueType: 'number',
+        componentProps: {
+          min: -99999,
+          max: 99999
+        }
+      },
+      {
+        label: '值循环',
+        field: 'props.rollOver',
+        valueType: 'switch'
+      }
+    ],
+    styles: [
+      {
+        label: '模块状态',
+        field: 'part',
+        valueType: 'part'
+      },
+      {
+        valueType: 'dependency',
+        name: ['part'],
+        dependency: ({ part }) => {
+          if (part?.name === 'main') {
+            return [
+              {
+                label: '背景',
+                field: 'background',
+                valueType: 'background'
+              },
+              {
+                label: '字体',
+                field: 'text',
+                valueType: 'font'
+              },
+              {
+                label: '间距',
+                field: 'spacer',
+                valueType: 'spacer'
+              },
+              {
+                label: '边框',
+                field: 'border',
+                valueType: 'border'
+              },
+              {
+                label: '内边距',
+                field: 'padding',
+                valueType: 'padding'
+              },
+              {
+                label: '阴影',
+                field: 'shadow',
+                valueType: 'shadow'
+              }
+            ]
+          }
+
+          if (part?.name === 'cursor') {
+            return [
+              {
+                label: '背景',
+                field: 'background',
+                valueType: 'background',
+                componentProps: {
+                  onlyColor: true
+                }
+              },
+              {
+                label: '字体',
+                field: 'text',
+                valueType: 'font'
+              }
+            ]
+          }
+
+          return [
+            {
+              label: '背景',
+              field: 'background',
+              valueType: 'background'
+            },
+            {
+              label: '字体',
+              field: 'text',
+              valueType: 'font'
+            },
+            {
+              label: '边框',
+              field: 'border',
+              valueType: 'border'
+            },
+            {
+              label: '阴影',
+              field: 'shadow',
+              valueType: 'shadow'
+            }
+          ]
+        }
+      }
+    ]
+  }
+} as IComponentModelConfig

+ 133 - 0
src/renderer/src/lvgl-widgets/spinbox/style.json

@@ -0,0 +1,133 @@
+{
+  "widget": "lv_spinbox",
+  "styleName": "defualt",
+  "part": [
+    {
+      "partName": "main",
+      "defaultStyle": {
+        "background": {
+          "color": "#ffffffff",
+          "image": {
+            "imgId": "",
+            "recolor": "#ffffff00",
+            "alpha": 255
+          }
+        },
+        "text": {
+          "color": "#212121ff",
+          "size": 16,
+          "family": "xx",
+          "align": "left",
+          "decoration": "none"
+        },
+        "spacer": {
+          "letterSpacing": 0
+        },
+        "border": {
+          "color": "#e0e0e0ff",
+          "width": 2,
+          "radius": 10,
+          "side": ["all"]
+        },
+        "padding": {
+          "top": 11,
+          "right": 11,
+          "bottom": 11,
+          "left": 11
+        },
+        "shadow": {
+          "color": "#000000ff",
+          "offsetX": 0,
+          "offsetY": 0,
+          "spread": 0,
+          "width": 0
+        }
+      },
+      "state": []
+    },
+    {
+      "partName": "cursor",
+      "defaultStyle": {
+        "background": {
+          "color": "#2196f3ff"
+        },
+        "text": {
+          "color": "#ffffffff",
+          "size": 16,
+          "family": "xx",
+          "align": "left",
+          "decoration": "none"
+        }
+      },
+      "state": []
+    },
+    {
+      "partName": "leftButton",
+      "defaultStyle": {
+        "background": {
+          "color": "#2196f3ff",
+          "image": {
+            "imgId": "",
+            "recolor": "#ffffff00",
+            "alpha": 255
+          }
+        },
+        "text": {
+          "color": "#ffffffff",
+          "size": 16,
+          "family": "xx",
+          "align": "center",
+          "decoration": "none"
+        },
+        "border": {
+          "color": "#000000ff",
+          "width": 0,
+          "radius": 13,
+          "side": ["all"]
+        },
+        "shadow": {
+          "color": "#9e9e9e7f",
+          "offsetX": 0,
+          "offsetY": 2,
+          "spread": 0,
+          "width": 2
+        }
+      },
+      "state": []
+    },
+    {
+      "partName": "rightButton",
+      "defaultStyle": {
+        "background": {
+          "color": "#2196f3ff",
+          "image": {
+            "imgId": "",
+            "recolor": "#ffffff00",
+            "alpha": 255
+          }
+        },
+        "text": {
+          "color": "#ffffffff",
+          "size": 16,
+          "family": "xx",
+          "align": "center",
+          "decoration": "none"
+        },
+        "border": {
+          "color": "#000000ff",
+          "width": 0,
+          "radius": 13,
+          "side": ["all"]
+        },
+        "shadow": {
+          "color": "#9e9e9e7f",
+          "offsetX": 0,
+          "offsetY": 2,
+          "spread": 0,
+          "width": 2
+        }
+      },
+      "state": []
+    }
+  ]
+}