Pārlūkot izejas kodu

Merge branch 'main' of https://git.shalu.com/jiaxing.liao/sunmicro-lvgl-designer

Mickey Mike 1 dienu atpakaļ
vecāks
revīzija
3363a0f188
29 mainītis faili ar 292 papildinājumiem un 87 dzēšanām
  1. 4 0
      src/renderer/components.d.ts
  2. 13 0
      src/renderer/src/components/IconButton/index.vue
  3. 6 2
      src/renderer/src/components/SplitterCollapse/SplitterCollapseItem.vue
  4. 3 6
      src/renderer/src/components/SplitterCollapse/index.vue
  5. 6 0
      src/renderer/src/components/index.ts
  6. 9 0
      src/renderer/src/constants/index.ts
  7. 2 1
      src/renderer/src/locales/en_US.json
  8. 2 1
      src/renderer/src/locales/zh_CN.json
  9. 46 4
      src/renderer/src/lvgl-widgets/button/Button.vue
  10. 2 7
      src/renderer/src/lvgl-widgets/button/index.ts
  11. 2 3
      src/renderer/src/lvgl-widgets/container/index.ts
  12. 7 1
      src/renderer/src/lvgl-widgets/index.ts
  13. 15 18
      src/renderer/src/lvgl-widgets/page/index.ts
  14. 8 8
      src/renderer/src/lvgl-widgets/type.d.ts
  15. 32 2
      src/renderer/src/model/index.ts
  16. 4 1
      src/renderer/src/store/modules/app.ts
  17. 4 1
      src/renderer/src/store/modules/recentProject.ts
  18. 11 0
      src/renderer/src/style.less
  19. 2 4
      src/renderer/src/types/baseWidget.d.ts
  20. 1 1
      src/renderer/src/types/page.d.ts
  21. 22 0
      src/renderer/src/utils/index.ts
  22. 1 3
      src/renderer/src/views/designer/index.vue
  23. 23 3
      src/renderer/src/views/designer/info/index.vue
  24. 2 2
      src/renderer/src/views/designer/modals/projectModal/Recent.vue
  25. 1 1
      src/renderer/src/views/designer/sidebar/Libary.vue
  26. 12 3
      src/renderer/src/views/designer/workspace/composite/index.vue
  27. 2 7
      src/renderer/src/views/designer/workspace/index.vue
  28. 13 4
      src/renderer/src/views/designer/workspace/stage/DesignerCanvas.vue
  29. 37 4
      src/renderer/src/views/designer/workspace/stage/Node.vue

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

@@ -14,6 +14,7 @@ declare module 'vue' {
   export interface GlobalComponents {
     CodeEditor: typeof import('./src/components/CodeEditor/index.vue')['default']
     ColorPicker: typeof import('./src/components/ColorPicker/index.vue')['default']
+    Components: typeof import('./src/components/index.vue')['default']
     EditorModal: typeof import('./src/components/EditorModal/index.vue')['default']
     ElAutocomplete: typeof import('element-plus/es')['ElAutocomplete']
     ElAutoComplete: typeof import('element-plus/es')['ElAutoComplete']
@@ -69,6 +70,7 @@ declare module 'vue' {
     ElTextarea: typeof import('element-plus/es')['ElTextarea']
     ElTooltip: typeof import('element-plus/es')['ElTooltip']
     ElTree: typeof import('element-plus/es')['ElTree']
+    IconButton: typeof import('./src/components/IconButton/index.vue')['default']
     LocalImage: typeof import('./src/components/LocalImage/index.vue')['default']
     MonacoEditor: typeof import('./src/components/MonacoEditor/index.vue')['default']
     PanelTitle: typeof import('./src/components/PanelTitle/index.vue')['default']
@@ -87,6 +89,7 @@ declare module 'vue' {
 declare global {
   const CodeEditor: typeof import('./src/components/CodeEditor/index.vue')['default']
   const ColorPicker: typeof import('./src/components/ColorPicker/index.vue')['default']
+  const Components: typeof import('./src/components/index.vue')['default']
   const EditorModal: typeof import('./src/components/EditorModal/index.vue')['default']
   const ElAutocomplete: typeof import('element-plus/es')['ElAutocomplete']
   const ElAutoComplete: typeof import('element-plus/es')['ElAutoComplete']
@@ -142,6 +145,7 @@ declare global {
   const ElTextarea: typeof import('element-plus/es')['ElTextarea']
   const ElTooltip: typeof import('element-plus/es')['ElTooltip']
   const ElTree: typeof import('element-plus/es')['ElTree']
+  const IconButton: typeof import('./src/components/IconButton/index.vue')['default']
   const LocalImage: typeof import('./src/components/LocalImage/index.vue')['default']
   const MonacoEditor: typeof import('./src/components/MonacoEditor/index.vue')['default']
   const PanelTitle: typeof import('./src/components/PanelTitle/index.vue')['default']

+ 13 - 0
src/renderer/src/components/IconButton/index.vue

@@ -0,0 +1,13 @@
+<template>
+  <div
+    class="inline-flex min-w-24px h-24px px-2px rounded-4px cursor-pointer items-center justify-center hover:bg-bg-tertiary"
+  >
+    <slot></slot>
+  </div>
+</template>
+
+<script setup lang="ts">
+defineOptions({
+  name: 'IconButton'
+})
+</script>

+ 6 - 2
src/renderer/src/components/SplitterCollapse/SplitterCollapseItem.vue

@@ -31,11 +31,14 @@
       </div>
     </template>
   </SplitterPanel>
-  <SplitterResizeHandle class="splitter-handle" />
+  <SplitterResizeHandle
+    class="splitter-handle"
+    :class="direction === 'vertical' ? 'h-2px' : 'w-2px'"
+  />
 </template>
 
 <script setup lang="ts">
-import { defineProps, defineEmits, withDefaults } from 'vue'
+import { inject } from 'vue'
 import { AiOutlineRight, AiOutlineDown } from 'vue-icons-plus/ai'
 import { SplitterPanel, SplitterResizeHandle } from 'reka-ui'
 
@@ -47,6 +50,7 @@ withDefaults(
 )
 
 const emit = defineEmits(['change'])
+const direction = inject('direction')
 </script>
 
 <style scoped></style>

+ 3 - 6
src/renderer/src/components/SplitterCollapse/index.vue

@@ -8,10 +8,10 @@
 </template>
 
 <script setup lang="ts">
-import { defineProps, withDefaults, provide, useSlots } from 'vue'
+import { provide } from 'vue'
 import { SplitterGroup } from 'reka-ui'
 
-withDefaults(
+const props = withDefaults(
   defineProps<{
     layout?: 'vertical' | 'horizontal'
     value?: string[]
@@ -21,14 +21,11 @@ withDefaults(
   }
 )
 
-const slots = useSlots()
 const emit = defineEmits<{
   (e: 'update:value', value: string[]): void
 }>()
 
-provide('info', {
-  list: slots.default?.()?.map((item) => item.props?.name),
-})
+provide('direction', props.layout)
 
 </script>
 

+ 6 - 0
src/renderer/src/components/index.ts

@@ -0,0 +1,6 @@
+export { default as ColorPicker } from './ColorPicker/index.vue'
+export { default as LocalImage } from './LocalImage/index.vue'
+export { default as MonacoEditor } from './MonacoEditor/index.vue'
+export * from './SplitterCollapse'
+export { default as ViewTitle } from './ViewTitle/index.vue'
+export { default as IconButton } from './IconButton/index.vue'

+ 9 - 0
src/renderer/src/constants/index.ts

@@ -146,3 +146,12 @@ export const languagesMenu = [
   { label: '西班牙语', value: 'es-ES' },
   { label: '俄语', value: 'ru-RU' }
 ]
+/**
+ * 滚动条模式
+ */
+export const scrollbarModes = [
+  { label: 'On', value: 'ON' },
+  { label: 'Off', value: 'OFF' },
+  { label: 'Auto', value: 'AUTO' },
+  { label: 'Active', value: 'ACTIVE' }
+]

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

@@ -83,5 +83,6 @@
   "button": "Button",
   "basic": "Basic",
   "container": "Container",
-  "page": "page"
+  "page": "page",
+  "createTime": "Create Time"
 }

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

@@ -83,5 +83,6 @@
   "button": "按钮",
   "basic": "基础",
   "container": "容器",
-  "page": "页面"
+  "page": "页面",
+  "createTime": "创建时间"
 }

+ 46 - 4
src/renderer/src/lvgl-widgets/button/Button.vue

@@ -1,15 +1,57 @@
 <template>
-  <div></div>
+  <div v-bind="getProps">{{ props.text }}</div>
 </template>
 
 <script setup lang="ts">
-defineProps<{
+import { computed } from 'vue'
+const props = defineProps<{
   width: number
   height: number
-  text: string
+  text?: string
   styles: any
-  state: string
+  state?: string
 }>()
+
+const getProps = computed(() => {
+  const styles = props.styles
+  const stateStyles = styles.find((item) => item.state === props.state)
+
+  return {
+    class: 'button',
+    style: {
+      width: `${props.width}px`,
+      height: `${props.height}px`,
+
+      backgroundColor: stateStyles?.background.color,
+
+      fontSize: `${stateStyles?.text.size}px`,
+      color: stateStyles?.text?.color,
+      display: 'flex',
+      justifyContent: stateStyles?.text?.align || 'center',
+      alignItems: 'center',
+
+      borderRadius: `${stateStyles?.border.radius}px`,
+      borderColor: 'transparent',
+      borderWidth: `${stateStyles?.border.width}px`,
+      borderTopColor: ['all', 'top'].includes(stateStyles?.border?.side)
+        ? stateStyles?.border?.color
+        : 'transparent',
+      borderRightColor: ['all', 'right'].includes(stateStyles?.border?.side)
+        ? stateStyles?.border?.color
+        : 'transparent',
+      borderBottomColor: ['all', 'bottom'].includes(stateStyles?.border?.side)
+        ? stateStyles?.border?.color
+        : 'transparent',
+      borderLeftColor: ['all', 'left'].includes(stateStyles?.border?.side)
+        ? stateStyles?.border?.color
+        : 'transparent',
+      /* x 偏移量 | y 偏移量 | 阴影模糊半径 | 阴影扩散半径 | 阴影颜色 */
+      boxShodow: stateStyles?.boxShadow
+        ? `${stateStyles?.boxShadow?.offsetX}px ${stateStyles?.boxShadow?.offsetY}px ${stateStyles?.boxShadow?.width}px ${stateStyles?.boxShadow?.spread}px ${stateStyles?.boxShadow?.color}`
+        : 'none'
+    }
+  }
+})
 </script>
 
 <style scoped></style>

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

@@ -7,9 +7,8 @@ import i18n from '@/locales'
 export default {
   label: i18n.global.t('button'),
   icon,
-  componentName: 'LvButton',
   component: LvButton,
-  type: 'lv_button',
+  key: 'lv_button',
   group: i18n.global.t('basic'),
   sort: 1,
   parts: [
@@ -21,7 +20,6 @@ export default {
   defaultSchema: {
     props: {
       name: 'button',
-      type: 'lv_button',
       x: 0,
       y: 0,
       width: 90,
@@ -40,16 +38,13 @@ export default {
           alpha: 255,
           image: {
             imgId: '',
-            alpha: 255,
-            colorFormat: '',
-            colorDepth: '',
             color: ''
           }
         },
         text: {
           color: '#ffffff',
           family: '',
-          size: 14,
+          size: 16,
           weight: 400,
           align: 'center'
         },

+ 2 - 3
src/renderer/src/lvgl-widgets/container/index.ts

@@ -7,11 +7,11 @@ import i18n from '@/locales'
 export default {
   label: i18n.global.t('container'),
   icon,
-  componentName: 'lv_container',
   component: Container,
-  type: 'lv_obj',
+  key: 'lv_obj',
   group: i18n.global.t('container'),
   sort: 1,
+  hasChildren: true,
   parts: [
     {
       name: 'main',
@@ -21,7 +21,6 @@ export default {
   defaultSchema: {
     props: {
       name: 'container',
-      type: 'lv_obj',
       x: 0,
       y: 0,
       width: 300,

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

@@ -1,6 +1,12 @@
 import Button from './button'
 import Container from './container'
+import { IComponentModelConfig } from './type'
 
 export const ComponentArray = [Button, Container]
 
-export { Button, Container }
+const componentMap: { [key: string]: IComponentModelConfig } = ComponentArray.reduce((acc, cur) => {
+  acc[cur.key] = cur
+  return acc
+}, {})
+
+export default componentMap

+ 15 - 18
src/renderer/src/lvgl-widgets/page/index.ts

@@ -1,12 +1,12 @@
 import Page from './Page.vue'
 import type { IComponentModelConfig } from '../type'
 import i18n from '@/locales'
+import { scrollbarModes } from '@/constants'
 
 export default {
   label: i18n.global.t('page'),
-  componentName: 'screen',
   component: Page,
-  type: 'page',
+  key: 'page',
   group: i18n.global.t('container'),
   parts: [
     {
@@ -17,9 +17,19 @@ export default {
   defaultSchema: {
     props: {
       name: 'screen',
-      type: '',
       scrollbarMode: 'OFF'
-    }
+    },
+    styles: [
+      {
+        part: {
+          name: 'main',
+          state: 'default'
+        },
+        background: {
+          color: '#ffffffff'
+        }
+      }
+    ]
   },
   config: {
     // 组件属性
@@ -35,14 +45,6 @@ export default {
             componentProps: {
               placeholder: '请输入名称'
             }
-          },
-          {
-            label: '类型',
-            field: 'type',
-            valueType: 'text',
-            componentProps: {
-              readOnly: true
-            }
           }
         ]
       },
@@ -51,12 +53,7 @@ export default {
         field: 'scollbarMode',
         valueType: 'select',
         componentProps: {
-          options: [
-            { label: 'On', value: 'on' },
-            { label: 'Off', value: 'off' },
-            { label: 'Auto', value: 'auto' },
-            { label: 'Active', value: 'active' }
-          ]
+          options: scrollbarModes
         }
       }
     ],

+ 8 - 8
src/renderer/src/lvgl-widgets/type.d.ts

@@ -44,18 +44,14 @@ export interface IComponentModelConfig {
    * 组件图标
    */
   icon?: string
-  /**
-   * 组件名称
-   */
-  componentName: string
   /**
    * 组件
    */
   component: any
   /**
-   * 组件类型
+   * 组件key
    */
-  type: string
+  key: string
   /**
    * 所属分组
    */
@@ -68,6 +64,10 @@ export interface IComponentModelConfig {
    * 是否在组件库隐藏
    */
   hideLibary?: boolean
+  /**
+   * 是否存在子组件
+   */
+  hasChildren?: boolean
   /**
    * 组件模块
    */
@@ -79,11 +79,11 @@ export interface IComponentModelConfig {
     /**
      * 默认属性
      */
-    props?: {}
+    props?: Record<string, any>
     /**
      * 默认样式
      */
-    styles?: any[]
+    styles?: Record<string, any>[]
   }
   /**
    * 组件配置

+ 32 - 2
src/renderer/src/model/index.ts

@@ -3,8 +3,13 @@ import type { Screen } from '@/types/screen'
 import type { ScreenConfig } from '@/types/appMeta'
 import type { Bin } from '@/types/bins'
 import type { Resource } from '@/types/resource'
+import type { IComponentModelConfig } from '@/lvgl-widgets/type'
 
 import { v4 } from 'uuid'
+import { klona } from 'klona'
+import { BaseWidget } from '@/types/baseWidget'
+import { getUUID } from '@/utils'
+import LvglWidgets from '@/lvgl-widgets'
 
 /**
  * 创建屏幕
@@ -54,7 +59,7 @@ export const createPage = (): Page => {
     // 属性
     props: {},
     // 样式
-    style: {},
+    style: klona(LvglWidgets['page'].defaultSchema.styles || []),
     // 事件
     events: [],
     // 页面变量
@@ -135,7 +140,32 @@ export const createFileResource = (path: string, type: 'image' | 'font' | 'other
 export const createMethod = () => {
   return {
     id: v4(),
-    name: 'func_name',
+    name: 'func_name' + '_' + getUUID(6, 'number'),
     action: ''
   }
 }
+
+/**
+ * 创建控件数据
+ * @param schema 组件模型
+ */
+export const createWidget = (schema: IComponentModelConfig) => {
+  const { defaultSchema, hasChildren } = schema
+  const componentSchema: BaseWidget = {
+    id: v4(),
+    name: defaultSchema.props?.name + '_' + getUUID(6, 'number'),
+    type: schema.key,
+    isCopy: false,
+    copyFrom: '',
+    hidden: false,
+    locked: false,
+    props: klona(defaultSchema.props || {}),
+    style: klona(defaultSchema.styles || []),
+    events: []
+  }
+  if (hasChildren) {
+    componentSchema.children = []
+  }
+
+  return componentSchema
+}

+ 4 - 1
src/renderer/src/store/modules/app.ts

@@ -31,6 +31,8 @@ export const useAppStore = defineStore('app', () => {
   const sunmicroPath = useLocalStorage('sunmicroPath', '')
   // 底部工具
   const showComposite = ref(true)
+  // 底部工具当前tab
+  const compositeTabAcitve = ref('log')
 
   const { locale } = useI18n()
 
@@ -68,6 +70,7 @@ export const useAppStore = defineStore('app', () => {
     showGeneralModal,
     setTheme,
     sunmicroPath,
-    showComposite
+    showComposite,
+    compositeTabAcitve
   }
 })

+ 4 - 1
src/renderer/src/store/modules/recentProject.ts

@@ -1,6 +1,7 @@
 import { defineStore } from 'pinia'
 import { ref } from 'vue'
 import { openDB } from 'idb'
+import dayjs from 'dayjs'
 
 type ProjectRecord = {
   id: string
@@ -28,7 +29,9 @@ export const useRecentProject = defineStore('recentProject', () => {
   const getAllProjects = async () => {
     const db = await getDB()
     const projects = await db.getAll('projects')
-    recentProjects.value = projects
+    recentProjects.value = projects.sort((a, b) =>
+      dayjs(a.modifyTime).isAfter(dayjs(b.modifyTime)) ? -1 : 1
+    )
   }
 
   // 添加项目

+ 11 - 0
src/renderer/src/style.less

@@ -40,6 +40,9 @@ body {
   .el-tabs__nav-wrap:after {
     height: 1px;
   }
+  .el-tabs__item {
+    height: 34px;
+  }
 }
 
 .el-collapse {
@@ -53,4 +56,12 @@ body {
   --el-collapse-content-text-color: var(--el-text-color-primary);
   border-bottom: 1px solid var(--el-collapse-border-color);
   border-top: 1px solid var(--el-collapse-border-color);
+}
+
+.el-tabs--border-card {
+  border: none !important;
+}
+
+[data-state="hover"], [data-state="drag"] {
+  background: var(--accent-blue);
 }

+ 2 - 4
src/renderer/src/types/baseWidget.d.ts

@@ -6,9 +6,7 @@ export type BaseWidget = {
   // 控件名称
   name: string
   // 元素类型
-  type: 'widget'
-  // 控件类型
-  widgetType: string
+  type: string
   // 是否为可复用控件
   isCopy: boolean
   // 复用来源控件ID
@@ -20,6 +18,6 @@ export type BaseWidget = {
   // 事件
   events: WidgetEvent[]
   // 子控件
-  children: BaseWidget[]
+  children?: BaseWidget[]
   [key: string]: any
 }

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

@@ -29,7 +29,7 @@ export type Page = {
   // 属性
   props: Record<string, any>
   // 样式
-  style: Record<string, any>
+  style: Record<string, any>[]
   // 事件
   events: {
     // 事件ID

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

@@ -26,3 +26,25 @@ export const getImageByPath = async (path: string) => {
   const dimensions = imageSize(buffer)
   return { base64, dimensions }
 }
+
+/**
+ * 生成一个用不重复的ID
+ * @param randomLength 随机id长度 0 - 11
+ */
+export function getUUID(randomLength = 6, type: 'number' | 'string' = 'string'): string {
+  // 生成字符类型UUID
+  if (type === 'string') {
+    const buf: string[] = []
+    const max = 36 // 0-9, a-z 共36个字符
+    const offset = 87 // 'a'的charCodeAt值为97,97 - 10 = 87
+    for (let i = 0; i < randomLength; i++) {
+      const k = Math.floor(Math.random() * max)
+      buf.push(k < 10 ? k.toString() : String.fromCodePoint(offset + k))
+    }
+    return buf.join('')
+  }
+
+  // 生成数值类型UUID
+  const times = 10 ** (randomLength - 1)
+  return (times + Math.floor(Math.random() * 9 * times)).toString()
+}

+ 1 - 3
src/renderer/src/views/designer/index.vue

@@ -19,9 +19,7 @@
           </SplitterPanel>
         </SplitterGroup>
       </div>
-      <div class="h-32px">
-        <Info></Info>
-      </div>
+      <Info></Info>
     </div>
   </div>
   <ProjectModal ref="projectModel" />

+ 23 - 3
src/renderer/src/views/designer/info/index.vue

@@ -1,9 +1,29 @@
 <template>
   <div
-    class="w-full h-full bg-bg-sidebar box-border border-t-2px border-t-solid border-t-border"
-  ></div>
+    class="w-full h-40px flex items-center justify-end px-2 bg-bg-sidebar box-border border-t-2px border-t-solid border-t-border"
+  >
+    <IconButton @click="toggleShowEvent">
+      <span class="text-10px">事件</span>
+    </IconButton>
+    <IconButton @click="toggleShowLog">
+      <span class="text-10px">日志</span>
+    </IconButton>
+  </div>
 </template>
 
-<script setup lang="ts"></script>
+<script setup lang="ts">
+import { IconButton } from '@/components'
+import { useAppStore } from '@/store/modules/app'
+
+const appStore = useAppStore()
+const toggleShowEvent = () => {
+  appStore.showComposite = true
+  appStore.compositeTabAcitve = 'event'
+}
+const toggleShowLog = () => {
+  appStore.showComposite = true
+  appStore.compositeTabAcitve = 'log'
+}
+</script>
 
 <style scoped></style>

+ 2 - 2
src/renderer/src/views/designer/modals/projectModal/Recent.vue

@@ -15,8 +15,8 @@
           <div class="text-12px text-text-primary mb-4px">{{ item.projectName }}</div>
           <div class="text-10px">
             <p class="m0 p0 flex gap-8px">
-              <span>{{ $t('projectPath') }}</span>
-              <span>{{ item.createTime }}</span>
+              <span class="shrkin-0">{{ $t('createTime') }}</span>
+              <span class="truncate">{{ item.createTime }}</span>
             </p>
             <p class="m0 p0 flex gap-8px">
               <span>{{ $t('modifyTime') }}</span>

+ 1 - 1
src/renderer/src/views/designer/sidebar/Libary.vue

@@ -9,7 +9,7 @@
     <el-collapse>
       <el-collapse-item v-for="group in getGroups" :key="group.label" :title="group.label">
         <div class="px-2 pb-2 pt-1 grid grid-cols-[auto_auto] gap-2">
-          <LibaryItem v-for="item in group.items" :key="item.componentName" :comp="item" />
+          <LibaryItem v-for="item in group.items" :key="item.key" :comp="item" />
         </div>
       </el-collapse-item>
     </el-collapse>

+ 12 - 3
src/renderer/src/views/designer/workspace/composite/index.vue

@@ -1,5 +1,5 @@
 <template>
-  <el-tabs v-model="active" class="w-full h-full">
+  <el-tabs v-model="appStore.compositeTabAcitve" class="w-full h-full relative">
     <el-tab-pane label="日志" name="log">
       <Log />
     </el-tab-pane>
@@ -7,14 +7,23 @@
       <EventEdit />
     </el-tab-pane>
   </el-tabs>
+  <div class="absolute h-32px top-0 right-0 flex items-center px-1">
+    <el-tooltip content="隐藏面板" placement="top">
+      <IconButton @click="appStore.showComposite = !appStore.showComposite">
+        <LuX size="12px" />
+      </IconButton>
+    </el-tooltip>
+  </div>
 </template>
 
 <script setup lang="ts">
-import { ref } from 'vue'
 import Log from './Log.vue'
 import EventEdit from './eventEdit/index.vue'
+import { IconButton } from '@/components'
+import { LuX } from 'vue-icons-plus/lu'
+import { useAppStore } from '@/store/modules/app'
 
-const active = ref('log')
+const appStore = useAppStore()
 </script>
 
 <style scoped>

+ 2 - 7
src/renderer/src/views/designer/workspace/index.vue

@@ -47,7 +47,7 @@
       </SplitterPanel>
       <SplitterResizeHandle v-show="appStore.showComposite" class="bg-border h-2px" />
       <SplitterPanel v-show="appStore.showComposite" :default-size="30">
-        <div class="w-full h-full">
+        <div class="w-full h-full relative">
           <Composite />
         </div>
       </SplitterPanel>
@@ -75,14 +75,9 @@ const activeTab = ref<TabPaneName>('design')
 // tab pane列表
 const tabPaneList = ref([
   {
-    label: 'aa.h',
+    label: '项目预览',
     name: '111',
     component: MonacoEditor
-  },
-  {
-    label: 'bb.h',
-    name: '222',
-    component: MonacoEditor
   }
 ])
 onMounted(() => {

+ 13 - 4
src/renderer/src/views/designer/workspace/stage/DesignerCanvas.vue

@@ -5,15 +5,24 @@
       <!-- 格子背景 -->
       <div class="absolute transparent-bg" :style="getStyles.transpartBg"></div>
       <!-- 画布区域 -->
-      <div ref="canvasRef" class="absolute" :style="getStyles.canvasStyle">
+      <div
+        ref="canvasRef"
+        class="absolute"
+        :style="{
+          ...getStyles.canvasStyle,
+          background: 'transparent'
+        }"
+      >
         <!-- 网格背景 -->
         <div class="canvas-grid" v-show="state.showBgGrid"></div>
+        <!-- 内容节点 -->
         <Nodes
-          :schema="page"
+          :schema="page!"
           :style="{
             ...getStyles.canvasStyle,
             transform: '',
-            border: 'none'
+            border: 'none',
+            position: 'static'
           }"
         />
       </div>
@@ -109,7 +118,7 @@ const getStyles = computed(() => {
       transform: `scale(${scale})`,
       left: `${canvasLeft}px`,
       top: `${canvasTop}px`,
-      background: '#fff',
+      background: page?.style?.[0]?.background?.color,
       border
     },
     // 提示样式

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

@@ -1,25 +1,58 @@
 <template>
-  <div ref="nodeRef"></div>
+  <div :style="getStyle">
+    <component :is="widget" v-bind="schema.props" :styles="schema.style" />
+    <div v-if="schema.children" class="w-full h-full" ref="nodeRef">
+      <template v-for="child in schema.children" :key="child.id">
+        <NodeItem :schema="child" />
+      </template>
+    </div>
+  </div>
 </template>
 
 <script setup lang="ts">
-import { ref } from 'vue'
+import type { BaseWidget } from '@/types/baseWidget'
+import type { Page } from '@/types/page'
+
+import { computed, type CSSProperties, ref } from 'vue'
 import { useDrop } from 'vue-hooks-plus'
+import { createWidget } from '@/model'
+import LvglWidgets from '@/lvgl-widgets'
 
 defineOptions({
   name: 'NodeItem'
 })
 
 const props = defineProps<{
-  schema: any
+  schema: BaseWidget | Page
+  style?: CSSProperties
 }>()
 
+const widget = computed(() => LvglWidgets[props.schema.type]?.component)
 const nodeRef = ref<HTMLDivElement>()
 const hovering = ref(false)
 
+// 组件样式
+const getStyle = computed((): CSSProperties => {
+  const { style = {}, schema } = props
+  return {
+    position: 'absolute',
+    left: schema.props.x + 'px',
+    top: schema.props.y + 'px',
+    width: schema.props.w + 'px',
+    height: schema.props.h + 'px',
+    ...style
+  }
+})
+
+// 拖拽放置事件处理
 useDrop(nodeRef, {
   onDom: (content, event) => {
-    console.log(content, event)
+    // 创建控件
+    const { offsetX = 0, offsetY = 0 } = event || {}
+    const newWidget = createWidget(content)
+    newWidget.props.x = offsetX
+    newWidget.props.y = offsetY
+    props.schema.children?.push(newWidget)
   },
   onDragEnter: () => (hovering.value = true),
   onDragLeave: () => (hovering.value = false)