Переглянути джерело

perf: 优化矩阵按钮控件

jiaxing.liao 2 тижнів тому
батько
коміт
45b812236e

+ 1 - 0
src/renderer/src/components/InputNumber/index.vue

@@ -1,6 +1,7 @@
 <template>
   <el-input-number
     :model-value="modelValue"
+    controls-position="right"
     v-bind="$attrs"
     @focus="onFocus"
     @blur="onBlur"

+ 46 - 5
src/renderer/src/lvgl-widgets/button-matrix/ButtonMatrix.vue

@@ -7,30 +7,71 @@
       :style="{ columnGap: styleMap?.mainStyle?.columnGap }"
     >
       <div
-        class="flex-1 h-full flex items-center justify-center"
-        :style="styleMap?.itemsStyle"
+        class="h-full flex items-center justify-center overflow-hidden"
         v-for="(item, index) in row || []"
         :key="`${index}-${index}`"
+        :style="{
+          ...(isCustom ? getBtnStyle(item) || styleMap?.itemsStyle : styleMap?.itemsStyle),
+          ...getBtnFlex(
+            item.width,
+            row.map((item) => item.width)
+          )
+        }"
       >
-        {{ item }}
+        {{ item.text }}
       </div>
     </div>
   </div>
 </template>
 
 <script setup lang="ts">
-import { useWidgetStyle } from '../hooks/useWidgetStyle'
+import { useWidgetStyle, getStyle } from '../hooks/useWidgetStyle'
+import { ButtonItem } from './data'
+import { isEmpty, assign } from 'lodash-es'
 
 const props = defineProps<{
   width: number
   height: number
-  group?: string[][]
+  group?: ButtonItem[][]
   styles: any
   state?: string
+  openRotate?: false
+  rotate?: {
+    x: number
+    y: number
+    angle: number
+  }
+  isCustom?: boolean
 }>()
 
 const styleMap = useWidgetStyle({
   widget: 'lv_buttonmatrix',
   props
 })
+
+/**
+ * 获取自定义样式
+ */
+const getBtnStyle = (btnItem: ButtonItem) => {
+  const style = btnItem.style.find((item) => item.part.state === 'default') || {}
+  const styleMap = {}
+  Object.keys(style).forEach((key) => {
+    assign(styleMap, getStyle(key, style?.[key]))
+  })
+
+  return isEmpty(styleMap) ? null : styleMap
+}
+
+/**
+ * 获取按钮宽度
+ */
+const getBtnFlex = (btnWidth: number, list: number[]) => {
+  const count = list.reduce((acc, cur) => acc + cur, 0)
+  const basic = (btnWidth / count) * 100 + '%'
+  return {
+    'flex-basis': basic,
+    'flex-grow': 1,
+    'flex-shrink': 1
+  }
+}
 </script>

+ 245 - 30
src/renderer/src/lvgl-widgets/button-matrix/Config.vue

@@ -6,68 +6,220 @@
         <el-button type="primary" :icon="LuPlus" @click="handleAddRow"></el-button>
       </div>
     </template>
-    <div
-      class="flex items-center gap-4px mb-4px"
-      v-for="(row, rowIndex) in modelValue || []"
-      :key="rowIndex"
-    >
-      <div class="relative group/item" v-for="(_val, columnIndex) in row" :key="columnIndex">
-        <el-input spellcheck="false" v-model="modelValue[rowIndex][columnIndex]" />
+    <el-scrollbar>
+      <div
+        class="flex items-center gap-4px mb-4px"
+        v-for="(row, rowIndex) in group || []"
+        :key="rowIndex"
+      >
         <div
-          class="absolute top--7px right--4px cursor-pointer invisible group-hover/item:visible"
-          @click="handleDeleteItem(rowIndex, columnIndex)"
+          class="relative group/item flex-1"
+          v-for="(_val, columnIndex) in row"
+          :key="columnIndex"
         >
-          <LuX size="14px" />
+          <el-button
+            class="w-full"
+            @click="handleEdit(group[rowIndex][columnIndex], rowIndex, columnIndex)"
+            >{{ group[rowIndex][columnIndex].text }}</el-button
+          >
+          <div
+            class="absolute top--7px right--4px cursor-pointer invisible group-hover/item:visible"
+            @click="handleDeleteItem(rowIndex, columnIndex)"
+          >
+            <LuX size="14px" />
+          </div>
         </div>
-      </div>
 
-      <el-button
-        type="primary"
-        :icon="LuPlus"
-        :disabled="row.length === 5"
-        @click="handleAddItem(rowIndex)"
-      ></el-button>
-    </div>
+        <el-button
+          type="primary"
+          :icon="LuPlus"
+          :disabled="row.length === 5"
+          @click="handleAddItem(rowIndex)"
+        ></el-button>
+      </div>
+    </el-scrollbar>
   </el-card>
+
+  <el-dialog title="编辑按钮" width="600px" v-model="open" close-on-click-modal>
+    <el-form ref="form" :model="formData" :rules="rules" hide-required-asterisk>
+      <el-form-item label="名称" prop="name">
+        <el-input v-model="formData.name"></el-input>
+      </el-form-item>
+      <el-form-item label="文本" prop="text">
+        <el-input v-model="formData.text"></el-input>
+      </el-form-item>
+      <el-form-item label="宽度">
+        <input-number
+          v-model="formData.width"
+          style="width: 100%"
+          :min="1"
+          :max="255"
+        ></input-number>
+      </el-form-item>
+    </el-form>
+    <el-collapse v-if="isCustom" :model-value="['style']">
+      <el-collapse-item name="style">
+        <template #title>
+          <div class="flex items-center">
+            <IoColorPaletteOutline size="12px" />
+            <span class="ml-4px text-12px">样式配置</span>
+          </div>
+        </template>
+        <el-form
+          ref="formRef"
+          class="px-12px box-border"
+          label-position="left"
+          :model="styleFormData"
+        >
+          <el-form-item label="模块/状态" class="mb-12px!">
+            <div class="w-full">
+              <StylePart
+                v-model="part"
+                :hasStyle="!!styleFormData"
+                @change="onChangeStyleByState"
+              />
+            </div>
+          </el-form-item>
+
+          <el-row :gutter="12" v-if="styleFormData">
+            <CusFormItem
+              v-for="(item, index) in btn.config.styles || []"
+              :key="item.valueType + '_' + index"
+              :schema="item"
+              :formData="styleFormData"
+              :widgetData="projectStore.activeWidget!"
+            />
+          </el-row>
+        </el-form>
+      </el-collapse-item>
+    </el-collapse>
+    <template #footer>
+      <el-button type="primary" @click="handleSubmit">确定</el-button>
+    </template>
+  </el-dialog>
 </template>
 
 <script setup lang="ts">
-import { computed } from 'vue'
+import { computed, ref, provide, type Ref } from 'vue'
 import { LuPlus, LuX } from 'vue-icons-plus/lu'
+import { ButtonItem } from './data'
+import { getNextIndex } from '@/utils'
+import { useProjectStore } from '@/store/modules/project'
+import btn from './index'
+import { klona } from 'klona'
+import { FormInstance } from 'element-plus'
+import StylePart from '@/views/designer/config/property/components/StylePart.vue'
+import CusFormItem from '@/views/designer/config/property/CusFormItem.vue'
 
 const props = defineProps<{
-  values: any
+  values: Ref<{
+    props: {
+      group: ButtonItem[][]
+      isCustom: boolean
+      col: number
+    }
+  }>
 }>()
 
-const modelValue = computed({
+const open = ref(false)
+const projectStore = useProjectStore()
+const form = ref<FormInstance>()
+const itemParts = ref<any[]>([btn.parts.find((item) => item.name === 'items')])
+provide('parts', itemParts)
+
+/**
+ * 编辑表单数据
+ */
+const formData = ref<ButtonItem>({
+  name: '',
+  text: '',
+  type: '',
+  width: 0,
+  style: []
+})
+
+const rules = {
+  name: [
+    { required: true, message: '请输入按钮名称', trigger: 'blur' },
+    {
+      validator: (_rule: any, value: string, callback: any) => {
+        // 校验名称是否重复
+        const index = group.value.findIndex((row, rowIndex) =>
+          row.find((col, colIndex) => {
+            return value === col.name && (rowIndex !== tempRow || colIndex !== tempCol)
+          })
+        )
+        if (index !== -1) {
+          callback(new Error('名称重复'))
+        } else {
+          callback()
+        }
+      }
+    }
+  ],
+  text: [{ required: true, message: '请输入按钮文本', trigger: 'blur' }]
+}
+
+// 按钮列表
+const group = computed({
   get() {
-    return props.values?.value
+    return props.values?.value.props.group
   },
   set(val) {
-    props.values.value = val
+    props.values.value.props.group = val
   }
 })
 
+/**
+ * 是否自定义
+ */
+const isCustom = computed(() => {
+  return props.values?.value.props.isCustom
+})
+
 /**
  * 添加一项
  * @param rowIndex 行索引
  */
 const handleAddItem = (rowIndex: number | string) => {
-  const row = modelValue.value?.[rowIndex]
+  const i = getNextIndex(group.value.flat(), 'name')
+  const row = group.value?.[rowIndex]
   if (row?.length < 5) {
-    row.push('0')
+    row.push({
+      name: `btn_${i}`,
+      text: `${i}`,
+      type: 'button',
+      width: 1,
+      style: []
+    })
   }
 }
 
+let tempRow, tempCol
+
+const handleEdit = (record: ButtonItem, rowIndex: number | string, colIndex: number | string) => {
+  tempRow = rowIndex
+  tempCol = colIndex
+  open.value = true
+  formData.value = klona(record)
+}
+
+const handleSubmit = () => {
+  form.value?.validate().then(() => {
+    group.value[tempRow][tempCol] = klona(formData.value)
+    open.value = false
+  })
+}
+
 /**
  * 删除一项
  * @param rowIndex 行索引
  * @param index 索引
  */
-const handleDeleteItem = (rowIndex: number | string, index: number | string) => {
-  modelValue.value[rowIndex].splice(index, 1)
-  if (modelValue.value[rowIndex].length === 0) {
-    modelValue.value.splice(rowIndex, 1)
+const handleDeleteItem = (rowIndex: number, index: number) => {
+  group.value[rowIndex].splice(index, 1)
+  if (group.value[rowIndex].length === 0) {
+    group.value.splice(rowIndex, 1)
   }
 }
 
@@ -75,7 +227,70 @@ const handleDeleteItem = (rowIndex: number | string, index: number | string) =>
  * 添加一行
  */
 const handleAddRow = () => {
-  modelValue.value?.push(['0', '0', '0'])
+  const i = getNextIndex(group.value.flat(), 'name')
+  const newArr = new Array(props.values.value.props.col || 3).fill({})
+
+  group.value?.push(
+    newArr.map((_, index) => {
+      return {
+        name: `btn_${i + index}`,
+        text: `${i + index}`,
+        type: 'button',
+        width: 1,
+        style: []
+      }
+    })
+  )
+}
+
+// 样式表单数据
+const styleFormData = computed(() => {
+  const item = formData.value.style?.find(
+    (item) => item?.part.name === part.value?.name && item?.part.state === part.value?.state
+  )
+
+  return item
+})
+
+const part = ref<{
+  name: string
+  state: string
+}>({
+  name: 'items',
+  state: 'default'
+})
+
+const onChangeStyleByState = (type: 'add' | 'delete') => {
+  if (!part.value.name || !part.value.state) return
+  if (type === 'add') {
+    // 从全局样式查找控件样式
+    const widgetType = 'lv_buttonmatrix'
+    const style = projectStore.globalStyle
+      ?.find((item) => item.widget === widgetType)
+      ?.part?.find((item) => item.partName === part.value.name)
+      ?.state?.find((item) => item.state === part.value.state)?.style
+
+    // 找多对应样式时添加
+    if (style) {
+      const result = klona({
+        ...style,
+        part: {
+          name: part.value.name,
+          state: part.value.state
+        }
+      })
+      formData.value.style?.push(result)
+    }
+  } else {
+    // 删除样式
+    const index =
+      formData.value?.style.findIndex(
+        (item) => item.part.name === part.value.name && item.part.state === part.value.state
+      ) ?? -1
+    if (index !== -1) {
+      formData.value?.style.splice(index, 1)
+    }
+  }
 }
 </script>
 

+ 25 - 0
src/renderer/src/lvgl-widgets/button-matrix/data.ts

@@ -0,0 +1,25 @@
+/**
+ * 按钮数据
+ */
+export type ButtonItem = {
+  /**
+   * 按钮名称
+   */
+  name: string
+  /**
+   * 按钮文字
+   */
+  text: string
+  /**
+   * 按钮类型
+   */
+  type: string
+  /**
+   * 按钮宽度
+   */
+  width: number
+  /**
+   * 按钮样式
+   */
+  style: Record<string, any>[]
+}

+ 156 - 6
src/renderer/src/lvgl-widgets/button-matrix/index.tsx

@@ -33,11 +33,85 @@ export default {
       height: 150,
       addFlags: [],
       removeFlags: [],
+      isCustom: false,
+      col: 3,
       group: [
-        ['1', '2', '3'],
-        ['4', '5', '6'],
-        ['7', '8', '9']
-      ]
+        [
+          {
+            name: 'btn_1',
+            text: '1',
+            type: 'button',
+            width: 1,
+            style: []
+          },
+          {
+            name: 'btn_2',
+            text: '2',
+            type: 'button',
+            width: 1,
+            style: []
+          },
+          {
+            name: 'btn_3',
+            text: '3',
+            type: 'button',
+            width: 1,
+            style: []
+          }
+        ],
+        [
+          {
+            name: 'btn_4',
+            text: '4',
+            type: 'button',
+            width: 1,
+            style: []
+          },
+          {
+            name: 'btn_5',
+            text: '5',
+            type: 'button',
+            width: 1,
+            style: []
+          },
+          {
+            name: 'btn_6',
+            text: '6',
+            type: 'button',
+            width: 1,
+            style: []
+          }
+        ],
+        [
+          {
+            name: 'btn_7',
+            text: '7',
+            type: 'button',
+            width: 1,
+            style: []
+          },
+          {
+            name: 'btn_8',
+            text: '8',
+            type: 'button',
+            width: 1,
+            style: []
+          },
+          {
+            name: 'btn_9',
+            text: '9',
+            type: 'button',
+            width: 1,
+            style: []
+          }
+        ]
+      ],
+      openRotate: false,
+      rotate: {
+        x: 0,
+        y: 0,
+        angle: 0
+      }
     },
     styles: [
       {
@@ -176,13 +250,80 @@ export default {
       }
     ],
     coreProps: [
+      {
+        label: '列数',
+        field: 'props.col',
+        valueType: 'number',
+        componentProps: {
+          min: 1,
+          max: 5,
+          step: 1
+        }
+      },
+      {
+        label: '自定义',
+        field: 'props.isCustom',
+        valueType: 'switch'
+      },
       {
         label: '属性',
-        field: 'props.group',
+        field: '',
         valueType: '',
         render: (val) => {
           return <Config values={val} />
         }
+      },
+      {
+        label: '旋转',
+        field: 'props.openRotate',
+        valueType: 'switch'
+      },
+      {
+        valueType: 'dependency',
+        name: ['props.openRotate'],
+        dependency: (dependency) => {
+          return dependency?.['props.openRotate']
+            ? [
+                {
+                  label: '旋转中心',
+                  valueType: 'group',
+                  labelWidth: '80px',
+                  children: [
+                    {
+                      field: 'props.rotate.x',
+                      valueType: 'number',
+                      componentProps: {
+                        span: 12,
+                        min: -10000,
+                        max: 10000
+                      },
+                      slots: { prefix: 'X' }
+                    },
+                    {
+                      field: 'props.rotate.y',
+                      valueType: 'number',
+                      componentProps: {
+                        span: 12,
+                        min: -10000,
+                        max: 10000
+                      },
+                      slots: { prefix: 'Y' }
+                    },
+                    {
+                      field: 'props.rotate.angle',
+                      valueType: 'number',
+                      componentProps: {
+                        span: 12,
+                        min: -360,
+                        max: 360
+                      },
+                      slots: { prefix: '角度' }
+                    }
+                  ]
+                }
+              ]
+            : []
+        }
       }
     ],
     // 组件样式
@@ -190,7 +331,16 @@ export default {
       {
         label: '模块状态',
         field: 'part',
-        valueType: 'part'
+        valueType: 'part',
+        componentProps: {
+          filterPartOptions: (item, formData) => {
+            return {
+              label: item.name,
+              value: item.name,
+              disabled: formData.props?.isCustom && item.name === 'items'
+            }
+          }
+        }
       },
       {
         label: '背景',

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

@@ -208,7 +208,7 @@ export default {
                       valueType: 'number',
                       componentProps: {
                         span: 12,
-                        min: 0,
+                        min: -360,
                         max: 360
                       },
                       slots: { prefix: '角度' }

+ 21 - 21
src/renderer/src/lvgl-widgets/image-button/index.ts

@@ -38,10 +38,10 @@ export default {
         x: 0,
         y: 0,
         angle: 0
-      },
-      scale: 256,
-      // 抗锯齿
-      antiAliasing: false
+      }
+      // scale: 256,
+      // // 抗锯齿
+      // antiAliasing: false
     },
     styles: [
       {
@@ -215,7 +215,7 @@ export default {
                       valueType: 'number',
                       componentProps: {
                         span: 12,
-                        min: 0,
+                        min: -360,
                         max: 360
                       },
                       slots: { prefix: '角度' }
@@ -225,23 +225,23 @@ export default {
               ]
             : []
         }
-      },
-      {
-        label: '缩放',
-        field: 'props.scale',
-        valueType: 'number',
-        labelWidth: '80px',
-        componentProps: {
-          min: 0,
-          max: 10000
-        }
-      },
-      {
-        label: '抗锯齿',
-        field: 'props.antiAliasing',
-        valueType: 'switch',
-        labelWidth: '80px'
       }
+      // {
+      //   label: '缩放',
+      //   field: 'props.scale',
+      //   valueType: 'number',
+      //   labelWidth: '80px',
+      //   componentProps: {
+      //     min: 0,
+      //     max: 10000
+      //   }
+      // },
+      // {
+      //   label: '抗锯齿',
+      //   field: 'props.antiAliasing',
+      //   valueType: 'switch',
+      //   labelWidth: '80px'
+      // }
     ],
     // 组件样式
     styles: [

+ 1 - 1
src/renderer/src/types/baseWidget.d.ts

@@ -14,7 +14,7 @@ export type BaseWidget = {
   // 控件属性
   props: Record<string, any>
   // 样式
-  style: Record<string, any>
+  style: Record<string, any>[]
   // 事件
   events: WidgetEvent[]
   // 子控件

+ 12 - 0
src/renderer/src/utils/index.ts

@@ -93,3 +93,15 @@ export function moveToPosition(arr, fromIndex, toIndex) {
  * @returns lvgl symbol
  */
 export const getSymbol = (key: string) => symbols.find((item) => item.label === key)?.value
+
+/**
+ * 获取创建下一项的索引
+ * @param arr 数组
+ * @param key
+ */
+export const getNextIndex = (arr: any[], key: string) => {
+  const indexs =
+    arr.map((item) => Number(item[key].split('_')?.at(-1))).filter((n) => !Number.isNaN(n)) || []
+
+  return indexs.length ? Math.max(...indexs) + 1 : 1
+}

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

@@ -214,6 +214,8 @@ defineOptions({
 const props = defineProps<{
   schema: ComponentSchema
   formData: Record<string, any>
+  // 控件数据,补充额外数据
+  widgetData?: Record<string, any>
 }>()
 const key = v4()
 
@@ -299,11 +301,11 @@ const getComp = (fun: any) => {
  * 2. 根据值获取对应的表单项
  */
 const dependencyFormItems = computed(() => {
-  const { schema, formData } = props
+  const { schema, formData, widgetData } = props
   if (schema.valueType === 'dependency') {
     const values: { [key: string]: any } = {}
     schema?.name?.forEach((key) => {
-      const val = get(formData, key)
+      const val = get(formData, key) ?? get(widgetData, key)
       values[key] = val
     })
     const result = schema.dependency(values) || []

+ 21 - 2
src/renderer/src/views/designer/config/property/components/StylePart.vue

@@ -3,7 +3,7 @@
     <el-col :span="11">
       <el-select-v2
         placeholder="模块"
-        :options="parts?.map((item) => ({ label: item.name, value: item.name })) || []"
+        :options="partOptions"
         v-model="part"
         @change="handleChangePart"
       />
@@ -27,18 +27,37 @@ import { computed, inject } from 'vue'
 import type { PartItem } from '@/lvgl-widgets/type'
 
 const parts = inject<Ref<PartItem[]>>('parts')
+
 const modelValue = defineModel<{
   name: string
   state: string
 }>('modelValue')
 
-defineProps<{
+const props = defineProps<{
   hasStyle: boolean
+  widgetData?: Record<string, any>
+  componentProps?: {
+    // 处理part选项方法
+    filterPartOptions?: (item: PartItem, widgetData?: Record<string, any>) => any
+    [key: string]: any
+  }
 }>()
 const emit = defineEmits<{
   (e: 'change', val: 'add' | 'delete'): void
 }>()
 
+// 部件列表
+const partOptions = computed(() => {
+  const fn = props.componentProps?.filterPartOptions
+  return (
+    parts?.value?.map((item) => {
+      return typeof fn === 'function'
+        ? fn(item, props.widgetData)
+        : { label: item.name, value: item.name }
+    }) || []
+  )
+})
+
 const part = computed({
   get() {
     return modelValue.value?.name

+ 7 - 1
src/renderer/src/views/designer/config/property/index.vue

@@ -37,6 +37,7 @@
               :key="item.valueType + '_' + index"
               :schema="item"
               :formData="projectStore.activeWidget!"
+              :widgetData="projectStore.activeWidget!"
             />
           </el-row>
         </el-form>
@@ -65,6 +66,10 @@
                 v-model="part"
                 :hasStyle="!!styleFormData"
                 @change="onChangeStyleByState"
+                :widgetData="projectStore.activeWidget!"
+                :componentProps="
+                  formConfig.styles.find((item) => item.field === 'part')?.componentProps
+                "
               />
             </div>
           </el-form-item>
@@ -75,6 +80,7 @@
               :key="item.valueType + '_' + index"
               :schema="item"
               :formData="styleFormData"
+              :widgetData="projectStore.activeWidget!"
             />
           </el-row>
         </el-form>
@@ -178,7 +184,7 @@ const onChangeStyleByState = (type: 'add' | 'delete') => {
     const index =
       projectStore.activeWidget?.style.findIndex(
         (item) => item.part.name === part.value.name && item.part.state === part.value.state
-      ) || -1
+      ) ?? -1
     if (index !== -1) {
       projectStore.activeWidget?.style.splice(index, 1)
     }

+ 4 - 6
src/renderer/src/views/designer/modals/projectModal/index.vue

@@ -497,12 +497,14 @@ import { useRecentProject } from '@/store/modules/recentProject'
 import { useAppStore } from '@/store/modules/app'
 import { useLocalStorageState } from 'vue-hooks-plus'
 import { useI18n } from 'vue-i18n'
+import { klona } from 'klona'
+import { getNextIndex } from '@/utils'
+
 import Recent from './Recent.vue'
 
 import chipConfig from '@/config/multi_chip_config.json'
 import boardConfig from '@/config/board_card_config .json'
 import displayConfig from '@/config/analog_display_config.json'
-import { klona } from 'klona'
 
 const props = defineProps<{
   initHide?: boolean
@@ -580,11 +582,7 @@ const formData = reactive<
  * 设置默认项目名
  */
 const setProjectDefaultName = () => {
-  const list =
-    recentProject.recentProjects
-      ?.map((item) => Number(item.projectName.split('_')?.[1]))
-      ?.filter((n) => !Number.isNaN(n)) || []
-  const index = list.length ? Math.max(...list) + 1 : 1
+  const index = getNextIndex(recentProject.recentProjects || [], 'projectName')
 
   formData.name = `projectName_${index}`
 }