Browse Source

fix: 修复文档问题

jiaxing.liao 2 weeks ago
parent
commit
a293cc2f8e
32 changed files with 931 additions and 627 deletions
  1. 135 33
      src/renderer/src/components/ColorModal/index.vue
  2. 1 3
      src/renderer/src/hooks/useHistory.ts
  3. 1 0
      src/renderer/src/locales/en_US.json
  4. 1 0
      src/renderer/src/locales/zh_CN.json
  5. 2 2
      src/renderer/src/lvgl-widgets/button/index.ts
  6. 17 16
      src/renderer/src/lvgl-widgets/dropdown/Dropdown.vue
  7. 12 105
      src/renderer/src/lvgl-widgets/dropdown/index.tsx
  8. 4 4
      src/renderer/src/lvgl-widgets/image/Image.vue
  9. 6 6
      src/renderer/src/lvgl-widgets/image/index.ts
  10. 3 1
      src/renderer/src/lvgl-widgets/index.ts
  11. 1 1
      src/renderer/src/lvgl-widgets/label/index.ts
  12. 0 6
      src/renderer/src/lvgl-widgets/list/List.vue
  13. 0 6
      src/renderer/src/lvgl-widgets/message/MessageBox.vue
  14. 131 0
      src/renderer/src/lvgl-widgets/scale/Config.vue
  15. 318 205
      src/renderer/src/lvgl-widgets/scale/Scale.vue
  16. 117 134
      src/renderer/src/lvgl-widgets/scale/index.ts
  17. 29 35
      src/renderer/src/lvgl-widgets/scale/style.json
  18. 0 6
      src/renderer/src/lvgl-widgets/span-group/SpanGroup.vue
  19. 0 6
      src/renderer/src/lvgl-widgets/switch/Switch.vue
  20. 6 9
      src/renderer/src/lvgl-widgets/switch/index.ts
  21. 0 6
      src/renderer/src/lvgl-widgets/tabview/Tabview.vue
  22. 3 8
      src/renderer/src/lvgl-widgets/textarea/Textarea.vue
  23. 27 7
      src/renderer/src/lvgl-widgets/textarea/index.ts
  24. 0 6
      src/renderer/src/lvgl-widgets/window/Window.vue
  25. 2 0
      src/renderer/src/store/modules/project.ts
  26. 19 2
      src/renderer/src/views/designer/config/property/CusFormItem.vue
  27. 13 13
      src/renderer/src/views/designer/config/property/components/StyleBackground.vue
  28. 1 1
      src/renderer/src/views/designer/config/property/components/StyleBorder.vue
  29. 45 0
      src/renderer/src/views/designer/config/property/components/StyleOther.vue
  30. 1 1
      src/renderer/src/views/designer/config/property/components/StyleOutline.vue
  31. 32 0
      src/renderer/src/views/designer/config/property/components/SymbolSelect.vue
  32. 4 5
      src/renderer/src/views/designer/workspace/stage/Node.vue

+ 135 - 33
src/renderer/src/components/ColorModal/index.vue

@@ -11,7 +11,13 @@
 
         <!-- 纯色 -->
         <div v-if="type === 'pure'" class="mt-20px">
-          <ColorPicker v-model:pureColor="pureColor" format="hex8" picker-type="chrome" use-type="pure" isWidget />
+          <ColorPicker
+            v-model:pureColor="pureColor"
+            format="hex8"
+            picker-type="chrome"
+            use-type="pure"
+            isWidget
+          />
         </div>
 
         <!-- 基础渐变 -->
@@ -26,7 +32,12 @@
 
             <el-form-item label="开始颜色">
               <div class="flex items-center gap-12px w-full">
-                <ColorPicker v-model:pureColor="basicStartColor" format="hex8" picker-type="chrome" use-type="pure" />
+                <ColorPicker
+                  v-model:pureColor="basicStartColor"
+                  format="hex8"
+                  picker-type="chrome"
+                  use-type="pure"
+                />
                 <div>
                   {{ basicStartColor }}
                 </div>
@@ -35,7 +46,12 @@
 
             <el-form-item label="结束颜色">
               <div class="flex items-center gap-12px w-full">
-                <ColorPicker v-model:pureColor="basicEndColor" format="hex8" picker-type="chrome" use-type="pure" />
+                <ColorPicker
+                  v-model:pureColor="basicEndColor"
+                  format="hex8"
+                  picker-type="chrome"
+                  use-type="pure"
+                />
                 <div>
                   {{ basicEndColor }}
                 </div>
@@ -77,20 +93,44 @@
             <template v-if="advancedType === 'linear'">
               <el-form-item label="开始坐标">
                 <div class="w-full flex items-center gap-8px">
-                  <input-number v-model="linearStartX" :min="0" :max="width" size="small" style="flex: 1;">
+                  <input-number
+                    v-model="linearStartX"
+                    :min="0"
+                    :max="width"
+                    size="small"
+                    style="flex: 1"
+                  >
                     <template #prefix>X</template>
                   </input-number>
-                  <input-number v-model="linearStartY" :min="0" :max="height" size="small" style="flex: 1;">
+                  <input-number
+                    v-model="linearStartY"
+                    :min="0"
+                    :max="height"
+                    size="small"
+                    style="flex: 1"
+                  >
                     <template #prefix>Y</template>
                   </input-number>
                 </div>
               </el-form-item>
               <el-form-item label="结束坐标">
                 <div class="w-full flex items-center gap-8px">
-                  <input-number v-model="linearEndX" :min="0" :max="width" size="small" style="flex: 1;">
+                  <input-number
+                    v-model="linearEndX"
+                    :min="0"
+                    :max="width"
+                    size="small"
+                    style="flex: 1"
+                  >
                     <template #prefix>X</template>
                   </input-number>
-                  <input-number v-model="linearEndY" :min="0" :max="height" size="small" style="flex: 1;">
+                  <input-number
+                    v-model="linearEndY"
+                    :min="0"
+                    :max="height"
+                    size="small"
+                    style="flex: 1"
+                  >
                     <template #prefix>Y</template>
                   </input-number>
                 </div>
@@ -101,10 +141,22 @@
             <template v-else-if="advancedType === 'radial'">
               <el-form-item label="中心点坐标">
                 <div class="w-full flex items-center gap-8px">
-                  <input-number v-model="centerX" :min="0" :max="width" size="small" style="flex: 1;">
+                  <input-number
+                    v-model="centerX"
+                    :min="0"
+                    :max="width"
+                    size="small"
+                    style="flex: 1"
+                  >
                     <template #prefix>X</template>
                   </input-number>
-                  <input-number v-model="centerY" :min="0" :max="height" size="small" style="flex: 1;">
+                  <input-number
+                    v-model="centerY"
+                    :min="0"
+                    :max="height"
+                    size="small"
+                    style="flex: 1"
+                  >
                     <template #prefix>Y</template>
                   </input-number>
                 </div>
@@ -115,16 +167,40 @@
             <template v-else>
               <el-form-item label="中心点坐标">
                 <div class="w-full flex items-center gap-8px">
-                  <input-number v-model="centerX" :min="0" :max="width" size="small" style="flex: 1;" />
-                  <input-number v-model="centerY" :min="0" :max="height" size="small" style="flex: 1;" />
+                  <input-number
+                    v-model="centerX"
+                    :min="0"
+                    :max="width"
+                    size="small"
+                    style="flex: 1"
+                  />
+                  <input-number
+                    v-model="centerY"
+                    :min="0"
+                    :max="height"
+                    size="small"
+                    style="flex: 1"
+                  />
                 </div>
               </el-form-item>
               <el-form-item label="角度">
                 <div class="w-full flex items-center gap-8px">
-                  <input-number v-model="coneStartAngle" :min="0" :max="3600" size="small" style="width: 100%;">
+                  <input-number
+                    v-model="coneStartAngle"
+                    :min="0"
+                    :max="3600"
+                    size="small"
+                    style="width: 100%"
+                  >
                     <template #prefix>S</template>
                   </input-number>
-                  <input-number v-model="coneEndAngle" :min="0" :max="3600" size="small" style="width: 100%;">
+                  <input-number
+                    v-model="coneEndAngle"
+                    :min="0"
+                    :max="3600"
+                    size="small"
+                    style="width: 100%"
+                  >
                     <template #prefix>E</template>
                   </input-number>
                 </div>
@@ -140,8 +216,13 @@
                 <div v-for="(pt, index) in pointsList" :key="index" class="gradient-point-item">
                   <el-card shadow="hover" bodyClass="p-10px! relative">
                     <div class="flex gap-12px items-center pr-12px jusitfy-between">
-                      <ColorPicker class="flex-1" v-model:pureColor="pt.color" format="hex8" picker-type="chrome"
-                        use-type="pure" />
+                      <ColorPicker
+                        class="flex-1"
+                        v-model:pureColor="pt.color"
+                        format="hex8"
+                        picker-type="chrome"
+                        use-type="pure"
+                      />
                       <div class="flex-1/3">
                         <div>颜色值</div>
                         <div>{{ pt.color }}</div>
@@ -165,16 +246,22 @@
       </div>
     </el-scrollbar>
 
-
     <template #footer>
       <el-button type="primary" @click="submit">确定</el-button>
     </template>
   </el-dialog>
-  <div v-if="useType != 'pure'" class="trigger" @click="open" :style="triggerStyle">
-    <slot name="trigger"></slot>
+  <div v-if="useType != 'pure'" class="trigger" @click="open">
+    <slot name="trigger">
+      <div class="w-full h-full" :style="triggerStyle"></div>
+    </slot>
   </div>
-  <ColorPicker v-else v-model:pureColor="pureColor" format="hex8" picker-type="chrome" use-type="pure" />
-
+  <ColorPicker
+    v-else
+    v-model:pureColor="pureColor"
+    format="hex8"
+    picker-type="chrome"
+    use-type="pure"
+  />
 </template>
 <script setup lang="ts">
 import { ref, computed, watch } from 'vue'
@@ -188,14 +275,17 @@ import type { GradientColor } from '@/lvgl-widgets/type'
 const pureColor = defineModel<string>('pureColor')
 const gradientColor = defineModel<GradientColor | string>('gradientColor')
 
-const props = withDefaults(defineProps<{
-  useType?: 'pure' | 'gradient' | 'both';
-  onlyColor?: boolean;
-  width?: number
-  height?: number
-}>(), {
-  useType: 'pure'
-})
+const props = withDefaults(
+  defineProps<{
+    useType?: 'pure' | 'gradient' | 'both'
+    onlyColor?: boolean
+    width?: number
+    height?: number
+  }>(),
+  {
+    useType: 'pure'
+  }
+)
 const show = ref(false)
 
 const type = ref<'pure' | 'gradient' | 'advanced'>('pure')
@@ -208,7 +298,7 @@ const typeOptions = computed(() => {
     {
       label: '基础渐变',
       value: 'gradient'
-    },
+    }
   ]
   // 如果宽高存在,则高级渐变可选
   if (props.width && props.height) {
@@ -309,18 +399,26 @@ const triggerStyle = computed(() => {
       }
     }
     return {
-      backgroundImage: generateCssGradient(gradientColor.value as GradientColor, props.width, props.height)
+      backgroundImage: generateCssGradient(
+        gradientColor.value as GradientColor,
+        props.width,
+        props.height
+      )
     }
   }
 
   return {
-    backgroundColor: pureColor.value || '#ffffff00'
+    background: pureColor.value || '#ffffffff'
   }
 })
 
 // 预览渐变色
 const previewGradient = computed(() => {
-  return { background: internalGradient.value ? generateCssGradient(internalGradient.value, props.width, props.height) : '#ffffff00' }
+  return {
+    background: internalGradient.value
+      ? generateCssGradient(internalGradient.value, props.width, props.height)
+      : '#ffffff00'
+  }
 })
 
 // 基础渐变:方向
@@ -595,9 +693,13 @@ const submit = () => {
   width: 24px;
   height: 24px;
   border-radius: 4px;
+  overflow: hidden;
   cursor: pointer;
   background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCGIasIUwhT25BVBADtzYNYrHvv4gAAAABJRU5ErkJggg==);
   background-repeat: repeat;
   margin-right: 10px;
 }
-</style>
+.trigger-bg {
+  background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCGIasIUwhT25BVBADtzYNYrHvv4gAAAABJRU5ErkJggg==);
+}
+</style>

+ 1 - 3
src/renderer/src/hooks/useHistory.ts

@@ -41,8 +41,7 @@ export const useHistory = () => {
     canRedo,
     canUndo,
     clear,
-    last,
-    reset
+    last
   } = useDebouncedRefHistory(project, {
     capacity: MAX_HISTORY_LENGTH,
     clone: klona,
@@ -72,7 +71,6 @@ export const useHistory = () => {
   const initHistory = async () => {
     setTimeout(() => {
       clear()
-      reset()
       operationHistory.clear()
     }, DEBOUNCE_TIME + 100)
   }

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

@@ -108,6 +108,7 @@
   "line": "Line",
   "display": "Display",
   "arc": "Arc",
+  "scale": "Scale",
   "widthRequired": "width is required",
   "heightRequired": "height is required",
   "pathRequired": "project path is required",

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

@@ -108,6 +108,7 @@
   "line": "线条",
   "display": "显示",
   "arc": "圆弧",
+  "scale": "标尺",
   "widthRequired": "宽不能为空",
   "heightRequired": "高不能为空",
   "pathRequired": "项目路径不能为空",

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

@@ -29,7 +29,7 @@ export default {
       flags: [],
       states: [],
       text: 'Button',
-      longMode: 'warp'
+      longMode: 'wrap'
     },
     styles: [
       {
@@ -231,7 +231,7 @@ export default {
         valueType: 'shadow'
       },
       {
-        label: '变',
+        label: '变',
         field: 'transform',
         valueType: 'transform',
         componentProps: {

+ 17 - 16
src/renderer/src/lvgl-widgets/dropdown/Dropdown.vue

@@ -7,15 +7,8 @@
     />
     <div class="relative w-full overflow-hidden" :class="direction === 'left' ? 'text-right' : ''">
       {{ options?.[0] }}
-      <div
-        v-if="showArrow"
-        class="absolute right-0 top-0"
-        :class="direction === 'left' ? 'left-0 right-auto' : ''"
-      >
-        <EpArrowDownBold v-if="direction === 'bottom'" size="14px" />
-        <EpArrowUpBold v-if="direction === 'top'" size="14px" />
-        <EpArrowLeftBold v-if="direction === 'left'" size="14px" />
-        <EpArrowRightBold v-if="direction === 'right'" size="14px" />
+      <div class="absolute right-0 top-0" :class="direction === 'left' ? 'left-0 right-auto' : ''">
+        <i class="lvgl-icon not-italic" v-html="icon ? getSymbol(icon) : ''" :style="iconStyle"></i>
       </div>
     </div>
     <div
@@ -49,22 +42,17 @@
 
 <script setup lang="ts">
 import { computed, CSSProperties } from 'vue'
-import {
-  EpArrowDownBold,
-  EpArrowUpBold,
-  EpArrowLeftBold,
-  EpArrowRightBold
-} from 'vue-icons-plus/ep'
 import { useWidgetStyle } from '../hooks/useWidgetStyle'
 import ImageBg from '../ImageBg.vue'
+import { getSymbol } from '@/utils'
 
 const props = defineProps<{
   width: number
   height: number
+  icon?: string
   styles: any
   state?: string
   part?: string
-  showArrow?: boolean
   options: string[]
   direction?: 'top' | 'bottom' | 'left' | 'right'
 }>()
@@ -114,6 +102,19 @@ const scrollbarStyle = computed(() => {
     height: '40%'
   }
 })
+
+// 图标样式
+const iconStyle = computed(() => {
+  const direction = props.direction
+  const rotate =
+    direction === 'top' ? 180 : direction === 'left' ? 90 : direction === 'right' ? -90 : 0
+  return {
+    display: 'flex',
+    alignItems: 'center',
+    justifyContent: 'center',
+    transform: `rotate(${rotate}deg)`
+  }
+})
 </script>
 
 <style lang="less" scoped>

+ 12 - 105
src/renderer/src/lvgl-widgets/dropdown/index.tsx

@@ -41,7 +41,7 @@ export default {
       height: 40,
       flags: [],
       states: [],
-      showArrow: false,
+      icon: 'LV_SYMBOL_DOWN',
       options: ['option1', 'option2', 'option3'],
       direction: 'bottom'
     },
@@ -105,98 +105,6 @@ export default {
           scale: 256
         }
       }
-      // {
-      //   part: {
-      //     name: 'list',
-      //     state: 'default'
-      //   },
-      //   background: {
-      //     color: '#ff00ffff',
-      //     image: {
-      //       imgId: '',
-      //       color: '#ffffff00',
-      //       alpha: 255
-      //     }
-      //   },
-      //   text: {
-      //     color: '#000000ff',
-      //     size: 12,
-      //     family: 'xx',
-      //     align: 'center',
-      //     decoration: 'none'
-      //   },
-      //   spacer: {
-      //     letterSpacing: 0,
-      //     lineHeight: 0
-      //   },
-      //   border: {
-      //     color: '#eeeeeeff',
-      //     width: 0,
-      //     radius: 0,
-      //     side: ['all']
-      //   },
-      //   outline: {
-      //     color: '#000000ff',
-      //     width: 0,
-      //     pad: 0
-      //   },
-      //   padding: {
-      //     left: 10,
-      //     right: 10,
-      //     top: 10,
-      //     bottom: 10
-      //   },
-      //   shadow: {
-      //     color: '#2092f5ff',
-      //     x: 0,
-      //     y: 0,
-      //     spread: 0,
-      //     width: 0
-      //   }
-      // },
-      // {
-      //   part: {
-      //     name: 'listSelected',
-      //     state: 'default'
-      //   },
-      //   background: {
-      //     color: '#55ffffff',
-      //     image: {
-      //       imgId: '',
-      //       color: '#ffffff00',
-      //       alpha: 255
-      //     }
-      //   },
-      //   text: {
-      //     color: '#000000ff',
-      //     size: 12,
-      //     family: 'xx',
-      //     align: 'center',
-      //     decoration: 'none'
-      //   },
-      //   spacer: {
-      //     letterSpacing: 0,
-      //     lineHeight: 0
-      //   },
-      //   border: {
-      //     color: '#eeeeeeff',
-      //     width: 0,
-      //     radius: 0,
-      //     side: ['all']
-      //   }
-      // },
-      // {
-      //   part: {
-      //     name: 'scrollbar',
-      //     state: 'default'
-      //   },
-      //   background: {
-      //     color: '#00ff00ff'
-      //   },
-      //   border: {
-      //     radius: 3
-      //   }
-      // }
     ]
   },
   config: {
@@ -286,17 +194,11 @@ export default {
       }
     ],
     coreProps: [
-      {
-        label: '绘制箭头图标',
-        field: 'props.showArrow',
-        valueType: 'switch',
-        labelWidth: '120px'
-      },
       {
         label: '展开方向',
         field: 'props.direction',
         valueType: 'select',
-        labelWidth: '120px',
+        labelWidth: '80px',
         componentProps: {
           options: [
             { label: 'BOTTOM', value: 'bottom' },
@@ -306,6 +208,12 @@ export default {
           ]
         }
       },
+      {
+        label: '图标',
+        field: 'props.icon',
+        valueType: 'symbol',
+        labelWidth: '80px'
+      },
       {
         label: '属性',
         field: 'props.options',
@@ -439,12 +347,11 @@ export default {
                       }
                     },
                     {
-                      label: '边框圆角',
-                      field: 'border.radius',
-                      valueType: 'number',
+                      label: '边框',
+                      field: 'border',
+                      valueType: 'border',
                       componentProps: {
-                        min: 0,
-                        step: 1
+                        onlyRadius: true
                       }
                     }
                   ]

+ 4 - 4
src/renderer/src/lvgl-widgets/image/Image.vue

@@ -27,11 +27,11 @@ const props = defineProps<{
   styles: any
   part?: string
   state?: string
-  rotate: {
+  center: {
     x: number
     y: number
-    angle: number
   }
+  rotate: number
   openScale?: boolean
   scale?: number
   antiAliasing?: boolean
@@ -47,9 +47,9 @@ const styleMap = useWidgetStyle({
 })
 
 const imageProps = computed(() => {
-  const { openScale, scale = 256, width, height, rotate } = props
+  const { openScale, scale = 256, width, height, center } = props
   const s = openScale ? scale / 256 : 1
-  const { x = width / 2, y = height / 2 } = rotate
+  const { x = width / 2, y = height / 2 } = center
 
   return {
     width: `${width}px`,

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

@@ -30,11 +30,11 @@ export default {
       flags: [],
       states: [],
       image: '',
-      rotate: {
+      center: {
         x: 50,
-        y: 50,
-        angle: 0
+        y: 50
       },
+      rotate: 0,
       openScale: false,
       scale: 256
       // antiAliasing: false
@@ -201,7 +201,7 @@ export default {
         children: [
           {
             label: 'X',
-            field: 'props.rotate.x',
+            field: 'props.center.x',
             valueType: 'number',
             componentProps: {
               span: 12
@@ -209,7 +209,7 @@ export default {
           },
           {
             label: 'Y',
-            field: 'props.rotate.y',
+            field: 'props.center.y',
             valueType: 'number',
             componentProps: {
               span: 12
@@ -219,7 +219,7 @@ export default {
       },
       {
         label: '旋转角度',
-        field: 'props.rotate.angle',
+        field: 'props.rotate',
         labelWidth: '60px',
         valueType: 'number',
         componentProps: {

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

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

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

@@ -29,7 +29,7 @@ export default {
       flags: [],
       states: [],
       text: 'Label',
-      longMode: 'warp',
+      longMode: 'wrap',
       lightStart: -1,
       lightEnd: -1
     },

+ 0 - 6
src/renderer/src/lvgl-widgets/list/List.vue

@@ -89,12 +89,6 @@ const props = defineProps<{
   part?: string
   state?: string
   items: ListItem[]
-  openRotate?: boolean
-  rotate?: {
-    x: number
-    y: number
-    angle: number
-  }
 }>()
 
 const containerRef = ref<HTMLDivElement>()

+ 0 - 6
src/renderer/src/lvgl-widgets/message/MessageBox.vue

@@ -86,14 +86,8 @@ const props = defineProps<{
   btnWidth: number
   btnHeight: number
   btns: { text: string }[]
-  openRotate?: boolean
   id?: string
   fixedHeight?: boolean
-  rotate?: {
-    x: number
-    y: number
-    angle: number
-  }
 }>()
 
 const projectStore = useProjectStore()

+ 131 - 0
src/renderer/src/lvgl-widgets/scale/Config.vue

@@ -0,0 +1,131 @@
+<template>
+  <div>
+    <el-button type="primary" size="small" @click="onAdd">{{
+      $t?.('添加区域') ?? '添加区域'
+    }}</el-button>
+    <div
+      v-for="(section, idx) in sections"
+      :key="section.name"
+      class="section-config"
+      style="margin-top: 16px; border: 1px solid #eee; padding: 12px; border-radius: 4px"
+    >
+      <div style="display: flex; align-items: center; justify-content: space-between">
+        <div>
+          <b>{{ section.name }}</b>
+        </div>
+        <el-button
+          v-if="sections.length > 1"
+          type="danger"
+          size="small"
+          icon="el-icon-delete"
+          @click="onRemove(idx)"
+          circle
+        />
+      </div>
+      <el-form :inline="true" label-width="80px" style="margin-top: 12px">
+        <el-form-item :label="$t?.('起始值') ?? '起始值'">
+          <input-number
+            v-model="section.start"
+            :min="0"
+            :max="10000"
+            :step="1"
+            size="small"
+            @change="emitChange"
+          ></input-number>
+        </el-form-item>
+        <el-form-item :label="$t?.('结束值') ?? '结束值'">
+          <input-number
+            v-model="section.end"
+            :min="1"
+            :max="10000"
+            :step="1"
+            size="small"
+            @change="emitChange"
+          ></input-number>
+        </el-form-item>
+        <el-form-item :label="$t?.('文本颜色') ?? '文本颜色'">
+          <ColorPicker v-model="section.textColor" use-type="pure" format="hex8" />
+          <span class="text-text-active">{{ section.textColor }}</span>
+        </el-form-item>
+        <el-form-item :label="$t?.('直线颜色') ?? '直线颜色'">
+          <ColorPicker v-model="section.lineColor" use-type="pure" format="hex8" />
+          <span class="text-text-active">{{ section.lineColor }}</span>
+        </el-form-item>
+      </el-form>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, watch } from 'vue'
+
+import { ColorPicker } from '@/components'
+
+export type AreaSection = {
+  name: string
+  start: number
+  end: number
+  textColor: string
+  lineColor: string
+}
+
+const props = defineProps<{
+  values: AreaSection[]
+}>()
+const emit = defineEmits(['update:modelValue', 'change'])
+
+const defaultSection = (idx: number) => ({
+  name: `curSection_${idx}`,
+  start: 0,
+  end: 20,
+  textColor: '#0000ff',
+  lineColor: '#0000ff'
+})
+
+const sections = ref<any[]>(
+  Array.isArray(props.values) && props.values.length
+    ? props.values.map((s, idx) => ({
+        name: s.name || `curSection_${idx}`,
+        start: typeof s.start === 'number' ? s.start : 0,
+        end: typeof s.end === 'number' ? s.end : 20,
+        textColor: s.textColor ?? '#000000ff',
+        lineColor: s.lineColor ?? '#000000ff'
+      }))
+    : [defaultSection(0)]
+)
+
+function onAdd() {
+  sections.value.push(defaultSection(sections.value.length))
+  emitChange()
+}
+function onRemove(idx: number) {
+  sections.value.splice(idx, 1)
+  emitChange()
+}
+function emitChange() {
+  const output = sections.value.map((section) => ({
+    name: section.name,
+    start: section.start,
+    end: section.end,
+    textColor: section.textColor,
+    lineColor: section.lineColor
+  }))
+  emit('update:modelValue', output)
+  emit('change', output)
+}
+
+watch(
+  () => props.values,
+  (val) => {
+    if (Array.isArray(val)) {
+      sections.value = val.map((s: any, idx: number) => ({
+        name: s.name || `curSection_${idx}`,
+        start: typeof s.start === 'number' ? s.start : 0,
+        end: typeof s.end === 'number' ? s.end : 20,
+        textColor: s.textColor ?? '#000000ff',
+        lineColor: s.lineColor ?? '#000000ff'
+      }))
+    }
+  }
+)
+</script>

+ 318 - 205
src/renderer/src/lvgl-widgets/scale/Scale.vue

@@ -1,62 +1,75 @@
 <template>
-  <div :style="{
-    ...styleMap?.mainStyle
-  }" class="relative w-full h-full box-border overflow-hidden relative">
+  <div
+    :style="{
+      ...styleMap?.mainStyle
+    }"
+    class="relative w-full h-full box-border relative"
+  >
+    <ImageBg
+      v-if="styleMap?.mainStyle?.imageSrc"
+      :src="styleMap?.mainStyle?.imageSrc"
+      :imageStyle="styleMap?.mainStyle?.imageStyle"
+    />
 
-    <ImageBg v-if="styleMap?.mainStyle?.imageSrc" :src="styleMap?.mainStyle?.imageSrc"
-      :imageStyle="styleMap?.mainStyle?.imageStyle" />
-
-    <div class="absolute inset-0 w-full h-full" :style="{ transform: `rotate(${props.rotate}deg)` }">
-      <svg :viewBox="`0 0 ${width} ${height}`" preserveAspectRatio="xMidYMid meet" class="w-full h-full block"
-        xmlns="http://www.w3.org/2000/svg">
-        <defs>
-          <!-- 背景条图片 pattern:保持图像原始尺寸,居中显示 -->
-          <pattern v-if="styleMap?.mainStyle?.curve?.imageSrc" :id="`${arcId}-bg-pattern`" patternUnits="userSpaceOnUse"
-            :width="width" :height="height" x="0" y="0">
-            <image :href="styleMap.mainStyle.curve.imageSrc" transform="translate(-50%, -50%)" />
-          </pattern>
-          <!-- 进度值条图片 pattern:保持图像原始尺寸,居中显示 -->
-          <pattern v-if="styleMap?.indicatorStyle?.curve?.imageSrc" :id="`${arcId}-indicator-pattern`"
-            patternUnits="userSpaceOnUse" :width="width" :height="height" x="0" y="0">
-            <image :href="styleMap.indicatorStyle.curve.imageSrc" transform="translate(-50%, -50%)" />
-          </pattern>
-        </defs>
-        <!-- 背景条:绘制完整的起始到结束角度 -->
-        <path :d="bgPath" fill="none"
-          :stroke="styleMap?.mainStyle?.curve?.imageSrc ? `url(#${arcId}-bg-pattern)` : (styleMap?.mainStyle?.curve?.color || '#eeeeee')"
-          :stroke-width="styleMap?.mainStyle?.curve?.width ?? 1"
-          :stroke-linecap="styleMap?.mainStyle?.curve?.radius ? 'round' : 'butt'"
-          :opacity="styleMap?.mainStyle?.curve?.opacity" />
-
-        <!-- 进度值条 -->
-        <path :d="valuePath" fill="none"
-          :stroke="styleMap?.indicatorStyle?.curve?.imageSrc ? `url(#${arcId}-indicator-pattern)` : (styleMap?.indicatorStyle?.curve?.color || '#2092f5')"
-          :stroke-width="styleMap?.indicatorStyle?.curve?.width ?? 1"
-          :stroke-linecap="styleMap?.indicatorStyle?.curve?.radius ? 'round' : 'butt'"
-          :opacity="styleMap?.indicatorStyle?.curve?.opacity" />
-
-        <!-- 进度圆点:无背景图时用实心圆 -->
-        <circle v-if="dotPos && !styleMap?.knobStyle?.imageSrc" :cx="dotPos.x" :cy="dotPos.y" :r="knobRadius.radius"
-          :fill="styleMap?.knobStyle?.backgroundColor || '#2092f5'" />
+    <div class="absolute inset-0 w-full h-full">
+      <svg
+        :viewBox="`0 0 ${width} ${height}`"
+        preserveAspectRatio="xMidYMid meet"
+        class="w-full h-full block"
+        xmlns="http://www.w3.org/2000/svg"
+      >
+        <defs></defs>
+        <!-- 主轴直线:按 area 分段绘制,每段使用对应范围的直线颜色 -->
+        <line
+          v-for="(seg, segIdx) in axisSegments"
+          :key="'axis-' + segIdx"
+          :x1="seg.x1"
+          :y1="seg.y1"
+          :x2="seg.x2"
+          :y2="seg.y2"
+          :stroke="seg.stroke"
+          :stroke-width="mainLineWidth"
+          stroke-linecap="butt"
+        />
+        <!-- 刻度线:所有刻度,按 area 使用对应直线颜色 -->
+        <line
+          v-for="(pos, idx) in tickPositions"
+          :key="'tick-' + idx"
+          :x1="pos.x1"
+          :y1="pos.y1"
+          :x2="pos.x2"
+          :y2="pos.y2"
+          :stroke="pos.stroke"
+          :stroke-width="itemsLineWidth"
+          stroke-linecap="butt"
+        />
+        <!-- 标签(仅主刻度位置显示),按 area 使用对应文本颜色 -->
+        <template v-if="enableLabels">
+          <text
+            v-for="(lb, idx) in labelList"
+            :key="'label-' + idx"
+            :x="lb.x"
+            :y="lb.y"
+            :text-anchor="lb.textAnchor"
+            :dominant-baseline="lb.dominantBaseline"
+            :fill="lb.fill"
+            :font-size="mainTextSize"
+            :font-family="mainFontFamily"
+          >
+            {{ lb.text }}
+          </text>
+        </template>
       </svg>
-
-      <!-- 进度圆点背景图:与 SVG 同坐标系,用 ImageBg 统一实现 -->
-      <div v-if="dotPos && styleMap?.knobStyle?.imageSrc" class="absolute overflow-hidden pointer-events-none"
-        :style="knobOverlayStyle">
-        <ImageBg :src="styleMap.knobStyle.imageSrc" :imageStyle="styleMap.knobStyle.imageStyle" />
-      </div>
     </div>
   </div>
 </template>
 
 <script setup lang="ts">
-import { computed, ref } from 'vue'
+import { computed } from 'vue'
 import { useWidgetStyle } from '../hooks/useWidgetStyle'
-import { useProjectStore } from '@/store/modules/project';
-
-import ImageBg from '../ImageBg.vue';
 
-const arcId = ref('arc-' + Math.random().toString(36).slice(2))
+import ImageBg from '../ImageBg.vue'
+import type { AreaSection } from './Config.vue'
 
 const props = defineProps<{
   width: number
@@ -64,196 +77,296 @@ const props = defineProps<{
   styles: any
   state?: string
   part?: string
-  mode: 'normal' | 'symmetrical' | 'reverse'
+  mode: 'horizontal_top' | 'horizontal_bottom' | 'vertical_left' | 'vertical_right'
+  tick: number
+  mainTick: number
   rangeStart: number
   rangeEnd: number
-  angleStart: number
-  angleEnd: number
-  value: number
-  rotate: number
-  // 偏移开关
-  rotateOffset?: boolean
-  // 偏移量(度数)
-  rotateOffsetValue?: number
+  enableLabels: boolean
+  labels: string
+  area: AreaSection[]
 }>()
 
-const projectStore = useProjectStore()
-
 const styleMap = useWidgetStyle({
-  widget: 'lv_arc',
+  widget: 'lv_scale',
   props
 })
 
-const cx = computed(() => {
-  const { width, height } = props
-  const min = Math.min(width, height)
-  return min / 2
-})
-
-/** 弧线半径:与背景/进度条一致,留出边距避免贴边 */
-const trackRadius = computed(() => {
-  const { width, height } = props
-  const min = Math.min(width, height)
-  const curveWidth = styleMap.value?.mainStyle?.curve?.width ?? 12
-  return Math.max(4, min / 2 - curveWidth / 2 - knobRadius.value.padding)
+// 归一化后的 area 列表(start/end 为数值范围,颜色为 #hex 字符串)
+const normalizedAreas = computed(() => {
+  const list = props.area || []
+  return list.map(
+    (s: AreaSection & { textColor?: string | number; lineColor?: string | number }) => ({
+      start: Number(s.start),
+      end: Number(s.end),
+      textColor: s.textColor,
+      lineColor: s.lineColor
+    })
+  )
 })
 
-/** 从 schema 的 styles 中解析 knob 的 padding.left 作为圆点半径 */
-const knobRadius = computed(() => {
-  const defaultIndicatorStyle = projectStore.globalStyle
-    ?.find((item) => item.widget === 'lv_arc')
-    ?.part?.find((item) => item.partName === 'indicator')?.defaultStyle
+// 根据归一化位置 t∈[0,1] 得到数值
+const valueAt = (t: number) => props.rangeStart + t * (props.rangeEnd - props.rangeStart)
 
-  const defaultKnobStyle = projectStore.globalStyle
-    ?.find((item) => item.widget === 'lv_arc')
-    ?.part?.find((item) => item.partName === 'knob')?.defaultStyle
-
-  const indicatorStyle = props.styles?.find((s: any) => s.part?.name === 'indicator' && s.part.state === props.state)
-  const knobStyle = props.styles?.find((s: any) => s.part?.name === 'knob' && s.part.state === props.state)
-  const padding = knobStyle?.padding?.left ?? defaultKnobStyle?.padding?.left
-  const r = indicatorStyle?.curve?.width ?? defaultIndicatorStyle?.curve?.width ?? 12
-
-  return {
-    radius: r / 2 + padding,
-    padding: padding,
-    width: r
+// 根据数值取所在 area 的直线颜色,否则用默认
+const getLineColorForValue = (value: number): string => {
+  for (const s of normalizedAreas.value) {
+    if (value >= s.start && value <= s.end) return s.lineColor
   }
-})
+  return itemsLineColor.value
+}
 
-/**
- * 极坐标转直角坐标
- * 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)
+// 根据数值取所在 area 的文本颜色,否则用默认
+const getTextColorForValue = (value: number): string => {
+  for (const s of normalizedAreas.value) {
+    if (value >= s.start && value <= s.end) return s.textColor
   }
+  return mainTextColor.value
 }
 
-/**
- * 生成 SVG 弧线路径指令
- * 始终从 startAngle 顺时针画到 endAngle,得到“近整圆、缺口在底部”的环(图2效果)
- */
-function describeArc(x: number, y: number, radius: number, startAngle: number, endAngle: number) {
-  const start = polarToCartesian(x, y, radius, startAngle)
-  const end = polarToCartesian(x, y, radius, endAngle)
-  // 顺时针扫过的角度(0~360)
-  let clockwiseSpan = (endAngle - startAngle) % 360
-  if (clockwiseSpan <= 0) clockwiseSpan += 360
-  if (clockwiseSpan === 0) clockwiseSpan = 360
-  const largeArcFlag = clockwiseSpan > 180 ? '1' : '0'
-  const sweepFlag = '1' // 顺时针
+// 从 styles 中读取 items 的 other.length(刻度线长度)
+const itemsStyleConfig = computed(() =>
+  props.styles?.find(
+    (s: any) =>
+      s.part?.name === 'items' && (s.part?.state === props.state || s.part?.state === 'default')
+  )
+)
+const tickLength = computed(() => itemsStyleConfig.value?.other?.length ?? 5)
+const mainTickLength = computed(() => Math.max(tickLength.value * 1.5, tickLength.value + 4))
 
-  return [
-    'M',
-    start.x,
-    start.y,
-    'A',
-    radius,
-    radius,
-    0,
-    largeArcFlag,
-    sweepFlag,
-    end.x,
-    end.y
-  ].join(' ')
-}
+const mainLineColor = computed(() => styleMap.value?.mainStyle?.line?.color ?? '#212121')
+const mainLineWidth = computed(() => styleMap.value?.mainStyle?.line?.width ?? 2)
+const itemsLineColor = computed(() => styleMap.value?.itemsStyle?.line?.color ?? '#212121')
+const itemsLineWidth = computed(() => styleMap.value?.itemsStyle?.line?.width ?? 2)
 
-// 1. 背景路径
-const bgPath = computed(() => {
-  const r = trackRadius.value
-  return describeArc(cx.value, cx.value, r, props.angleStart, props.angleEnd)
-})
+const mainTextColor = computed(() => styleMap.value?.mainStyle?.color ?? '#000000')
+const mainTextSize = computed(() => styleMap.value?.mainStyle?.fontSize ?? '16px')
+const mainFontFamily = computed(() => styleMap.value?.mainStyle?.fontFamily ?? 'sans-serif')
+
+const isHorizontal = computed(
+  () => props.mode === 'horizontal_top' || props.mode === 'horizontal_bottom'
+)
+const isTopOrLeft = computed(
+  () => props.mode === 'horizontal_top' || props.mode === 'vertical_left'
+)
 
-// 顺时针从 angleStart 到 angleEnd 的弧度跨度(0~360)
-const clockwiseSpan = computed(() => {
-  const { angleStart, angleEnd } = props
-  let s = (angleEnd - angleStart) % 360
-  if (s <= 0) s += 360
-  return s === 0 ? 360 : s
+// 刻度数量、主刻度数量
+const tickCount = computed(() => Math.max(2, props.tick))
+const mainTickCount = computed(() => Math.max(1, Math.min(props.mainTick, tickCount.value)))
+
+// 主刻度在刻度数组中的索引集合
+const majorIndices = computed(() => {
+  const set = new Set<number>()
+  const n = tickCount.value
+  const m = mainTickCount.value
+  if (m <= 1) {
+    set.add(0)
+    return set
+  }
+  for (let i = 0; i < m; i++) {
+    const idx = Math.round((i * (n - 1)) / (m - 1))
+    set.add(Math.min(idx, n - 1))
+  }
+  return set
 })
 
-// 2. 进度计算逻辑(与 describeArc 的顺时针方向一致)
-const progressData = computed(() => {
-  const { value, rangeStart, rangeEnd, angleStart, angleEnd, mode } = props
+// 归一化位置 [0..1] 的刻度列表
+const tickNormPositions = computed(() => {
+  const n = tickCount.value
+  if (n <= 1) return [0.5]
+  const list: number[] = []
+  for (let i = 0; i < n; i++) list.push(i / (n - 1))
+  return list
+})
 
-  const rangeDiff = rangeEnd - rangeStart
-  const ratio =
-    Math.abs(rangeDiff) < 1e-9
-      ? 0
-      : Math.max(0, Math.min(1, (value - rangeStart) / rangeDiff))
+// 根据 mode 计算轴线坐标(主轴直线)
+// horizontal_top: 主轴贴在最底部;horizontal_bottom: 主轴贴在最顶部
+const axisLine = computed(() => {
+  const { width, height } = props
+  const pad = mainTickLength.value + 8
+  const halfLine = mainLineWidth.value / 2
+  if (isHorizontal.value) {
+    const y = isTopOrLeft.value ? height - halfLine : halfLine
+    return { x1: 0, y1: y, x2: width, y2: y }
+  }
+  const x = isTopOrLeft.value ? pad : width - pad
+  return { x1: x, y1: 0, x2: x, y2: height }
+})
 
-  const span = clockwiseSpan.value
-  let startA = angleStart
-  let endA = angleEnd
+// 主轴按 area 分段:根据范围边界拆成多段,每段使用对应 area 的直线颜色
+const axisSegments = computed(() => {
+  const { width, height } = props
+  const start = props.rangeStart
+  const end = props.rangeEnd
+  const span = end - start
+  const axis = axisLine.value
 
-  if (mode === 'normal') {
-    // 从 angleStart 顺时针填充 span * ratio
-    endA = angleStart + span * ratio
-  } else if (mode === 'reverse') {
-    startA = angleEnd - span * ratio
-    endA = angleEnd
-  } else if (mode === 'symmetrical') {
-    const midAngle = angleStart + span / 2
-    const midValue = (rangeStart + rangeEnd) / 2
-    const rangeToEnd = rangeEnd - midValue
-    const rangeToStart = midValue - rangeStart
+  const boundaries = new Set<number>([0, 1])
+  normalizedAreas.value.forEach((s) => {
+    if (span !== 0) {
+      const t0 = Math.max(0, Math.min(1, (s.start - start) / span))
+      const t1 = Math.max(0, Math.min(1, (s.end - start) / span))
+      boundaries.add(t0)
+      boundaries.add(t1)
+    }
+  })
+  const sorted = Array.from(boundaries).sort((a, b) => a - b)
 
-    if (value >= midValue && Math.abs(rangeToEnd) >= 1e-9) {
-      startA = midAngle
-      const halfRatio = Math.max(0, Math.min(1, (value - midValue) / rangeToEnd))
-      endA = midAngle + (span / 2) * halfRatio
-    } else if (value < midValue && Math.abs(rangeToStart) >= 1e-9) {
-      endA = midAngle
-      const halfRatio = Math.max(0, Math.min(1, (midValue - value) / rangeToStart))
-      startA = midAngle - (span / 2) * halfRatio
+  const segments: { x1: number; y1: number; x2: number; y2: number; stroke: string }[] = []
+  for (let i = 0; i < sorted.length - 1; i++) {
+    const t0 = sorted[i]
+    const t1 = sorted[i + 1]
+    const midValue = valueAt((t0 + t1) / 2)
+    const stroke = getLineColorForValue(midValue)
+    if (isHorizontal.value) {
+      segments.push({
+        x1: t0 * width,
+        y1: axis.y1,
+        x2: t1 * width,
+        y2: axis.y2,
+        stroke
+      })
     } else {
-      startA = midAngle
-      endA = midAngle
+      segments.push({
+        x1: axis.x1,
+        y1: t0 * height,
+        x2: axis.x2,
+        y2: t1 * height,
+        stroke
+      })
     }
   }
-
-  return { startA, endA }
+  if (segments.length === 0) {
+    segments.push({
+      x1: axis.x1,
+      y1: axis.y1,
+      x2: axis.x2,
+      y2: axis.y2,
+      stroke: mainLineColor.value
+    })
+  }
+  return segments
 })
 
-// 3. 值路径(与背景弧同半径)
-const valuePath = computed(() => {
-  const { startA, endA } = progressData.value
-  if (startA === endA) return ''
-  return describeArc(cx.value, cx.value, trackRadius.value, startA, endA)
+// 刻度线端点:与主轴共线;每根刻度按数值取 area 直线颜色
+const tickPositions = computed(() => {
+  const { width, height } = props
+  const pad = mainTickLength.value + 8
+  const axis = axisLine.value
+  const norm = tickNormPositions.value
+  const major = majorIndices.value
+  const list: { x1: number; y1: number; x2: number; y2: number; stroke: string }[] = []
+
+  if (isHorizontal.value) {
+    const yAxis = axis.y1
+    // horizontal_top: 刻度朝上;horizontal_bottom: 刻度朝下
+    const sign = isTopOrLeft.value ? -1 : 1
+    norm.forEach((t, idx) => {
+      const x = t * width
+      const len = major.has(idx) ? mainTickLength.value : tickLength.value
+      const value = valueAt(t)
+      list.push({
+        x1: x,
+        y1: yAxis,
+        x2: x,
+        y2: yAxis + sign * len,
+        stroke: getLineColorForValue(value)
+      })
+    })
+  } else {
+    const xAxis = isTopOrLeft.value ? pad : width - pad
+    const sign = isTopOrLeft.value ? -1 : 1
+    norm.forEach((t, idx) => {
+      const y = t * height
+      const len = major.has(idx) ? mainTickLength.value : tickLength.value
+      const value = valueAt(t)
+      list.push({
+        x1: xAxis,
+        y1: y,
+        x2: xAxis + sign * len,
+        y2: y,
+        stroke: getLineColorForValue(value)
+      })
+    })
+  }
+  return list
 })
 
-// 4. 圆点位置(与弧同半径)
-const dotPos = computed(() => {
-  // 原始结束角:normal / symmetrical 用 endA,reverse 用 startA
-  const baseAngle =
-    props.mode === 'reverse' ? progressData.value.startA : progressData.value.endA
-  // 仅对“结束值原点”做偏移,不整体偏移整条进度弧
-  const offset = props.rotateOffset ? props.rotateOffsetValue ?? 0 : 0
-  const angle = baseAngle + offset
-  return polarToCartesian(cx.value, cx.value, trackRadius.value, angle)
+// 标签文案:有设置内容用设置内容,否则用主刻度数值(根据范围变化),主刻度数为整数
+const labelTexts = computed(() => {
+  const raw = (props.labels || '').trim()
+  if (raw) {
+    return raw
+      .split(',')
+      .map((s) => s.trim())
+      .filter(Boolean)
+  }
+  const m = mainTickCount.value
+  const start = props.rangeStart
+  const end = props.rangeEnd
+  const list: string[] = []
+  for (let i = 0; i < m; i++) {
+    const v = m <= 1 ? start : start + ((end - start) * i) / (m - 1)
+    list.push(String(Math.round(v)))
+  }
+  return list
 })
 
-/** 进度圆点 overlay 的定位样式(与 viewBox 比例一致,用 ImageBg 时使用) */
-const knobOverlayStyle = computed(() => {
-  if (!dotPos.value || !props.width || !props.height) return {}
-  const { x, y } = dotPos.value
-  const r = knobRadius.value.radius
-  return {
-    left: `${((x - r) / props.width) * 100}%`,
-    top: `${((y - r) / props.height) * 100}%`,
-    width: `${(r * 2 / props.width) * 100}%`,
-    height: `${(r * 2 / props.height) * 100}%`,
-    borderRadius: '50%'
+// 标签位置与对齐(仅主刻度位置),按 area 使用对应文本颜色
+const labelList = computed(() => {
+  if (!props.enableLabels || labelTexts.value.length === 0) return []
+  const { width, height } = props
+  const pad = mainTickLength.value + 8
+  const labelGap = 4
+  const m = mainTickCount.value
+  const texts = labelTexts.value
+  const list: {
+    x: number
+    y: number
+    text: string
+    textAnchor: string
+    dominantBaseline: string
+    fill: string
+  }[] = []
+
+  if (isHorizontal.value) {
+    const yAxis = axisLine.value.y1
+    // horizontal_top: 文字在刻度上(刻度朝上,标签在刻度线顶端上方);horizontal_bottom: 文字在刻度下
+    const labelY = isTopOrLeft.value
+      ? yAxis - mainTickLength.value - labelGap
+      : yAxis + mainTickLength.value + labelGap
+    for (let i = 0; i < m; i++) {
+      const t = m <= 1 ? 0.5 : i / (m - 1)
+      const x = t * width
+      const value = valueAt(t)
+      list.push({
+        x,
+        y: labelY,
+        text: texts[i] ?? '',
+        textAnchor: 'middle',
+        dominantBaseline: isTopOrLeft.value ? 'auto' : 'hanging',
+        fill: getTextColorForValue(value)
+      })
+    }
+  } else {
+    const xAxis = isTopOrLeft.value ? pad : width - pad
+    const labelX = isTopOrLeft.value
+      ? xAxis - mainTickLength.value - labelGap
+      : xAxis + mainTickLength.value + labelGap
+    for (let i = 0; i < m; i++) {
+      const t = m <= 1 ? 0.5 : i / (m - 1)
+      const y = t * height
+      const value = valueAt(t)
+      list.push({
+        x: labelX,
+        y,
+        text: texts[i] ?? '',
+        textAnchor: isTopOrLeft.value ? 'end' : 'start',
+        dominantBaseline: 'middle',
+        fill: getTextColorForValue(value)
+      })
+    }
   }
+  return list
 })
 </script>

+ 117 - 134
src/renderer/src/lvgl-widgets/scale/index.ts

@@ -1,9 +1,10 @@
 import Scale from './Scale.vue'
-import icon from '../assets/icon/icon_22scale.svg'
+import icon from '../assets/icon/icon_22ruler.svg'
 import type { IComponentModelConfig } from '../type'
 import i18n from '@/locales'
 import { flagOptions, stateOptions, stateList } from '@/constants'
 import defaultStyle from './style.json'
+import Config from './Config.vue'
 
 export default {
   label: i18n.global.t('scale'),
@@ -28,26 +29,24 @@ export default {
     }
   ],
   defaultSchema: {
-    name: 'arc',
+    name: 'scale',
     props: {
       x: 0,
       y: 0,
-      width: 120,
-      height: 120,
+      width: 300,
+      height: 30,
       flags: [],
       states: [],
-      mode: 'normal',
+      mode: 'horizontal_top',
+      // 刻度
+      tick: 41,
+      // 主刻度
+      mainTick: 8,
       rangeStart: 0,
       rangeEnd: 100,
-      angleStart: 135,
-      angleEnd: 45,
-      value: 70,
-      rotate: 0,
-      // 旋转偏移开关
-      rotateOffset: false,
-      rotateOffsetValue: 60,
-      // 变化率
-      changeRate: 720
+      enableLabels: true,
+      labels: '',
+      area: []
     },
     styles: [
       {
@@ -63,24 +62,23 @@ export default {
             alpha: 255
           }
         },
+        text: {
+          color: '#000000ff',
+          family: 'xx',
+          size: 16,
+          align: 'center',
+          decoration: 'none'
+        },
         border: {
-          color: '#2092f5ff',
+          color: '#000000ff',
           width: 0,
-          radius: 6,
+          radius: 0,
           side: ['all']
         },
-        padding: {
-          top: 20,
-          right: 20,
-          bottom: 20,
-          left: 20
-        },
-        curve: {
-          color: '#e0e0e0ff',
-          width: 12,
-          radius: true,
-          alpha: 255,
-          image: ''
+        line: {
+          color: '#212121FF',
+          width: 2,
+          radius: false
         },
         shadow: {
           color: '#2092f5ff',
@@ -99,6 +97,34 @@ export default {
           rotate: 0,
           scale: 256
         }
+      },
+      {
+        part: {
+          name: 'items',
+          state: 'default'
+        },
+        line: {
+          color: '#212121FF',
+          width: 2,
+          radius: false
+        },
+        other: {
+          length: 5
+        }
+      },
+      {
+        part: {
+          name: 'indicator',
+          state: 'default'
+        },
+        line: {
+          color: '#212121FF',
+          width: 2,
+          radius: false
+        },
+        other: {
+          length: 5
+        }
       }
     ]
   },
@@ -191,12 +217,33 @@ export default {
         labelWidth: '80px',
         componentProps: {
           options: [
-            { label: 'normal', value: 'normal' },
-            { label: 'symmetrical', value: 'symmetrical' },
-            { label: 'reverse', value: 'reverse' }
+            { label: 'Horizontal Top', value: 'horizontal_top' },
+            { label: 'Horizontal Bottom', value: 'horizontal_bottom' },
+            { label: 'Vertical Left', value: 'vertical_left' },
+            { label: 'Vertical Right', value: 'vertical_right' }
           ]
         }
       },
+      {
+        label: '刻度',
+        field: 'props.tick',
+        valueType: 'number',
+        labelWidth: '80px',
+        componentProps: {
+          min: 2,
+          max: 10000
+        }
+      },
+      {
+        label: '主刻度',
+        field: 'props.mainTick',
+        valueType: 'number',
+        labelWidth: '80px',
+        componentProps: {
+          min: 2,
+          max: 10000
+        }
+      },
       {
         label: '范围',
         valueType: 'group',
@@ -228,81 +275,25 @@ export default {
         ]
       },
       {
-        label: '',
-        field: 'props.value',
-        labelWidth: '80px',
-        valueType: 'number'
+        label: '启用文本',
+        field: 'props.enableLabels',
+        valueType: 'switch',
+        labelWidth: '80px'
       },
       {
-        label: '角度',
-        valueType: 'group',
-        children: [
-          {
-            field: 'props.angleStart',
-            valueType: 'number',
-            componentProps: {
-              span: 12,
-              min: 0,
-              max: 360
-            },
-            slots: {
-              prefix: 'S'
-            }
-          },
-          {
-            field: 'props.angleEnd',
-            valueType: 'number',
-            componentProps: {
-              span: 12,
-              min: 0,
-              max: 360
-            },
-            slots: {
-              prefix: 'E'
-            }
-          }
-        ]
-      },
-      {
-        label: '旋转',
-        field: 'props.rotate',
+        label: '标签',
+        field: 'props.labels',
+        valueType: 'textarea',
         labelWidth: '80px',
-        valueType: 'number'
-      },
-      {
-        label: '旋转偏移',
-        field: 'props.rotateOffset',
-        labelWidth: '80px',
-        valueType: 'switch'
-      },
-      {
-        valueType: 'dependency',
-        name: ['props.rotateOffset'],
-        dependency: (dependency) => {
-          return dependency?.['props.rotateOffset']
-            ? [
-                {
-                  label: '旋转偏移值',
-                  field: 'props.rotateOffsetValue',
-                  labelWidth: '80px',
-                  valueType: 'number',
-                  componentProps: {
-                    min: -360,
-                    max: 360
-                  }
-                }
-              ]
-            : []
+        componentProps: {
+          placeholder: '标签内容用英文逗号分隔'
         }
       },
       {
-        label: '变化率',
-        field: 'props.changeRate',
-        valueType: 'number',
-        labelWidth: '80px',
-        componentProps: {
-          min: 1,
-          max: 10000
+        label: '区域',
+        field: 'props.area',
+        render: (val) => {
+          return <Config values={val} />
         }
       }
     ],
@@ -324,23 +315,20 @@ export default {
                   field: 'background',
                   valueType: 'background'
                 },
+                {
+                  label: '字体',
+                  field: 'text',
+                  valueType: 'font'
+                },
                 {
                   label: '边框',
                   field: 'border',
                   valueType: 'border'
                 },
                 {
-                  label: '内边距',
-                  field: 'padding',
-                  valueType: 'padding'
-                },
-                {
-                  label: '曲线',
-                  field: 'curve',
-                  valueType: 'line',
-                  componentProps: {
-                    hasImage: true
-                  }
+                  label: '直线',
+                  field: 'line',
+                  valueType: 'line'
                 },
                 {
                   label: '阴影',
@@ -353,32 +341,27 @@ export default {
                   valueType: 'transform'
                 }
               ]
-            : part?.name === 'indicator'
-              ? [
-                  {
-                    label: '曲线',
-                    field: 'curve',
-                    valueType: 'line',
-                    componentProps: {
-                      hasImage: true
-                    }
-                  }
-                ]
-              : [
-                  {
-                    label: '背景',
-                    field: 'background',
-                    valueType: 'background'
-                  },
-                  {
-                    label: '内边距',
-                    field: 'padding',
-                    valueType: 'padding',
+            : [
+                {
+                  label: '直线',
+                  field: 'line',
+                  valueType: 'line'
+                },
+                {
+                  label: '其他',
+                  field: 'other',
+                  valueType: 'other',
+                  componentProps: {
+                    keys: ['length'],
                     componentProps: {
-                      allInOne: true
+                      length: {
+                        min: 0,
+                        max: 10000
+                      }
                     }
                   }
-                ]
+                }
+              ]
         }
       }
     ]

+ 29 - 35
src/renderer/src/lvgl-widgets/scale/style.json

@@ -1,5 +1,5 @@
 {
-  "widget": "lv_arc",
+  "widget": "lv_scale",
   "styleName": "default",
   "part": [
     {
@@ -13,24 +13,23 @@
             "alpha": 255
           }
         },
+        "text": {
+          "color": "#000000ff",
+          "family": "xx",
+          "size": 16,
+          "align": "center",
+          "decoration": "none"
+        },
         "border": {
-          "color": "#2092f5ff",
+          "color": "#000000ff",
           "width": 0,
-          "radius": 6,
+          "radius": 0,
           "side": ["all"]
         },
-        "padding": {
-          "top": 20,
-          "right": 20,
-          "bottom": 20,
-          "left": 20
-        },
-        "curve": {
-          "color": "#e0e0e0ff",
-          "width": 12,
-          "radius": true,
-          "alpha": 255,
-          "image": ""
+        "line": {
+          "color": "#212121FF",
+          "width": 2,
+          "radius": false
         },
         "shadow": {
           "color": "#2092f5ff",
@@ -53,34 +52,29 @@
       "state": []
     },
     {
-      "partName": "indicator",
+      "partName": "items",
       "defaultStyle": {
-        "curve": {
-          "color": "#2196f3ff",
-          "width": 12,
-          "radius": true,
-          "alpha": 255,
-          "image": ""
+        "line": {
+          "color": "#212121FF",
+          "width": 2,
+          "radius": false
+        },
+        "other": {
+          "length": 5
         }
       },
       "state": []
     },
     {
-      "partName": "knob",
+      "partName": "indicator",
       "defaultStyle": {
-        "background": {
-          "color": "#2196f3ff",
-          "image": {
-            "imgId": "",
-            "recolor": "#ffffff00",
-            "alpha": 255
-          }
+        "line": {
+          "color": "#212121FF",
+          "width": 2,
+          "radius": false
         },
-        "padding": {
-          "left": 5,
-          "right": 5,
-          "top": 5,
-          "bottom": 5
+        "other": {
+          "length": 10
         }
       },
       "state": []

+ 0 - 6
src/renderer/src/lvgl-widgets/span-group/SpanGroup.vue

@@ -24,12 +24,6 @@ const props = defineProps<{
   state?: string
   mode: 'break' | 'fixed' | 'expand'
   items: SpanItem[]
-  openRotate?: boolean
-  rotate?: {
-    x: number
-    y: number
-    angle: number
-  }
 }>()
 
 const styleMap = useWidgetStyle({

+ 0 - 6
src/renderer/src/lvgl-widgets/switch/Switch.vue

@@ -36,12 +36,6 @@ const props = defineProps<{
   styles: any
   state?: string
   part?: string
-  openRotate?: boolean
-  rotate?: {
-    x: number
-    y: number
-    angle: number
-  }
 }>()
 
 const styleMap = useWidgetStyle({

+ 6 - 9
src/renderer/src/lvgl-widgets/switch/index.ts

@@ -195,16 +195,16 @@ export default {
                   field: 'outline',
                   valueType: 'outline'
                 },
-                {
-                  label: '动画',
-                  field: 'animation',
-                  valueType: 'animation'
-                },
                 {
                   label: '阴影',
                   field: 'shadow',
                   valueType: 'shadow'
                 },
+                {
+                  label: '动画',
+                  field: 'animation',
+                  valueType: 'animation'
+                },
                 {
                   label: '变换',
                   field: 'transform',
@@ -241,10 +241,7 @@ export default {
                   {
                     label: '背景',
                     field: 'background',
-                    valueType: 'background',
-                    componentProps: {
-                      onlyColor: true
-                    }
+                    valueType: 'background'
                   },
                   {
                     label: '边框',

+ 0 - 6
src/renderer/src/lvgl-widgets/tabview/Tabview.vue

@@ -43,12 +43,6 @@ const props = defineProps<{
   position: string
   children?: any[]
   activeIndex: number
-  openRotate?: boolean
-  rotate?: {
-    x: number
-    y: number
-    angle: number
-  }
 }>()
 
 const styleMap = useWidgetStyle({

+ 3 - 8
src/renderer/src/lvgl-widgets/textarea/Textarea.vue

@@ -49,13 +49,8 @@ const props = defineProps<{
   maxLength: number
   allowString: string
   passwordMode: boolean
+  passwordReplaceText: string
   nowrap: boolean
-  openRotate?: boolean
-  rotate?: {
-    x: number
-    y: number
-    angle: number
-  }
 }>()
 
 const boxRef = ref<HTMLDivElement | null>()
@@ -80,9 +75,9 @@ watch(() => [props.width, props.height, props.text, props.nowrap], handleResize,
 
 // 文本内容
 const getContent = computed(() => {
-  const { text, placeholder, passwordMode } = props
+  const { text, placeholder, passwordMode, passwordReplaceText } = props
 
-  return passwordMode ? '*'.repeat(text.length) : text || placeholder
+  return passwordMode ? (passwordReplaceText || '*').repeat(text.length) : text || placeholder
 })
 
 const styleMap = useWidgetStyle({

+ 27 - 7
src/renderer/src/lvgl-widgets/textarea/index.ts

@@ -37,6 +37,7 @@ export default {
       maxLength: 32,
       allowString: '',
       passwordMode: false,
+      passwordReplaceText: '',
       nowrap: false
     },
     styles: [
@@ -232,6 +233,25 @@ export default {
         valueType: 'switch',
         labelWidth: '80px'
       },
+      {
+        valueType: 'dependency',
+        name: ['props.passwordMode'],
+        dependency: (dependency) => {
+          return dependency['props.passwordMode']
+            ? [
+                {
+                  label: '替换字符',
+                  field: 'props.passwordReplaceText',
+                  valueType: 'text',
+                  labelWidth: '80px',
+                  componentProps: {
+                    maxlength: 10
+                  }
+                }
+              ]
+            : []
+        }
+      },
       {
         label: '单行模式',
         field: 'props.nowrap',
@@ -258,19 +278,19 @@ export default {
                   valueType: 'background'
                 },
                 {
-                  label: '圆角',
-                  field: 'border.radius',
-                  valueType: 'number'
+                  label: '边框',
+                  field: 'border',
+                  valueType: 'border',
+                  componentProps: {
+                    onlyRadius: true
+                  }
                 }
               ]
             : [
                 {
                   label: '背景',
                   field: 'background',
-                  valueType: 'background',
-                  componentProps: {
-                    onlyColor: true
-                  }
+                  valueType: 'background'
                 },
                 {
                   label: '字体',

+ 0 - 6
src/renderer/src/lvgl-widgets/window/Window.vue

@@ -72,12 +72,6 @@ const props = defineProps<{
   titleHeight: number
   text: string
   btns: ListItem[]
-  openRotate?: boolean
-  rotate?: {
-    x: number
-    y: number
-    angle: number
-  }
 }>()
 
 const styleMap = useWidgetStyle({

+ 2 - 0
src/renderer/src/store/modules/project.ts

@@ -184,6 +184,7 @@ export const useProjectStore = defineStore('project', () => {
     activePageId.value = ''
     currentMaxScreen.value = null
     openPageIds.value = []
+    activeWidgets.value = []
     meta.screens.forEach((screen, index) => {
       const newScreen = createScreen(screen)
       newScreen.name += `_${index + 1}`
@@ -280,6 +281,7 @@ export const useProjectStore = defineStore('project', () => {
       .map((screen) => screen.pages?.[0]?.id)
       .filter((item) => item)
     activePageId.value = newProject.screens[0].pages?.[0]?.id
+    activeWidgets.value = []
     imageCompressFormat.value = newProject.meta.imageCompress
     recentProjectStore.addProject({
       id: v4(),

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

@@ -81,6 +81,12 @@
         v-model="value"
         v-bind="schema?.componentProps"
       />
+      <!-- 图标选择 -->
+      <SymbolSelect
+        v-if="schema.valueType === 'symbol'"
+        v-model="value"
+        v-bind="schema?.componentProps"
+      />
     </el-form-item>
 
     <!-- 分组 -->
@@ -199,6 +205,12 @@
           v-model="value"
           v-bind="schema?.componentProps"
         />
+        <!-- 其他 -->
+        <StyleOther
+          v-if="schema.valueType === 'other'"
+          v-model="value"
+          v-bind="schema?.componentProps"
+        />
       </template>
     </el-card>
 
@@ -227,6 +239,8 @@ import { v4 } from 'uuid'
 import CusCheckbox from './components/CusCheckbox.vue'
 import CusTextarea from './components/CusTextarea.vue'
 import ImageSelect from './components/ImageSelect.vue'
+import SymbolSelect from './components/SymbolSelect.vue'
+
 import StyleBackground from './components/StyleBackground.vue'
 import StyleBorder from './components/StyleBorder.vue'
 import StyleFont from './components/StyleFont.vue'
@@ -240,6 +254,7 @@ import StyleOutline from './components/StyleOutline.vue'
 import StyleImage from './components/StyleImage.vue'
 import StyleTransform from './components/StyleTransform.vue'
 import StyleAnimation from './components/StyleAnimation.vue'
+import StyleOther from './components/StyleOther.vue'
 
 defineOptions({
   name: 'CusFormItem'
@@ -296,7 +311,8 @@ const isFormItem = computed(() => {
     'imageStyle',
     'transform',
     'outline',
-    'animation'
+    'animation',
+    'other'
   ]
   // 排除字段
   const excludeFields: string[] = []
@@ -325,7 +341,8 @@ const isStyle = computed(() => {
     'outline',
     'imageStyle',
     'transform',
-    'animation'
+    'animation',
+    'other'
   ]
 
   return include.includes(props.schema.valueType) && !props.schema?.render

+ 13 - 13
src/renderer/src/views/designer/config/property/components/StyleBackground.vue

@@ -1,6 +1,6 @@
 <template>
   <div>
-    <el-form-item label="背景颜色" label-position="left" label-width="60px">
+    <el-form-item label="背景颜色" label-position="left" label-width="70px">
       <ColorModal
         v-model:pureColor="pureColor"
         v-model:gradientColor="gradientColor"
@@ -12,20 +12,10 @@
         typeof modelValue?.color === 'object' ? '渐变颜色' : modelValue?.color
       }}</span>
     </el-form-item>
-    <el-form-item v-if="!onlyColor" label="背景图片" label-position="left" label-width="60px">
+    <el-form-item v-if="!onlyColor" label="背景图片" label-position="left" label-width="70px">
       <ImageSelect v-model="image" />
     </el-form-item>
-    <el-form-item v-if="!onlyColor" label="图片遮罩" label-position="left" label-width="60px">
-      <ColorPicker
-        use-type="pure"
-        picker-type="chrome"
-        format="hex8"
-        v-model:pureColor="imageColor"
-        :disabled="!modelValue?.image"
-      />
-      <span class="text-text-active">{{ imageColor }}</span>
-    </el-form-item>
-    <el-form-item v-if="!onlyColor" label="透明度" label-position="left" label-width="60px">
+    <el-form-item v-if="!onlyColor" label="图片透明度" label-position="left" label-width="70px">
       <div class="w-full flex gap-20px items-center">
         <el-slider
           v-model="imageAlpha"
@@ -39,6 +29,16 @@
         </span>
       </div>
     </el-form-item>
+    <el-form-item v-if="!onlyColor" label="图片遮罩" label-position="left" label-width="70px">
+      <ColorPicker
+        use-type="pure"
+        picker-type="chrome"
+        format="hex8"
+        v-model:pureColor="imageColor"
+        :disabled="!modelValue?.image"
+      />
+      <span class="text-text-active">{{ imageColor }}</span>
+    </el-form-item>
   </div>
 </template>
 

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

@@ -34,7 +34,7 @@
     </el-form-item>
 
     <el-form-item label-position="left" label-width="0px" v-if="!onlyRadius">
-      <div class="flex-1 flex items-center justify-between px-4px">
+      <div class="flex-1 flex items-center justify-between px-4px mt-8px">
         <BsBorderOuter
           class="cursor-pointer"
           :class="{ 'color-accent-blue': side?.includes('all') }"

+ 45 - 0
src/renderer/src/views/designer/config/property/components/StyleOther.vue

@@ -0,0 +1,45 @@
+<template>
+  <div>
+    <el-form-item
+      v-if="keys.includes('length')"
+      label="长度"
+      label-position="left"
+      label-width="60px"
+    >
+      <input-number
+        placeholder="请输入"
+        v-model="length"
+        controls-position="right"
+        style="width: 100%"
+        v-bind="componentProps?.['length']"
+      />
+    </el-form-item>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { computed } from 'vue'
+
+defineProps<{
+  keys: string[]
+  componentProps: {
+    [key: string]: any
+  }
+}>()
+
+const modelValue = defineModel<{
+  [key: string]: any
+}>('modelValue')
+
+// length
+const length = computed({
+  get: () => modelValue.value?.length,
+  set: (val: number) => {
+    if (modelValue.value) {
+      modelValue.value.length = val
+    }
+  }
+})
+</script>
+
+<style scoped></style>

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

@@ -1,7 +1,7 @@
 <template>
   <div>
     <el-row :gutter="12">
-      <el-col :span="12">
+      <el-col :span="24">
         <el-form-item label-width="0px">
           <ColorPicker v-model:pureColor="color" format="hex8" picker-type="chrome" use-type="pure">
           </ColorPicker>

+ 32 - 0
src/renderer/src/views/designer/config/property/components/SymbolSelect.vue

@@ -0,0 +1,32 @@
+<template>
+  <el-input spellcheck="false" readonly>
+    <template #prefix>
+      <i class="lvgl-icon not-italic" v-html="modelValue ? getSymbol(modelValue) : ''"></i>
+    </template>
+    <template #append>
+      <LuSmilePlus size="12px" class="cursor-pointer" @click="handleOpenSymbolModal" />
+    </template>
+  </el-input>
+  <SymbolSelectModal ref="symbolModalRef" @select="handleSelectSymbol" />
+</template>
+
+<script setup lang="ts">
+import { ref } from 'vue'
+import { LuSmilePlus } from 'vue-icons-plus/lu'
+import SymbolSelectModal from './SymbolSelectModal.vue'
+import { getSymbol } from '@/utils'
+
+defineProps<{}>()
+const modelValue = defineModel<string>('modelValue')
+const symbolModalRef = ref<InstanceType<typeof SymbolSelectModal>>()
+
+const handleOpenSymbolModal = () => {
+  symbolModalRef.value?.open()
+}
+
+const handleSelectSymbol = (val: string) => {
+  if (val) {
+    modelValue.value = val
+  }
+}
+</script>

+ 4 - 5
src/renderer/src/views/designer/workspace/stage/Node.vue

@@ -135,12 +135,11 @@ const getStyle = computed((): CSSProperties => {
   }
 
   let rotate = ''
-  // 存在旋转开关
-  const hasRotateSwitch = Object.hasOwn(schema.props, 'openRotate')
   // 存在旋转属性
-  if (schema.props?.rotate && (!hasRotateSwitch || schema.props.openRotate)) {
-    rotate = `rotate(${schema.props.rotate.angle}deg)`
-    other.transformOrigin = `${schema.props.rotate.x}px ${schema.props.rotate.y}px`
+  if (schema.props?.rotate) {
+    rotate = `rotate(${schema.props.rotate}deg)`
+    other.transformOrigin = `${schema.props.center.x}px ${schema.props.center.y}px`
+
     if (other.transform) {
       other.transform += ` ${rotate}`
     }