Ver código fonte

perf: 重构menu控件

jiaxing.liao 3 semanas atrás
pai
commit
436046cda6

+ 0 - 2
src/renderer/components.d.ts

@@ -52,7 +52,6 @@ declare module 'vue' {
     ElTabs: typeof import('element-plus/es')['ElTabs']
     ElTooltip: typeof import('element-plus/es')['ElTooltip']
     ElTree: typeof import('element-plus/es')['ElTree']
-    ElTreeV2: typeof import('element-plus/es')['ElTreeV2']
     ElUpload: typeof import('element-plus/es')['ElUpload']
     HightLight: typeof import('./src/components/HightLight/index.vue')['default']
     IconButton: typeof import('./src/components/IconButton/index.vue')['default']
@@ -112,7 +111,6 @@ declare global {
   const ElTabs: typeof import('element-plus/es')['ElTabs']
   const ElTooltip: typeof import('element-plus/es')['ElTooltip']
   const ElTree: typeof import('element-plus/es')['ElTree']
-  const ElTreeV2: typeof import('element-plus/es')['ElTreeV2']
   const ElUpload: typeof import('element-plus/es')['ElUpload']
   const HightLight: typeof import('./src/components/HightLight/index.vue')['default']
   const IconButton: typeof import('./src/components/IconButton/index.vue')['default']

+ 145 - 42
src/renderer/src/lvgl-widgets/menu/Config.vue

@@ -3,58 +3,110 @@
     <el-card class="mb-12px" body-class="pr-0px!">
       <template #header>
         <div class="flex items-center justify-between">
-          <span>选项卡</span>
-          <span class="flex gap-4px">
-            <LuPlus class="cursor-pointer" @click="handleAdd" size="14px" />
+          <span class="flex items-center gap-4px">
+            <span>菜单项</span>
+            <el-tooltip :content="inPage ? '退出到菜单' : '进入菜单页'">
+              <span
+                v-show="!sider"
+                class="flex items-center mr-4px cursor-pointer shrink-0"
+                @click.stop="handleChangeInPage"
+              >
+                <LuArrowLeftToLine v-if="inPage" size="14px" />
+                <LuArrowRightToLine v-else size="14px" />
+              </span>
+            </el-tooltip>
           </span>
+          <el-tooltip content="添加菜单片段">
+            <span class="flex gap-4px">
+              <LuPlus class="cursor-pointer" @click="handleAddSection" size="14px" />
+            </span>
+          </el-tooltip>
         </div>
       </template>
       <el-scrollbar height="120px">
         <div
-          v-for="(item, index) in props.values?.value.children || []"
+          v-for="(section, sectionIndex) in props.values?.value.children || []"
           :key="v4()"
-          class="flex items-center pr-12px"
-          @click="handleEdit(item)"
+          class="border-border border-solid border-1px p-4px bg-gray-800 mb-12px mr-12px rounded-4px"
         >
-          <span class="flex-1 truncate text-#00ff00 cursor-pointer">{{ item.title.text }}</span>
-          <LuTrash2 class="cursor-pointer shrink-0" @click.stop="handleDelete(index)" size="14px" />
+          <div class="flex items-center gap-8px">
+            <el-input
+              v-model="section.name"
+              type="textarea"
+              spellcheck="false"
+              :rows="1"
+              placeholder="子标题"
+            />
+            <el-tooltip content="添加菜单项">
+              <span class="flex gap-4px">
+                <LuPlus class="cursor-pointer" @click="handleAddItem(sectionIndex)" size="14px" />
+              </span>
+            </el-tooltip>
+            <LuTrash2
+              class="cursor-pointer shrink-0"
+              @click.stop="handleDeleteSection(sectionIndex)"
+              size="14px"
+            />
+          </div>
+          <div
+            v-for="(item, index) in section.children || []"
+            :key="v4()"
+            class="flex items-center justify-between"
+            @click="handleEdit(item)"
+          >
+            <span class="flex items-center">
+              <el-radio
+                v-model="activeIndex"
+                :value="`[${sectionIndex}].children.[${index}]`"
+                class="mr-0!"
+                @click.stop
+              />
+              <span class="flex-1 truncate text-#00ff00 cursor-pointer">{{ item.name }}</span>
+            </span>
+
+            <LuTrash2
+              class="cursor-pointer shrink-0"
+              @click.stop="handleDeleteItem(sectionIndex, index)"
+              size="14px"
+            />
+          </div>
         </div>
       </el-scrollbar>
     </el-card>
     <el-dialog v-model="dialogVisible" title="编辑" width="440px">
       <el-form ref="formRef" :model="formData" label-position="left" label-width="60px">
         <el-form-item label="类型">
-          <el-select v-model="formData.type">
+          <el-select v-model="formData.titleIcon.type">
             <el-option label="Symbol" value="symbol"></el-option>
             <el-option label="Image" value="img"></el-option>
             <el-option label="Text" value="text"></el-option>
           </el-select>
         </el-form-item>
         <el-form-item label="文本">
-          <el-input spellcheck="false" v-model="formData.text"></el-input>
+          <el-input spellcheck="false" v-model="formData.name"></el-input>
         </el-form-item>
         <el-form-item v-if="formData.type === 'symbol'" label="图标">
           <el-input spellcheck="false" readonly @click="handleShowSymbolModal">
             <template #prefix>
-              <i class="lvgl-icon not-italic" v-html="getSymbol(formData.img_symbol)"></i>
+              <i class="lvgl-icon not-italic" v-html="getSymbol(formData.titleIcon.img_symbol)"></i>
             </template>
           </el-input>
         </el-form-item>
-        <template v-if="formData.type === 'img'">
+        <template v-if="formData.titleIcon.type === 'img'">
           <el-form-item label="图片">
-            <ImageSelect v-model="formData.img_id" />
+            <ImageSelect v-model="formData.titleIcon.img_id" />
           </el-form-item>
           <el-row :gutter="12">
             <el-col :span="12">
               <el-form-item label="尺寸">
-                <input-number style="width: 100%" v-model="formData.img_width" :min="0">
+                <input-number style="width: 100%" v-model="formData.titleIcon.img_width" :min="0">
                   <template #prefix>宽度</template>
                 </input-number>
               </el-form-item>
             </el-col>
             <el-col :span="12">
               <el-form-item label-width="0">
-                <input-number style="width: 100%" v-model="formData.img_height" :min="0">
+                <input-number style="width: 100%" v-model="formData.titleIcon.img_height" :min="0">
                   <template #prefix>高度</template>
                 </input-number>
               </el-form-item>
@@ -65,20 +117,20 @@
               use-type="pure"
               picker-type="chrome"
               format="hex8"
-              v-model:pureColor="formData.img_recolor"
+              v-model:pureColor="formData.titleIcon.img_recolor"
             />
-            <span class="text-text-active">{{ formData.img_recolor }}</span>
+            <span class="text-text-active">{{ formData.titleIcon.img_recolor }}</span>
           </el-form-item>
           <el-form-item label="透明度" label-position="left" label-width="60px">
             <div class="w-full flex gap-20px items-center">
               <el-slider
-                v-model="formData.img_alpha"
+                v-model="formData.titleIcon.img_alpha"
                 :max="255"
                 :min="0"
                 style="flex: 1"
               ></el-slider>
               <span class="text-text-active inline w-30px cursor-pointer">
-                {{ formData.img_alpha }}
+                {{ formData.titleIcon.img_alpha }}
               </span>
             </div>
           </el-form-item>
@@ -95,10 +147,11 @@
 <script setup lang="ts">
 import type { MenuItem } from './data'
 
-import { type Ref, ref } from 'vue'
-import { LuPlus, LuTrash2 } from 'vue-icons-plus/lu'
+import { ref, computed, type Ref } from 'vue'
+import { LuPlus, LuTrash2, LuArrowRightToLine, LuArrowLeftToLine } from 'vue-icons-plus/lu'
 import { v4 } from 'uuid'
 import { symbols } from '@/constants'
+import { getNextIndex } from '@/utils'
 
 import ImageSelect from '@/views/designer/config/property/components/ImageSelect.vue'
 import SymbolSelectModal from '@/views/designer/config/property/components/SymbolSelectModal.vue'
@@ -109,25 +162,70 @@ const props = defineProps<{
 
 const dialogVisible = ref(false)
 const formData = ref<MenuItem>({
-  text: '',
-  img_id: '',
-  type: 'symbol',
-  img_width: 0,
-  img_height: 0,
-  img_alpha: 255,
-  img_recolor: '#ffffff00',
-  img_symbol: ''
+  name: '',
+  titleIcon: {
+    img_id: '',
+    type: 'symbol',
+    img_width: 0,
+    img_height: 0,
+    img_alpha: 255,
+    img_recolor: '#ffffff00',
+    img_symbol: ''
+  }
 })
 const symbolModalRef = ref<InstanceType<typeof SymbolSelectModal>>()
 
+// 当前激活
+const activeIndex = computed({
+  get() {
+    return props.values?.value?.props?.activeIndex
+  },
+  set(index: number) {
+    if (props.values?.value?.props) {
+      props.values.value.props.activeIndex = index
+    }
+  }
+})
+
+const sider = computed(() => props.values?.value?.props?.sider)
+
+const inPage = computed({
+  get() {
+    return props.values?.value?.props?.inPage
+  },
+  set(value: boolean) {
+    if (props.values?.value?.props) {
+      props.values.value.props.inPage = value
+    }
+  }
+})
+
 /**
- * 添加文本项
+ * 添加片段
  */
-const handleAdd = () => {
+const handleAddSection = () => {
+  const newIndex = getNextIndex(props.values?.value.children, 'name')
   props.values?.value.children?.push({
-    title: {
+    name: `section_${newIndex}`,
+    type: 'menu-section',
+    children: []
+  })
+}
+
+/**
+ * 删除菜单项
+ */
+const handleDeleteSection = (sectionIndex: number | string) => {
+  props.values?.value.children?.splice(sectionIndex, 1)
+}
+
+/**
+ * 添加文本项
+ */
+const handleAddItem = (sectionIndex: number | string) => {
+  props.values?.value.children?.[sectionIndex]?.children?.push({
+    titleIcon: {
       type: 'symbol',
-      text: 'item',
       img_id: '',
       img_width: 20,
       img_height: 20,
@@ -135,20 +233,25 @@ const handleAdd = () => {
       img_recolor: '#ffffff00',
       img_symbol: 'LV_SYMBOL_SETTINGS'
     },
+    name: 'item',
     type: 'menu',
     children: []
   })
 }
 
-const getSymbol = (symbol: string) => {
-  return symbols.find((item) => item.label === symbol)?.value
-}
-
 /**
  * 删除菜单项
  */
-const handleDelete = (index: number | string) => {
-  props.values?.value.children?.splice(index, 1)
+const handleDeleteItem = (sectionIndex: number | string, index: number | string) => {
+  props.values?.value.children?.[sectionIndex]?.children?.splice(index, 1)
+}
+
+const handleChangeInPage = () => {
+  inPage.value = !inPage.value
+}
+
+const getSymbol = (symbol: string) => {
+  return symbols.find((item) => item.label === symbol)?.value
 }
 
 const handleShowSymbolModal = () => {
@@ -159,11 +262,11 @@ const handleShowSymbolModal = () => {
  * 选择图标
  */
 const handleSelectSymbol = (val: string) => {
-  formData.value.img_symbol = val
+  formData.value.titleIcon.img_symbol = val
 }
 
-const handleEdit = (record: { title: MenuItem; [p: string]: any }) => {
-  formData.value = record.title
+const handleEdit = (record: MenuItem) => {
+  formData.value = record
   dialogVisible.value = true
 }
 </script>

+ 101 - 36
src/renderer/src/lvgl-widgets/menu/Menu.vue

@@ -1,43 +1,103 @@
 <template>
-  <div :style="styleMap?.mainStyle" class="w-full h-full flex overflow-hidden box-border">
+  <!-- 进入菜单页模式 -->
+  <div
+    v-if="!sider && inPage"
+    class="w-full h-full flex overflow-hidden box-border"
+    :style="styleMap?.mainStyle"
+    :class="titleMode === 'bottom_fixed' ? 'flex-col-reverse' : 'flex-col'"
+  >
     <div
-      class="shrink-0 box-border"
-      :class="expand ? 'flex-1! border-r-none!' : 'basis-1/3'"
+      class="w-full px-18px relative whitespace-pre! flex items-center box-border"
+      :style="styleMap?.menuTitleStyle"
+    >
+      <LuChevronLeft size="14px" />
+      <span class="flex-1">{{ get(children, activeIndex)?.name }}</span>
+    </div>
+    <div class="flex-1"></div>
+  </div>
+
+  <!-- 菜单 -->
+  <div v-else :style="styleMap?.mainStyle" class="w-full h-full flex overflow-hidden box-border">
+    <div
+      class="shrink-0 box-border relative flex flex-col"
+      :class="!sider ? 'flex-1! border-r-none!' : 'basis-1/3'"
       style="border-right: solid 1px #ededed"
+      :style="titleMode === 'bottom_fixed' ? 'flex-direction: column-reverse;' : ''"
     >
-      <div class="whitespace-pre!" :style="styleMap?.menuTitleStyle">{{ title }}</div>
-      <div :style="styleMap?.buttonBoxStyle" class="box-border overflow-hidden">
-        <div
-          v-for="({ title: item }, index) in children || []"
-          :key="index"
-          :style="activeIndex === index ? activeStyle : styleMap?.itemsStyle"
-          class="min-h-30px flex items-center justify-center gap-4px overflow-hidden"
-        >
-          <div class="min-w-80px flex items-center gap-4px">
-            <span v-if="item?.type === 'symbol'">
-              <i class="lvgl-icon not-italic" v-html="getSymbol(item.img_symbol)"></i>
-            </span>
+      <ImageBg :src="styleMap?.mainStyle?.imageSrc" :imageStyle="styleMap?.mainStyle?.imageStyle" />
+      <!-- 菜单标题 -->
+      <div class="relative whitespace-pre!" :style="styleMap?.menuTitleStyle">
+        <ImageBg
+          :src="styleMap?.menuTitleStyle?.imageSrc"
+          :imageStyle="styleMap?.menuTitleStyle?.imageStyle"
+        />
+        <span class="z-2">{{ title }}</span>
+      </div>
+      <!-- 侧边栏 -->
+      <div
+        :style="styleMap?.sidebarStyle"
+        :class="titleMode === 'bottom_fixed' ? 'flex-1' : ''"
+        class="relative box-border overflow-hidden"
+      >
+        <ImageBg
+          :src="styleMap?.sidebarStyle?.imageSrc"
+          :imageStyle="styleMap?.siderbaStyle?.imageStyle"
+        />
+        <!-- 菜单片段 -->
+        <template v-for="(section, sectionIndex) in children || []" :key="sectionIndex">
+          <div v-show="openSection" class="z-2 min-h-30px flex items-center">
+            <div class="w-full px-10px py-2px" :style="styleMap?.sectionTitleStyle">
+              {{ section.name }}
+            </div>
+          </div>
+          <div :style="styleMap?.sectionStyle" class="z-2 overflow-hidden">
             <div
-              v-if="item?.type === 'img'"
-              :id="item.img_id"
-              :style="{
-                width: item.img_width + 'px',
-                height: item.img_height + 'px',
-                position: 'relative'
-              }"
+              v-for="({ titleIcon, name }, index) in section.children || []"
+              :key="index"
+              :style="
+                activeIndex === `[${sectionIndex}].children.[${index}]`
+                  ? activeStyle
+                  : styleMap?.itemsStyle
+              "
+              class="relative min-h-30px flex items-center gap-4px overflow-hidden px-10px py-2px"
             >
               <ImageBg
-                :id="item.img_id"
-                :image-style="{ backgroundColor: item.img_recolor, opacity: item.img_alpha / 255 }"
-                :image-props="{ width: item.img_width + 'px', height: item.img_height + 'px' }"
+                :src="styleMap?.itemsStyle?.imageSrc"
+                :imageStyle="styleMap?.itemsStyle?.imageStyle"
               />
+              <div class="z-2 w-full min-w-80px flex items-center gap-4px mr-10px">
+                <span v-if="titleIcon?.type === 'symbol'">
+                  <i class="lvgl-icon not-italic" v-html="getSymbol(titleIcon.img_symbol)"></i>
+                </span>
+                <div
+                  v-if="titleIcon?.type === 'img'"
+                  :id="titleIcon.img_id"
+                  :style="{
+                    width: titleIcon.img_width + 'px',
+                    height: titleIcon.img_height + 'px',
+                    position: 'relative'
+                  }"
+                >
+                  <ImageBg
+                    :id="titleIcon.img_id"
+                    :image-style="{
+                      backgroundColor: titleIcon.img_recolor,
+                      opacity: titleIcon.img_alpha / 255
+                    }"
+                    :image-props="{
+                      width: titleIcon.img_width + 'px',
+                      height: titleIcon.img_height + 'px'
+                    }"
+                  />
+                </div>
+                <span class="flex-1 whitespace-pre!">{{ name }}</span>
+              </div>
             </div>
-            <span class="whitespace-pre!">{{ item.text }}</span>
           </div>
-        </div>
+        </template>
       </div>
     </div>
-    <div v-if="!expand" class="basis-2/3"></div>
+    <div v-if="sider" class="basis-2/3"></div>
   </div>
 </template>
 
@@ -45,9 +105,10 @@
 import { computed, type CSSProperties } from 'vue'
 import { useWidgetStyle, getStyle } from '../hooks/useWidgetStyle'
 import { useProjectStore } from '@/store/modules/project'
-import defaultStyle from './style.json'
+import { assign, get } from 'lodash-es'
 import { getSymbol } from '@/utils'
 
+import { LuChevronLeft } from 'vue-icons-plus/lu'
 import ImageBg from '../ImageBg.vue'
 
 const props = defineProps<{
@@ -59,11 +120,15 @@ const props = defineProps<{
   scrollbar: string
   title: string
   // 折叠展开
-  expand: boolean
+  sider: boolean
+  // 子标题开关
+  openSection: boolean
   // 子项
   children?: any[]
   // 当前激活index
-  activeIndex: number
+  activeIndex: string
+  titleMode?: 'top_fixed' | 'bottom_fixed'
+  inPage?: boolean
 }>()
 
 const styleMap = useWidgetStyle({
@@ -81,16 +146,16 @@ const activeStyle = computed(() => {
     (item) => item?.part?.name === 'items' && item.part?.state === 'checked'
   )
 
-  const globalItemStyle = projectStore.globalStyle
+  const stateItemStyle = projectStore.globalStyle
     ?.find((item) => item.widget === 'lv_menu')
     ?.part?.find((item) => item.partName === 'items')
     ?.state?.find((item) => item.state === 'checked')?.style
 
-  const defaultItemStyle = defaultStyle.part
-    .find((item) => item.partName === 'items')
-    ?.state.find((item) => item.state === 'checked')?.style
+  const defaultItemStyle = projectStore.globalStyle
+    ?.find((item) => item.widget === 'lv_menu')
+    ?.part?.find((item) => item.partName === 'items')?.defaultStyle
 
-  const style = itemStyle || globalItemStyle || defaultItemStyle
+  const style = assign({}, defaultItemStyle, stateItemStyle, itemStyle)
 
   let styleMap: CSSProperties = {}
 

+ 19 - 16
src/renderer/src/lvgl-widgets/menu/data.ts

@@ -2,20 +2,23 @@
  * 菜单项类型
  */
 export type MenuItem = {
-  // 类型
-  type: 'symbol' | 'text' | 'img'
-  // 文字内容
-  text: string
-  // 图片ID
-  img_id: string
-  // 图片宽度
-  img_width: number
-  // 图片高度
-  img_height: number
-  // 图片透明度
-  img_alpha: number
-  // 图片颜色
-  img_recolor: string
-  // symbol
-  img_symbol: string
+  name: string
+  titleIcon: {
+    // 类型
+    type: 'symbol' | 'text' | 'img'
+    // 图片ID
+    img_id: string
+    // 图片宽度
+    img_width: number
+    // 图片高度
+    img_height: number
+    // 图片透明度
+    img_alpha: number
+    // 图片颜色
+    img_recolor: string
+    // symbol
+    img_symbol: string
+  }
+  children?: any[]
+  type?: string
 }

+ 211 - 192
src/renderer/src/lvgl-widgets/menu/index.tsx

@@ -21,7 +21,15 @@ export default {
       stateList
     },
     {
-      name: 'buttonBox',
+      name: 'sidebar',
+      stateList
+    },
+    {
+      name: 'section',
+      stateList
+    },
+    {
+      name: 'sectionTitle',
       stateList
     },
     {
@@ -37,46 +45,52 @@ export default {
     name: 'menu',
     children: [
       {
-        title: {
-          type: 'symbol',
-          text: 'item_1',
-          img_id: '',
-          img_width: 20,
-          img_height: 20,
-          img_alpha: 255,
-          img_recolor: '#ffffff00',
-          img_symbol: 'LV_SYMBOL_SETTINGS'
-        },
-        type: 'menu',
-        children: []
-      },
-      {
-        title: {
-          type: 'symbol',
-          text: 'item_2',
-          img_id: '',
-          img_width: 20,
-          img_height: 20,
-          img_alpha: 255,
-          img_recolor: '#ffffff00',
-          img_symbol: 'LV_SYMBOL_AUDIO'
-        },
-        type: 'menu',
-        children: []
-      },
-      {
-        title: {
-          type: 'symbol',
-          text: 'item_3',
-          img_id: '',
-          img_width: 20,
-          img_height: 20,
-          img_alpha: 255,
-          img_recolor: '#ffffff00',
-          img_symbol: 'LV_SYMBOL_HOME'
-        },
-        type: 'menu',
-        children: []
+        type: 'menu-section',
+        name: 'section_1',
+        children: [
+          {
+            titleIcon: {
+              type: 'symbol',
+              img_id: '',
+              img_width: 20,
+              img_height: 20,
+              img_alpha: 255,
+              img_recolor: '#ffffff00',
+              img_symbol: 'LV_SYMBOL_SETTINGS'
+            },
+            name: 'item_1',
+            type: 'menu',
+            children: []
+          },
+          {
+            titleIcon: {
+              type: 'symbol',
+              img_id: '',
+              img_width: 20,
+              img_height: 20,
+              img_alpha: 255,
+              img_recolor: '#ffffff00',
+              img_symbol: 'LV_SYMBOL_AUDIO'
+            },
+            name: 'item_2',
+            type: 'menu',
+            children: []
+          },
+          {
+            titleIcon: {
+              type: 'symbol',
+              img_id: '',
+              img_width: 20,
+              img_height: 20,
+              img_alpha: 255,
+              img_recolor: '#ffffff00',
+              img_symbol: 'LV_SYMBOL_HOME'
+            },
+            name: 'item_3',
+            type: 'menu',
+            children: []
+          }
+        ]
       }
     ],
     props: {
@@ -88,16 +102,15 @@ export default {
       states: [],
       scrollbar: 'off',
       title: 'menu',
+      titleMode: 'top_fixed',
       // 折叠展开
-      expand: false,
+      sider: false,
+      // 子标题开关
+      openSection: true,
       // 当前激活index
-      activeIndex: 0,
-      openRotate: false,
-      rotate: {
-        x: 0,
-        y: 0,
-        angle: 0
-      }
+      activeIndex: '[0].children.[0]',
+      // 是否进入页面
+      inPage: false
     },
     styles: [
       {
@@ -106,10 +119,23 @@ export default {
           state: 'default'
         },
         background: {
-          color: '#ffffffff'
+          color: '#ffffffff',
+          image: {
+            imgId: '',
+            alpha: 255,
+            color: '#ffffff00'
+          }
         },
         border: {
-          radius: 0
+          color: '#000000ff',
+          width: 0,
+          radius: 0,
+          side: ['all']
+        },
+        outline: {
+          color: '#000000ff',
+          width: 0,
+          pad: 0
         },
         shadow: {
           color: '#2092f5ff',
@@ -117,27 +143,16 @@ export default {
           y: 0,
           spread: 0,
           width: 0
-        }
-      },
-      {
-        part: {
-          name: 'buttonBox',
-          state: 'default'
-        },
-        background: {
-          color: '#f6f6f6ff'
         },
-        border: {
-          color: '#32a0faff',
+        transform: {
           width: 0,
-          radius: 10,
-          side: ['all']
-        },
-        margin: {
-          left: 5,
-          right: 5,
-          top: 5,
-          bottom: 5
+          height: 0,
+          translateX: 0,
+          translateY: 0,
+          originX: 0,
+          originY: 0,
+          rotate: 0,
+          scale: 256
         }
       },
       {
@@ -146,42 +161,46 @@ export default {
           state: 'default'
         },
         background: {
-          color: '#ffffffff'
+          color: '#ffffffff',
+          image: {
+            imgId: '',
+            alpha: 255,
+            color: '#ffffff00'
+          }
         },
         text: {
-          color: '#171313ff',
+          color: '#000000ff',
           size: 12,
           family: 'xx',
-          weight: 'normal',
-          align: 'center'
+          align: 'left',
+          decoration: 'none'
         },
-        border: {
-          color: '#32a0ffff',
-          width: 0,
-          radius: 5,
-          side: ['bottom']
+        spacer: {
+          letterSpacing: 0
         }
       },
       {
         part: {
-          name: 'menuTitle',
-          state: 'default'
+          name: 'items',
+          state: 'checked'
         },
         background: {
-          color: '#ffffff'
+          color: '#d1e9fbff',
+          image: {
+            imgId: '',
+            alpha: 255,
+            color: '#ffffff00'
+          }
         },
         text: {
-          color: '#3b4250ff',
-          size: 14,
+          color: '#2196f3ff',
+          size: 12,
           family: 'xx',
-          weight: 'normal',
-          align: 'center'
+          align: 'left',
+          decoration: 'none'
         },
-        padding: {
-          top: 5,
-          bottom: 5,
-          left: 5,
-          right: 5
+        spacer: {
+          letterSpacing: 0
         }
       }
     ]
@@ -284,70 +303,38 @@ export default {
       {
         label: '标题',
         field: 'props.title',
-        valueType: 'text'
+        valueType: 'text',
+        labelWidth: '80px'
       },
       {
-        label: '折叠',
-        field: 'props.expand',
-        valueType: 'switch'
+        label: '标题模式',
+        field: 'props.titleMode',
+        valueType: 'select',
+        labelWidth: '80px',
+        componentProps: {
+          options: [
+            { label: 'Top Fixed', value: 'top_fixed' },
+            { label: 'Bottom Fixed', value: 'bottom_fixed' }
+          ]
+        }
       },
       {
-        label: '属性',
-        field: '',
-        render: (val) => {
-          return <Config values={val} />
-        }
+        label: '侧边栏',
+        field: 'props.sider',
+        labelWidth: '80px',
+        valueType: 'switch'
       },
       {
-        label: '旋转',
-        field: 'props.openRotate',
+        label: '子标题',
+        field: 'props.openSection',
+        labelWidth: '80px',
         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: '角度' }
-                    }
-                  ]
-                }
-              ]
-            : []
+        label: '属性',
+        field: '',
+        render: (val) => {
+          return <Config values={val} />
         }
       }
     ],
@@ -367,87 +354,119 @@ export default {
                 {
                   label: '背景',
                   field: 'background',
-                  valueType: 'background',
-                  componentProps: {
-                    onlyColor: true
-                  }
+                  valueType: 'background'
                 },
                 {
                   label: '边框',
                   field: 'border',
-                  valueType: 'border',
-                  componentProps: {
-                    onlyRadius: true
-                  }
+                  valueType: 'border'
+                },
+                {
+                  label: '轮廓',
+                  field: 'outline',
+                  valueType: 'outline'
                 },
                 {
                   label: '阴影',
                   field: 'shadow',
                   valueType: 'shadow'
+                },
+                {
+                  label: '变换',
+                  field: 'transform',
+                  valueType: 'transform'
                 }
               ]
-            : part?.name === 'buttonBox'
+            : part?.name === 'sidebar'
               ? [
                   {
                     label: '背景',
                     field: 'background',
-                    valueType: 'background',
-                    componentProps: {
-                      onlyColor: true
-                    }
+                    valueType: 'background'
                   },
                   {
                     label: '边框',
                     field: 'border',
                     valueType: 'border'
                   },
+                  {
+                    label: '内边距',
+                    field: 'padding',
+                    valueType: 'padding'
+                  },
                   {
                     label: '外边距',
                     field: 'margin',
                     valueType: 'margin'
                   }
                 ]
-              : part?.name === 'items'
+              : part?.name === 'section'
                 ? [
-                    {
-                      label: '背景',
-                      field: 'background',
-                      valueType: 'background',
-                      componentProps: {
-                        onlyColor: true
-                      }
-                    },
-                    {
-                      label: '字体',
-                      field: 'text',
-                      valueType: 'font'
-                    },
                     {
                       label: '边框',
                       field: 'border',
                       valueType: 'border'
                     }
                   ]
-                : [
-                    {
-                      label: '背景',
-                      field: 'background',
-                      valueType: 'background',
-                      componentProps: {
-                        onlyColor: true
+                : part?.name === 'sectionTitle'
+                  ? [
+                      {
+                        label: '字体',
+                        field: 'text',
+                        valueType: 'font'
+                      },
+                      {
+                        label: '间距',
+                        field: 'spacer',
+                        valueType: 'spacer',
+                        componentProps: {
+                          hideLineHeight: true
+                        }
                       }
-                    },
-                    {
-                      label: '字体',
-                      field: 'text',
-                      valueType: 'font'
-                    },
-                    {
-                      label: '内边距',
-                      field: 'padding',
-                      valueType: 'padding'
-                    }
-                  ]
+                    ]
+                  : part?.name === 'items'
+                    ? [
+                        {
+                          label: '背景',
+                          field: 'background',
+                          valueType: 'background'
+                        },
+                        {
+                          label: '字体',
+                          field: 'text',
+                          valueType: 'font'
+                        },
+                        {
+                          label: '间距',
+                          field: 'spacer',
+                          valueType: 'spacer',
+                          componentProps: {
+                            hideLineHeight: true
+                          }
+                        }
+                      ]
+                    : [
+                        {
+                          label: '背景',
+                          field: 'background',
+                          valueType: 'background'
+                        },
+                        {
+                          label: '字体',
+                          field: 'text',
+                          valueType: 'font'
+                        },
+                        {
+                          label: '边框',
+                          field: 'border',
+                          valueType: 'border'
+                        },
+                        {
+                          label: '内边距',
+                          field: 'padding',
+                          valueType: 'padding'
+                        }
+                      ]
         }
       }
     ]

+ 149 - 90
src/renderer/src/lvgl-widgets/menu/style.json

@@ -4,94 +4,146 @@
   "part": [
     {
       "partName": "main",
-      "state": [
-        {
-          "state": "default",
-          "style": {
-            "background": {
-              "color": "#ffffffff"
-            },
-            "border": {
-              "radius": 0
-            },
-            "shadow": {
-              "color": "#2092f5ff",
-              "x": 0,
-              "y": 0,
-              "spread": 0,
-              "width": 0
-            }
+      "defaultStyle": {
+        "background": {
+          "color": "#ffffffff",
+          "image": {
+            "imgId": "",
+            "alpha": 255,
+            "color": "#ffffff00"
           }
+        },
+        "border": {
+          "color": "#000000ff",
+          "width": 0,
+          "radius": 0,
+          "side": ["all"]
+        },
+        "outline": {
+          "color": "#000000ff",
+          "width": 0,
+          "pad": 0
+        },
+        "shadow": {
+          "color": "#2092f5ff",
+          "x": 0,
+          "y": 0,
+          "spread": 0,
+          "width": 0
+        },
+        "transform": {
+          "width": 0,
+          "height": 0,
+          "translateX": 0,
+          "translateY": 0,
+          "originX": 0,
+          "originY": 0,
+          "rotate": 0,
+          "scale": 256
         }
-      ]
+      },
+      "state": []
     },
     {
-      "partName": "buttonBox",
-      "state": [
-        {
-          "state": "default",
-          "style": {
-            "background": {
-              "color": "#f6f6f6ff"
-            },
-            "border": {
-              "color": "#32a0faff",
-              "width": 0,
-              "radius": 10,
-              "side": ["all"]
-            },
-            "margin": {
-              "left": 5,
-              "right": 5,
-              "top": 5,
-              "bottom": 5
-            }
+      "partName": "sidebar",
+      "defaultStyle": {
+        "background": {
+          "color": "#f5f5f5ff",
+          "image": {
+            "imgId": "",
+            "alpha": 255,
+            "color": "#ffffff00"
           }
+        },
+        "border": {
+          "color": "#32a0faff",
+          "width": 0,
+          "radius": 0,
+          "side": ["all"]
+        },
+        "padding": {
+          "left": 10,
+          "right": 10,
+          "top": 0,
+          "bottom": 4
+        },
+        "margin": {
+          "left": 0,
+          "right": 0,
+          "top": 0,
+          "bottom": 0
         }
-      ]
+      },
+      "state": []
+    },
+    {
+      "partName": "section",
+      "defaultStyle": {
+        "border": {
+          "color": "#32a0faff",
+          "width": 0,
+          "radius": 10,
+          "side": ["all"]
+        }
+      },
+      "state": []
+    },
+    {
+      "partName": "sectionTitle",
+      "defaultStyle": {
+        "text": {
+          "color": "#000000ff",
+          "size": 14,
+          "family": "xx",
+          "align": "center",
+          "decoration": "none"
+        },
+        "spacer": {
+          "letterSpacing": 0
+        }
+      },
+      "state": []
     },
     {
       "partName": "items",
-      "state": [
-        {
-          "state": "default",
-          "style": {
-            "background": {
-              "color": "#ffffffff"
-            },
-            "text": {
-              "color": "#171313ff",
-              "size": 12,
-              "family": "xx",
-              "weight": "normal",
-              "align": "center"
-            },
-            "border": {
-              "color": "#32a0ffff",
-              "width": 0,
-              "radius": 5,
-              "side": ["bottom"]
-            }
+      "defaultStyle": {
+        "background": {
+          "color": "#ffffffff",
+          "image": {
+            "imgId": "",
+            "alpha": 255,
+            "color": "#ffffff00"
           }
         },
+        "text": {
+          "color": "#000000ff",
+          "size": 12,
+          "family": "xx",
+          "align": "left",
+          "decoration": "none"
+        },
+        "spacer": {
+          "letterSpacing": 0
+        }
+      },
+      "state": [
         {
           "state": "checked",
           "style": {
             "background": {
-              "color": "#19a5ff3c"
+              "color": "#d1e9fbff",
+              "image": {
+                "imgId": "",
+                "alpha": 255,
+                "color": "#ffffff00"
+              }
             },
             "text": {
-              "color": "#9ab700ff",
+              "color": "#2196f3ff",
               "size": 12,
               "family": "xx",
-              "weight": "normal",
-              "align": "center"
-            },
-            "border": {
-              "color": "#32a0ffff",
-              "width": 0,
-              "radius": 5,
-              "side": ["bottom"]
+              "align": "left",
+              "decoration": "none"
             }
           }
         }
@@ -99,29 +151,36 @@
     },
     {
       "partName": "menuTitle",
-      "state": [
-        {
-          "state": "default",
-          "style": {
-            "background": {
-              "color": "#ffffff"
-            },
-            "text": {
-              "color": "#3b4250ff",
-              "size": 14,
-              "family": "xx",
-              "weight": "normal",
-              "align": "center"
-            },
-            "padding": {
-              "top": 5,
-              "bottom": 5,
-              "left": 5,
-              "right": 5
-            }
+      "defaultStyle": {
+        "background": {
+          "color": "#ffffffff",
+          "image": {
+            "imgId": "",
+            "alpha": 255,
+            "color": "#ffffff00"
           }
+        },
+        "text": {
+          "color": "#000000ff",
+          "size": 14,
+          "family": "xx",
+          "align": "center",
+          "decoration": "none"
+        },
+        "border": {
+          "color": "#2092f5ff",
+          "width": 0,
+          "radius": 0,
+          "side": ["all"]
+        },
+        "padding": {
+          "top": 10,
+          "bottom": 10,
+          "left": 18,
+          "right": 18
         }
-      ]
+      },
+      "state": []
     }
   ]
 }

+ 3 - 3
src/renderer/src/lvgl-widgets/table/index.tsx

@@ -25,9 +25,9 @@ export default {
       stateList
     }
   ],
-  onChangeSize: (props: any) => {
-    console.log('修改宽高')
-  },
+  // onChangeSize: (props: any) => {
+  //   console.log('修改宽高')
+  // },
   defaultSchema: {
     name: 'table',
     props: {

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

@@ -124,6 +124,12 @@ export interface IComponentModelConfig {
    * @returns
    */
   onChangeSize?: (props: any, ...args) => void
+  /**
+   * 位置触发事件
+   * @param props
+   * @returns
+   */
+  onChangePosition?: (props: any, ...args) => void
 }
 
 /**

+ 1 - 0
src/renderer/src/store/modules/action.ts

@@ -350,6 +350,7 @@ export const useActionStore = defineStore('action', () => {
         child.children.splice(index, 1)
       }
     })
+    projectStore.activeWidgets = []
   }
 
   /**

+ 3 - 3
src/renderer/src/store/modules/project.ts

@@ -181,7 +181,7 @@ export const useProjectStore = defineStore('project', () => {
     await window.electron.ipcRenderer.invoke(
       'write-file',
       `${meta.path}\\${meta.name}\\project.ui`,
-      JSON.stringify(project.value)
+      JSON.stringify(project.value, null, 2)
     )
 
     // 全局样式
@@ -193,7 +193,7 @@ export const useProjectStore = defineStore('project', () => {
     await window.electron.ipcRenderer.invoke(
       'write-file',
       `${meta.path}\\${meta.name}\\style.ui`,
-      JSON.stringify(globalStyle.value)
+      JSON.stringify(globalStyle.value, null, 2)
     )
 
     imageCompressFormat.value = meta.imageCompress
@@ -247,7 +247,7 @@ export const useProjectStore = defineStore('project', () => {
     await window.electron.ipcRenderer.invoke(
       'write-file',
       `${projectPath.value}\\project.ui`,
-      JSON.stringify(project.value)
+      JSON.stringify(project.value, null, 2)
     )
     // 2、更新修改时间
     recentProjectStore.updateProject({

+ 3 - 2
src/renderer/src/views/designer/config/property/components/StyleSpace.vue

@@ -1,6 +1,6 @@
 <template>
   <el-row :gutter="12">
-    <el-col v-if="!hideLetterSpacing" :span="12">
+    <el-col v-if="!hideLetterSpacing" :span="hideLineHeight ? 24 : 12">
       <el-form-item label="" label-position="left" label-width="0px">
         <input-number
           v-model="lineHeight"
@@ -15,7 +15,7 @@
         </input-number>
       </el-form-item>
     </el-col>
-    <el-col :span="hideLetterSpacing ? 24 : 12">
+    <el-col v-if="!hideLineHeight" :span="hideLetterSpacing ? 24 : 12">
       <el-form-item label="" label-position="left" label-width="0px">
         <input-number
           v-model="letterSpacing"
@@ -43,6 +43,7 @@ const modelValue = defineModel<{
 
 defineProps<{
   hideLetterSpacing?: boolean
+  hideLineHeight?: boolean
 }>()
 
 // letterSpacing

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

@@ -51,7 +51,7 @@ import LvglWidgets from '@/lvgl-widgets'
 import { useProjectStore } from '@/store/modules/project'
 import { useAppStore } from '@/store/modules/app'
 import { useActionStore } from '@/store/modules/action'
-import { has, isEmpty } from 'lodash-es'
+import { has, isEmpty, get } from 'lodash-es'
 
 import ContextMenu from './ContextMenu.vue'
 import { getAddWidgetIndex, isDescendant } from '@/utils'
@@ -94,7 +94,7 @@ const children = computed(() => {
   const { schema } = props
 
   return has(schema.props, 'activeIndex')
-    ? schema.children?.[schema.props.activeIndex]?.children || []
+    ? get(schema.children, schema.props.activeIndex)?.children || []
     : schema.children
 })
 
@@ -231,7 +231,8 @@ useDrop(nodeRef, {
 
     // 添加到前面
     if (has(schema.props, 'activeIndex')) {
-      schema.children[schema.props.activeIndex].children?.push(newWidget)
+      const parent = get(schema.children, schema.props.activeIndex)
+      parent?.children?.push(newWidget)
     } else {
       schema.children?.push(newWidget)
     }
@@ -280,7 +281,8 @@ watch(nodeState, (state) => {
       actionStore.onDeleteById(node.id)
       // 添加到前面
       if (has(schema.props, 'activeIndex')) {
-        schema.children[schema.props.activeIndex].children?.push(node)
+        const parent = get(schema.children, schema.props.activeIndex)
+        parent?.children?.push(node)
       } else {
         schema.children?.push(node)
       }