Jelajahi Sumber

feat: 添加本地文件加载协议、完善image控件绑定

jiaxing.liao 2 bulan lalu
induk
melakukan
69358826e2

+ 8 - 2
src/main/index.ts

@@ -1,4 +1,4 @@
-import { app, shell, BrowserWindow, ipcMain } from 'electron'
+import { app, shell, BrowserWindow, ipcMain, protocol, net as electronNet } from 'electron'
 import { join } from 'path'
 import { electronApp, optimizer, is } from '@electron-toolkit/utils'
 import icon from '../../resources/icon.png?asset'
@@ -16,7 +16,8 @@ function createWindow(): void {
     ...(process.platform === 'linux' ? { icon } : {}),
     webPreferences: {
       preload: join(__dirname, '../preload/index.js'),
-      sandbox: false
+      sandbox: false,
+      webSecurity: false
     }
   })
 
@@ -89,6 +90,11 @@ app.whenReady().then(() => {
   ipcMain.on('ping', () => console.log('pong'))
   // 文件处理
   handleFile()
+  // 注册文件协议
+  protocol.handle('local', (request) => {
+    const filePath = request.url.slice('local://'.length)
+    return electronNet.fetch(`file://${filePath}`.toString())
+  })
 
   createWindow()
 

+ 1 - 1
src/renderer/index.html

@@ -6,7 +6,7 @@
     <!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
     <meta
       http-equiv="Content-Security-Policy"
-      content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:"
+      content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: local: http: https:;"
     />
     <style>
       body {

+ 4 - 15
src/renderer/src/components/LocalImage/index.vue

@@ -1,5 +1,5 @@
 <template>
-  <el-image v-loading="loading" :src="imageSrc" v-bind="$attrs"></el-image>
+  <img v-loading="loading" :src="`local:///${(imageSrc || src)?.replaceAll('\\', '/')}`" v-bind="$attrs"></img>
 </template>
 
 <script setup lang="ts">
@@ -18,23 +18,12 @@ const projectStore = useProjectStore()
 // 读取文件
 const readFile = async () => {
   if (!props.src && !props.id) return
-
-  let src: string = props.src || ''
-
   if (props.id) {
     const path =
       projectStore.project?.resources.images.find((item) => item.id === props.id)?.path || ''
-    src = projectStore.projectPath + path
-  }
-
-  loading.value = true
-  try {
-    const res = await window.electron.ipcRenderer.invoke('read-file', src, 'base64')
-    imageSrc.value = `data:image/${src.split('.')[1]};base64,` + res
-  } catch (error) {
-    console.log(error)
-  } finally {
-    loading.value = false
+    imageSrc.value = projectStore.projectPath + path
+  } else {
+    imageSrc.value = ''
   }
 }
 

+ 6 - 10
src/renderer/src/lvgl-widgets/hooks/useWidgetStyle.ts

@@ -1,5 +1,4 @@
 import componentMap from '..'
-import { getImageByPath } from '@/utils'
 import { useProjectStore } from '@/store/modules/project'
 import { assign } from 'lodash-es'
 import { ref, watch } from 'vue'
@@ -188,15 +187,12 @@ export const useWidgetStyle = (param: StyleParam) => {
             (item) => item.id === style?.[key]?.image?.imgId
           )?.path
           if (basePath && imagePath) {
-            getImageByPath(basePath + imagePath).then((res) => {
-              if (res?.base64) {
-                styleMap.value[`${partItem.name}Style`].imageSrc = res.base64
-                styleMap.value[`${partItem.name}Style`].imageStyle = {
-                  backgroundColor: style?.[key]?.image?.color,
-                  opacity: (style?.[key]?.image?.alpha || 255) / 255
-                }
-              }
-            })
+            styleMap.value[`${partItem.name}Style`].imageSrc =
+              `local:///${(basePath + imagePath).replaceAll('\\', '/')}`
+            styleMap.value[`${partItem.name}Style`].imageStyle = {
+              backgroundColor: style?.[key]?.image?.color,
+              opacity: (style?.[key]?.image?.alpha || 255) / 255
+            }
           }
         }
         // 图片样式

+ 1 - 5
src/renderer/src/lvgl-widgets/image-button/ImageButton.vue

@@ -59,11 +59,7 @@ watch(
       const basePath = projectStore.project.meta.path
       const imagePath = projectStore.project.resources.images.find((item) => item.id === val)?.path
       const result = await getImageByPath(basePath + imagePath)
-      if (result?.base64) {
-        src.value = result.base64
-      } else {
-        src.value = ''
-      }
+      src.value = result?.src!
     } else {
       src.value = ''
     }

+ 44 - 17
src/renderer/src/lvgl-widgets/image/Image.vue

@@ -1,6 +1,10 @@
 <template>
   <div :style="boxStyle" class="overflow-hidden flex items-center justify-center">
-    <img :style="imgStyle" :src="src || defaultImg" alt="img" />
+    <ImageBg
+      :src="src || defaultImg"
+      :image-style="styleMap?.mainStyle?.imageStyle"
+      :image-props="imageProps"
+    />
   </div>
 </template>
 
@@ -9,22 +13,53 @@ import { computed, watch, ref } from 'vue'
 import { getImageByPath } from '@/utils'
 import { useProjectStore } from '@/store/modules/project'
 import defaultImg from '@/assets/default.png'
+import ImageBg from '../ImageBg.vue'
+import { useWidgetStyle } from '../hooks/useWidgetStyle'
 
 const props = defineProps<{
   width: number
   height: number
   image: string
+  styles: any
+  part?: string
+  state?: string
   rotate: {
     x: number
     y: number
     angle: number
   }
-  styles: any
-  state?: string
+  openScale: boolean
+  scale: number
+  antiAliasing: boolean
 }>()
 
 const src = ref('')
 const projectStore = useProjectStore()
+const width = ref(props.width)
+const height = ref(props.height)
+
+const styleMap = useWidgetStyle({
+  widget: 'lv_image',
+  props
+})
+
+const imageProps = computed(() => {
+  const { openScale, scale = 256, image } = props
+  const s = scale / 256
+  if (openScale) {
+    return {
+      width: `${width.value * s}px`,
+      height: `${height.value * s}px`
+    }
+  }
+  if (!image) {
+    return {
+      height: '100%',
+      width: '100%'
+    }
+  }
+  return {}
+})
 
 watch(
   () => props.image,
@@ -34,14 +69,15 @@ watch(
       const basePath = projectStore.project.meta.path
       const imagePath = projectStore.project.resources.images.find((item) => item.id === val)?.path
       const result = await getImageByPath(basePath + imagePath)
-      if (result?.base64) {
-        src.value = result.base64
-      } else {
-        src.value = ''
-      }
+      src.value = result?.src!
+      width.value = result?.dimensions.width!
+      height.value = result?.dimensions.height!
     } else {
       src.value = ''
     }
+  },
+  {
+    immediate: true
   }
 )
 
@@ -57,13 +93,4 @@ const boxStyle = computed(() => {
     alignItems: 'center'
   }
 })
-
-const imgStyle = computed(() => {
-  const { rotate = { x: 50, y: 0, angle: 0 } } = props
-
-  return {
-    // transform: `rotate(${rotate.angle}deg)`,
-    transformOrigin: `${rotate.x}px ${rotate.y}px`
-  }
-})
 </script>

+ 76 - 5
src/renderer/src/lvgl-widgets/image/index.ts

@@ -3,6 +3,8 @@ import icon from '../assets/icon/icon_4image.svg'
 import { flagOptions } from '@/constants'
 import type { IComponentModelConfig } from '../type'
 import i18n from '@/locales'
+import { getImageByPath } from '@/utils'
+import { useProjectStore } from '@/store/modules/project'
 
 export default {
   label: i18n.global.t('image'),
@@ -32,9 +34,23 @@ export default {
         x: 50,
         y: 50,
         angle: 0
-      }
+      },
+      openScale: false,
+      scale: 256,
+      antiAliasing: false
     },
-    styles: []
+    styles: [
+      {
+        part: {
+          name: 'main',
+          state: 'default'
+        },
+        imageStyle: {
+          color: '#ffffff00',
+          alpha: 255
+        }
+      }
+    ]
   },
   config: {
     // 组件属性
@@ -112,7 +128,27 @@ export default {
       {
         label: '图片',
         field: 'props.image',
-        valueType: 'image'
+        valueType: 'image',
+        componentProps: {
+          onChange: (value: string, formData: any) => {
+            // 选中图片后获取图片信息
+            // 根据图片信息设置控件宽高
+            if (value) {
+              const projectStore = useProjectStore()
+              const imgPath = projectStore.project?.resources.images.find(
+                (item) => item.id === value
+              )?.path
+              if (imgPath) {
+                getImageByPath(projectStore.projectPath + imgPath).then((res) => {
+                  if (res?.dimensions.height) {
+                    formData.props.height = res.dimensions.height
+                    formData.props.width = res.dimensions.width
+                  }
+                })
+              }
+            }
+          }
+        }
       },
       {
         label: '旋转中心',
@@ -140,14 +176,49 @@ export default {
             valueType: 'number',
             componentProps: {
               span: 12,
-              min: 0,
+              min: -360,
               max: 360
             }
           }
         ]
+      },
+      {
+        label: '缩放',
+        field: 'props.openScale',
+        valueType: 'switch'
+      },
+      {
+        valueType: 'dependency',
+        name: ['props.openScale'],
+        dependency: (dependency) => {
+          return dependency?.['props.openScale']
+            ? [
+                {
+                  label: '缩放值',
+                  field: 'props.scale',
+                  valueType: 'number',
+                  componentProps: {
+                    min: 0,
+                    max: 10000
+                  }
+                }
+              ]
+            : []
+        }
+      },
+      {
+        label: '抗锯齿',
+        field: 'props.antiAliasing',
+        valueType: 'switch'
       }
     ],
     // 组件样式
-    styles: []
+    styles: [
+      {
+        label: '图片',
+        field: 'imageStyle',
+        valueType: 'imageStyle'
+      }
+    ]
   }
 } as IComponentModelConfig

+ 20 - 0
src/renderer/src/lvgl-widgets/image/style.json

@@ -0,0 +1,20 @@
+{
+  "widget": "lv_image",
+  "styleName": "defualt",
+  "part": [
+    {
+      "partName": "main",
+      "state": [
+        {
+          "state": "default",
+          "style": {
+            "imageStyle": {
+              "color": "#ffffff00",
+              "alpha": 255
+            }
+          }
+        }
+      ]
+    }
+  ]
+}

+ 8 - 1
src/renderer/src/store/modules/project.ts

@@ -10,7 +10,7 @@ import type { BaseWidget } from '@/types/baseWidget'
 import type { Screen } from '@/types/screen'
 import type { Page } from '@/types/page'
 
-import { computed, ref } from 'vue'
+import { computed, ref, watch } from 'vue'
 import { defineStore } from 'pinia'
 import { klona } from 'klona'
 import { createBin, createScreen } from '@/model'
@@ -88,6 +88,13 @@ export const useProjectStore = defineStore('project', () => {
     return activeWidgets.value?.at(-1) ?? activePage.value
   })
 
+  watch(
+    () => activePageId,
+    () => {
+      activeWidgets.value = []
+    }
+  )
+
   // 当前激活元素map
   const activeWidgetMap = computed(() => {
     return activeWidgets.value.reduce((acc, cur) => {

+ 1 - 1
src/renderer/src/utils/index.ts

@@ -29,7 +29,7 @@ export const getImageByPath = async (path: string) => {
   const base64 = `data:image/${path.split('.').pop()};base64,` + res
   const buffer = await getFileByPath(path)
   const dimensions = imageSize(buffer)
-  return { base64, dimensions }
+  return { base64, dimensions, src: `local:///${path.replaceAll('\\', '/')}` }
 }
 
 /**

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

@@ -230,6 +230,10 @@ const value = computed({
     if (schema.field) {
       set(props.formData, schema.field, val)
     }
+    // 触发change事件
+    if (schema?.componentProps?.onChange) {
+      schema.componentProps.onChange(val, props.formData)
+    }
   }
 })
 

+ 1 - 1
src/renderer/src/views/designer/sidebar/components/EditImageModal.vue

@@ -213,7 +213,7 @@ defineExpose({
     // 获取图片信息
     const res = await getImageByPath(projectStore.projectPath + resouce.path)
     imageInfo.value = {
-      url: res?.base64,
+      url: res?.src,
       width: res?.dimensions.width,
       height: res?.dimensions.height,
       type: res?.dimensions.type