Przeglądaj źródła

feat: 添加menu滚动条,menu,tab子节点容器计算

jiaxing.liao 3 tygodni temu
rodzic
commit
be7e437f05

+ 68 - 48
src/renderer/src/lvgl-widgets/menu/Menu.vue

@@ -7,7 +7,7 @@
     :class="titleMode === 'bottom_fixed' ? 'flex-col-reverse' : 'flex-col'"
   >
     <div
-      class="w-full px-18px relative whitespace-pre! flex items-center box-border"
+      class="menu-header w-full px-18px relative whitespace-pre! flex items-center box-border"
       :style="styleMap?.menuTitleStyle"
     >
       <LuChevronLeft size="14px" />
@@ -26,7 +26,7 @@
     >
       <ImageBg :src="styleMap?.mainStyle?.imageSrc" :imageStyle="styleMap?.mainStyle?.imageStyle" />
       <!-- 菜单标题 -->
-      <div class="relative whitespace-pre!" :style="styleMap?.menuTitleStyle">
+      <div class="menu-header relative whitespace-pre!" :style="styleMap?.menuTitleStyle">
         <ImageBg
           :src="styleMap?.menuTitleStyle?.imageSrc"
           :imageStyle="styleMap?.menuTitleStyle?.imageStyle"
@@ -35,66 +35,74 @@
       </div>
       <!-- 侧边栏 -->
       <div
+        ref="sidebarRef"
         :style="styleMap?.sidebarStyle"
         :class="titleMode === 'bottom_fixed' ? 'flex-1' : ''"
-        class="relative box-border overflow-hidden"
+        class="menu-sidebar 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 ref="contentRef" class="z-2">
+          <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>
-          <div :style="styleMap?.sectionStyle" class="z-2 overflow-hidden">
-            <div
-              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
-                :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
+            <div :style="styleMap?.sectionStyle" class="z-2 overflow-hidden">
+              <div
+                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
+                  :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"
-                    :image-style="{
-                      backgroundColor: titleIcon.img_recolor,
-                      opacity: titleIcon.img_alpha / 255
-                    }"
-                    :image-props="{
+                    :style="{
                       width: titleIcon.img_width + 'px',
-                      height: titleIcon.img_height + '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>
-                <span class="flex-1 whitespace-pre!">{{ name }}</span>
               </div>
             </div>
-          </div>
-        </template>
+          </template>
+        </div>
+        <!-- 滚动条 -->
+        <div
+          v-if="showVScroll"
+          class="v-scrollbar absolute right-6px top-6px w-4px h-20% bg-#d7d7d7"
+        ></div>
       </div>
     </div>
     <div v-if="sider" class="basis-2/3"></div>
@@ -102,11 +110,12 @@
 </template>
 
 <script setup lang="ts">
-import { computed, type CSSProperties } from 'vue'
+import { computed, ref, type CSSProperties } from 'vue'
 import { useWidgetStyle, getStyle } from '../hooks/useWidgetStyle'
 import { useProjectStore } from '@/store/modules/project'
 import { assign, get } from 'lodash-es'
 import { getSymbol } from '@/utils'
+import { useResizeObserver } from 'vue-hooks-plus'
 
 import { LuChevronLeft } from 'vue-icons-plus/lu'
 import ImageBg from '../ImageBg.vue'
@@ -136,6 +145,17 @@ const styleMap = useWidgetStyle({
   props
 })
 
+const sidebarRef = ref<HTMLDivElement>()
+const contentRef = ref<HTMLDivElement>()
+const showVScroll = ref(false)
+
+useResizeObserver(contentRef, (entries) => {
+  const entry = entries[0]
+  const { height: _h } = entry.contentRect
+  const boxHeight = sidebarRef.value?.offsetHeight
+  showVScroll.value = !!boxHeight && _h > boxHeight
+})
+
 const projectStore = useProjectStore()
 
 // 激活样式

+ 29 - 0
src/renderer/src/lvgl-widgets/menu/index.tsx

@@ -5,6 +5,7 @@ import type { IComponentModelConfig } from '../type'
 import i18n from '@/locales'
 import defaultStyle from './style.json'
 import Config from './Config.vue'
+import { CSSProperties } from 'vue'
 
 export default {
   label: i18n.global.t('menu'),
@@ -470,5 +471,33 @@ export default {
         }
       }
     ]
+  },
+  getChildStyle: (schema) => {
+    const { width, height, sider, titleMode, inPage } = schema.props
+
+    const el = document.querySelector('[widget-id="' + schema.id + '"]')
+    const headerH = el?.querySelector('.menu-header')?.clientHeight || 0
+    const sidebarW = el?.querySelector('.menu-sidebar')?.clientWidth || 0
+
+    let style: CSSProperties = {}
+
+    if (titleMode === 'top_fixed') {
+      style = {
+        top: headerH + 'px',
+        height: height - headerH + 'px'
+      }
+    }
+    if (sider) {
+      style = {
+        top: 0,
+        left: sidebarW + 'px',
+        width: width - sidebarW + 'px'
+      }
+    }
+
+    return {
+      display: !sider && !inPage ? 'none' : '',
+      ...style
+    }
   }
 } as IComponentModelConfig

+ 21 - 7
src/renderer/src/lvgl-widgets/tabview/Tabview.vue

@@ -5,14 +5,26 @@
       :src="styleMap?.mainStyle?.imageSrc"
       :imageStyle="styleMap?.mainStyle?.imageStyle"
     />
-    <div class="flex box-border" :style="tabMainStyle">
+    <div class="lvtab-header flex box-border" :style="tabMainStyle">
       <div
-        class="flex-1 shrink-0 grid place-items-center box-border overflow-hidden whitespace-pre!"
+        class="flex-1 shrink-0 box-border overflow-hidden whitespace-pre!"
         v-for="(item, index) in children || []"
         :key="index"
         :style="activeIndex === index ? activeStyle : styleMap?.itemsStyle"
       >
-        {{ item.name }}
+        <div
+          class="w-full h-full grid place-items-center box-border"
+          :style="
+            activeIndex === index
+              ? {
+                  borderBottom: '2px solid',
+                  borderBottomColor: activeStyle?.color || '#000'
+                }
+              : null
+          "
+        >
+          {{ item.name }}
+        </div>
       </div>
     </div>
     <div class="w-auto h-auto flex-1 p-10px">
@@ -27,7 +39,7 @@
 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 } from 'lodash-es'
 
 import ImageBg from '../ImageBg.vue'
 
@@ -83,14 +95,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_tabview')
     ?.part?.find((item) => item.partName === 'items')
     ?.state?.find((item) => item.state === 'checked')?.style
 
-  const defaultItemStyle = defaultStyle.part.find((item) => item.partName === 'items')?.defaultStyle
+  const defaultItemStyle = projectStore.globalStyle
+    ?.find((item) => item.widget === 'lv_tabview')
+    ?.part?.find((item) => item.partName === 'items')?.defaultStyle
 
-  const style = itemStyle || globalItemStyle || defaultItemStyle
+  const style = assign({}, defaultItemStyle, stateItemStyle, itemStyle)
 
   let styleMap: CSSProperties = {}
 

+ 25 - 41
src/renderer/src/lvgl-widgets/tabview/index.tsx

@@ -5,6 +5,7 @@ import type { IComponentModelConfig } from '../type'
 import i18n from '@/locales'
 import defaultStyle from './style.json'
 import Config from './Config.vue'
+import { CSSProperties } from 'vue'
 
 export default {
   label: i18n.global.t('tabview'),
@@ -117,47 +118,6 @@ export default {
           rotate: 0,
           scale: 256
         }
-      },
-      {
-        part: {
-          name: 'tab',
-          state: 'default'
-        },
-        background: {
-          color: '#ffffffff'
-        },
-        border: {
-          color: '#2092f5ff',
-          width: 0,
-          radius: 0,
-          side: ['all']
-        }
-      },
-      {
-        part: {
-          name: 'items',
-          state: 'default'
-        },
-        background: {
-          color: '#ffffffff'
-        },
-        text: {
-          color: '#4d4d4dff',
-          size: 16,
-          family: 'xx',
-          align: 'center',
-          decoration: 'none'
-        },
-        spacer: {
-          letterSpacing: 0,
-          lineHeight: 0
-        },
-        border: {
-          color: '#2092f500',
-          width: 4,
-          radius: 0,
-          side: ['bottom']
-        }
       }
     ]
   },
@@ -379,5 +339,29 @@ export default {
         }
       }
     ]
+  },
+  getChildStyle: (schema) => {
+    const { width, height, position } = schema.props
+
+    const el = document.querySelector('[widget-id="' + schema.id + '"]')
+    const headerH = el?.querySelector('.lvtab-header')?.clientHeight || 0
+    const headerW = el?.querySelector('.lvtab-sidebar')?.clientWidth || 0
+
+    let style: CSSProperties = {}
+
+    if (position === 'top') {
+      style = {
+        top: headerH + 'px',
+        height: height - headerH + 'px'
+      }
+    }
+    if (position === 'left') {
+      style = {
+        left: headerW + 'px',
+        width: width - headerW + 'px'
+      }
+    }
+
+    return style
   }
 } as IComponentModelConfig

+ 2 - 2
src/renderer/src/lvgl-widgets/tabview/style.json

@@ -88,8 +88,8 @@
           "lineHeight": 0
         },
         "border": {
-          "color": "#2092f500",
-          "width": 4,
+          "color": "#2092f5ff",
+          "width": 0,
           "radius": 0,
           "side": ["bottom"]
         }

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

@@ -1,3 +1,5 @@
+import { CSSProperties } from 'vue'
+
 type PartItem = {
   name: string
   stateList: string[]
@@ -130,6 +132,10 @@ export interface IComponentModelConfig {
    * @returns
    */
   onChangePosition?: (props: any, ...args) => void
+  /**
+   * 获取子节点样式
+   */
+  getChildStyle?: (props: any) => CSSProperties
 }
 
 /**

+ 1 - 0
src/renderer/src/views/designer/workspace/stage/Moveable.vue

@@ -34,6 +34,7 @@
     :verticalGuidelines="verticalGuidelines"
     :horizontalGuidelines="horizontalGuidelines"
     :controlPadding="4"
+    :linePadding="10"
     :edge="true"
     :individualGroupable="true"
     :individualGroupableProps="individualGroupableProps"

+ 54 - 31
src/renderer/src/views/designer/workspace/stage/Node.vue

@@ -16,7 +16,11 @@
       :is="widget"
       :style="{ 'pointer-events': schema.type === 'page' ? 'none' : '' }"
     />
-    <div v-if="schema.children" class="absolute left-0 top-0 w-full h-full" :style="layoutStyle">
+    <div
+      v-if="schema.children"
+      class="absolute left-0 top-0 w-full h-full"
+      :style="{ ...layoutStyle, ...dropStyle }"
+    >
       <!-- 子节点 -->
       <NodeItem
         v-for="(child, index) in children"
@@ -44,7 +48,7 @@ import type { Page } from '@/types/page'
 import type { StageState } from './type'
 import type { CSSProperties } from 'vue'
 
-import { computed, ref, inject, watch } from 'vue'
+import { computed, ref, inject, watch, nextTick } from 'vue'
 import { useDrop, useMouse, useEventListener } from 'vue-hooks-plus'
 import { createWidget } from '@/model'
 import LvglWidgets from '@/lvgl-widgets'
@@ -157,44 +161,63 @@ const getStyle = computed((): CSSProperties => {
     height: schema.props?.height * scale + 'px',
     zIndex: zIndex.value,
     ...style,
-    ...other,
-    ...dropStyle.value
+    ...other
+    // ...dropStyle.value
   }
 })
 
 /**
  * 布局样式
  */
-const layoutStyle = computed((): CSSProperties => {
-  const { schema } = props
-
-  if (schema.props?.layout === 'flex' && schema.props?.flex) {
-    const { flex } = schema.props
-    const directionMap = {
-      row: 'row',
-      column: 'column',
-      'row-wrap': 'row',
-      'column-wrap': 'column',
-      'row-reverse': 'row-reverse',
-      'column-reverse': 'column-reverse',
-      'row-wrap-reverse': 'row-reverse',
-      'column-wrap-reverse': 'column-reverse'
+const layoutStyle = ref<CSSProperties>({})
+watch(
+  () => props.schema.props,
+  async () => {
+    await nextTick()
+    const { schema } = props
+    let style: CSSProperties = {}
+    layoutStyle.value = {}
+
+    if (schema.props?.layout === 'flex' && schema.props?.flex) {
+      const { flex } = schema.props
+      const directionMap = {
+        row: 'row',
+        column: 'column',
+        'row-wrap': 'row',
+        'column-wrap': 'column',
+        'row-reverse': 'row-reverse',
+        'column-reverse': 'column-reverse',
+        'row-wrap-reverse': 'row-reverse',
+        'column-wrap-reverse': 'column-reverse'
+      }
+      style = {
+        display: 'flex',
+        overflow: 'hidden',
+        flexDirection: directionMap?.[flex?.direction],
+        flexWrap: flex?.direction?.includes('wrap') ? 'wrap' : 'nowrap',
+        justifyContent: flex?.mainAxisAlign,
+        alignItems: flex?.crossAxisAlign,
+        alignContent: flex?.trackAxisAlign,
+        'row-gap': flex?.rowGap ? `${flex.rowGap}px` : 0,
+        'column-gap': flex?.columnGap ? `${flex.columnGap}px` : 0
+      }
     }
-    return {
-      display: 'flex',
-      overflow: 'hidden',
-      flexDirection: directionMap?.[flex?.direction],
-      flexWrap: flex?.direction?.includes('wrap') ? 'wrap' : 'nowrap',
-      justifyContent: flex?.mainAxisAlign,
-      alignItems: flex?.crossAxisAlign,
-      alignContent: flex?.trackAxisAlign,
-      'row-gap': flex?.rowGap ? `${flex.rowGap}px` : 0,
-      'column-gap': flex?.columnGap ? `${flex.columnGap}px` : 0
+
+    // 获取子节点容器样式
+    const getChildStyle = LvglWidgets[props.schema.type]?.getChildStyle
+    if (typeof getChildStyle === 'function') {
+      style = {
+        ...style,
+        ...getChildStyle(props.schema)
+      }
     }
+    layoutStyle.value = style
+  },
+  {
+    deep: true,
+    immediate: true
   }
-
-  return {}
-})
+)
 
 /**
  * 子元素样式