Prechádzať zdrojové kódy

perf: 优化控件问题

liaojiaxing 1 týždeň pred
rodič
commit
9040232b5a
23 zmenil súbory, kde vykonal 614 pridanie a 277 odobranie
  1. 0 1
      package.json
  2. 1 11
      pnpm-lock.yaml
  3. 15 16
      src/renderer/src/lvgl-widgets/bar/index.ts
  4. 1 1
      src/renderer/src/lvgl-widgets/hooks/useWidgetStyle.ts
  5. 1 1
      src/renderer/src/lvgl-widgets/image-button/index.ts
  6. 0 3
      src/renderer/src/lvgl-widgets/slider/Slider.vue
  7. 15 16
      src/renderer/src/lvgl-widgets/slider/index.ts
  8. 1 1
      src/renderer/src/lvgl-widgets/span-group/Config.vue
  9. 17 3
      src/renderer/src/lvgl-widgets/tileview/Config.vue
  10. 16 2
      src/renderer/src/lvgl-widgets/variableConfig.ts
  11. 58 41
      src/renderer/src/store/modules/action.ts
  12. 120 4
      src/renderer/src/store/modules/project.ts
  13. 1 1
      src/renderer/src/types/appMeta.d.ts
  14. 184 19
      src/renderer/src/utils/widgetName.ts
  15. 158 132
      src/renderer/src/views/designer/config/VariableConfig.vue
  16. 1 1
      src/renderer/src/views/designer/config/property/components/StyleBackground.vue
  17. 1 1
      src/renderer/src/views/designer/config/property/components/StyleBorder.vue
  18. 2 1
      src/renderer/src/views/designer/config/property/components/VariableBindWrapper.vue
  19. 16 9
      src/renderer/src/views/designer/modals/projectModal/index.vue
  20. 1 2
      src/renderer/src/views/designer/sidebar/components/ComponentLibary.vue
  21. 2 2
      src/renderer/src/views/designer/sidebar/components/ScreenTreeItem.vue
  22. 1 4
      src/renderer/src/views/designer/sidebar/components/WidgetLibary.vue
  23. 2 5
      src/renderer/src/views/designer/workspace/stage/Node.vue

+ 0 - 1
package.json

@@ -39,7 +39,6 @@
     "klona": "^2.0.6",
     "lodash-es": "^4.17.22",
     "monaco-editor": "^0.54.0",
-    "nanoid": "^5.1.11",
     "normalize.css": "^8.0.1",
     "pinia": "^3.0.3",
     "qrcode": "^1.5.4",

+ 1 - 11
pnpm-lock.yaml

@@ -62,9 +62,6 @@ importers:
       monaco-editor:
         specifier: ^0.54.0
         version: 0.54.0
-      nanoid:
-        specifier: ^5.1.11
-        version: 5.1.11
       normalize.css:
         specifier: ^8.0.1
         version: 8.0.1
@@ -3131,11 +3128,6 @@ packages:
     engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
     hasBin: true
 
-  nanoid@5.1.11:
-    resolution: {integrity: sha512-v+KEsUv2ps74PaSKv0gHTxTCgMXOIfBEbaqa6w6ISIGC7ZsvHN4N9oJ8d4cmf0n5oTzQz2SLmThbQWhjd/8eKg==}
-    engines: {node: ^18 || >=20}
-    hasBin: true
-
   natural-compare@1.4.0:
     resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
 
@@ -4814,7 +4806,7 @@ snapshots:
   '@isaacs/cliui@8.0.2':
     dependencies:
       string-width: 5.1.2
-      string-width-cjs: string-width@4.2.0
+      string-width-cjs: string-width@4.2.3
       strip-ansi: 7.2.0
       strip-ansi-cjs: strip-ansi@6.0.1
       wrap-ansi: 8.1.0
@@ -7452,8 +7444,6 @@ snapshots:
 
   nanoid@3.3.12: {}
 
-  nanoid@5.1.11: {}
-
   natural-compare@1.4.0: {}
 
   needle@3.5.0:

+ 15 - 16
src/renderer/src/lvgl-widgets/bar/index.ts

@@ -226,7 +226,8 @@ export default {
         valueType: 'dependency',
         name: ['props.mode'],
         dependency: (dependency) => {
-          return dependency['props.mode'] === 'range'
+          return dependency['props.mode'] === 'range' ||
+            dependency['props.mode']?.originValue === 'range'
             ? [
                 {
                   label: '',
@@ -238,21 +239,19 @@ export default {
                       dependency: (dependency) => {
                         const min = dependency['props.min']
                         const max = dependency['props.max']
-                        return dependency['props.mode'] === 'range'
-                          ? [
-                              {
-                                label: '开始值',
-                                field: 'props.startValue',
-                                valueType: 'number',
-                                componentProps: {
-                                  min: Math.min(min, max),
-                                  max: Math.max(min, max),
-                                  span: 18
-                                },
-                                canUseEventSet: true
-                              }
-                            ]
-                          : []
+                        return [
+                          {
+                            label: '开始值',
+                            field: 'props.startValue',
+                            valueType: 'number',
+                            componentProps: {
+                              min: Math.min(min, max),
+                              max: Math.max(min, max),
+                              span: 18
+                            },
+                            canUseEventSet: true
+                          }
+                        ]
                       }
                     },
                     {

+ 1 - 1
src/renderer/src/lvgl-widgets/hooks/useWidgetStyle.ts

@@ -323,7 +323,7 @@ export const useWidgetStyle = (param: StyleParam) => {
         if (key === 'imageStyle') {
           styleMap.value[`${partItem.name}Style`].image = {
             backgroundColor: resolvedValue?.recolor,
-            opacity: (resolvedValue?.alpha || 255) / 255
+            opacity: (resolvedValue?.alpha ?? 255) / 255
           }
         }
         // 线段返回原本值,并解析 image 为 imageSrc/imageAlpha

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

@@ -26,7 +26,7 @@ export default {
       y: 0,
       width: 90,
       height: 45,
-      text: 'imgbtn',
+      text: '',
       flags: [
         'LV_OBJ_FLAG_CLICKABLE',
         'LV_OBJ_FLAG_CLICK_FOCUSABLE',

+ 0 - 3
src/renderer/src/lvgl-widgets/slider/Slider.vue

@@ -246,9 +246,6 @@ const otherStyle = computed(() => {
   const startVerticalAnchor = endVerticalAnchor === 'top' ? 'bottom' : 'top'
   const getHorizontalKnobTransform = (anchor: 'left' | 'right') =>
     anchor === 'right' ? 'translateX(50%)' : 'translateX(-50%)'
-  const getVerticalKnobTransform = (anchor: 'top' | 'bottom') =>
-    anchor === 'bottom' ? 'translateY(50%)' : 'translateY(-50%)'
-
   return {
     barStyle: {
       width: width >= height ? 'auto' : '100%',

+ 15 - 16
src/renderer/src/lvgl-widgets/slider/index.ts

@@ -230,7 +230,8 @@ export default {
         valueType: 'dependency',
         name: ['props.mode'],
         dependency: (dependency) => {
-          return dependency['props.mode'] === 'range'
+          return dependency['props.mode'] === 'range' ||
+            dependency['props.mode']?.originValue === 'range'
             ? [
                 {
                   label: '',
@@ -242,21 +243,19 @@ export default {
                       dependency: (dependency) => {
                         const min = dependency['props.min']
                         const max = dependency['props.max']
-                        return dependency['props.mode'] === 'range'
-                          ? [
-                              {
-                                label: '开始值',
-                                field: 'props.startValue',
-                                valueType: 'number',
-                                componentProps: {
-                                  min: Math.min(min, max),
-                                  max: Math.max(min, max),
-                                  span: 18
-                                },
-                                canUseEventSet: true
-                              }
-                            ]
-                          : []
+                        return [
+                          {
+                            label: '开始值',
+                            field: 'props.startValue',
+                            valueType: 'number',
+                            componentProps: {
+                              min: Math.min(min, max),
+                              max: Math.max(min, max),
+                              span: 18
+                            },
+                            canUseEventSet: true
+                          }
+                        ]
                       }
                     },
                     {

+ 1 - 1
src/renderer/src/lvgl-widgets/span-group/Config.vue

@@ -64,7 +64,7 @@
         </el-form-item>
         <el-form-item label="">
           <VariableBindWrapper
-            v-model="formData.text"
+            v-model="formData.text_decor"
             :variable-config="{
               type: 'enum',
               enumMap: {

+ 17 - 3
src/renderer/src/lvgl-widgets/tileview/Config.vue

@@ -50,7 +50,7 @@
 </template>
 
 <script setup lang="ts">
-import { computed, type Ref } from 'vue'
+import { computed, type Ref, watch } from 'vue'
 import { LuPlus, LuTrash2, LuInfo } from 'vue-icons-plus/lu'
 
 const props = defineProps<{
@@ -101,7 +101,6 @@ const setRow = (index: number, val?: number) => {
   if (existIndex === -1) {
     // 不存在相同索引
     record.row = val
-    record.name = `tile_${val}_${record.col}`
   }
 }
 
@@ -119,10 +118,24 @@ const setColumn = (index: number, val?: number) => {
   if (existIndex === -1) {
     // 不存在相同索引
     record.col = val
-    record.name = `tile_${record.row}_${val}`
   }
 }
 
+watch(
+  () => children.value,
+  (list) => {
+    list.forEach((item) => {
+      const tn = `tile_${item.row}_${item.col}`
+      if (tn !== item.name) {
+        item.name = tn
+      }
+    })
+  },
+  {
+    deep: true
+  }
+)
+
 /**
  * 删除一项
  * @param index 索引
@@ -148,6 +161,7 @@ const handleAddRow = () => {
     type: 'tile',
     row: maxRow,
     col: maxCol + 1,
+    direction: 'ALL',
     children: []
   })
 }

+ 16 - 2
src/renderer/src/lvgl-widgets/variableConfig.ts

@@ -10,7 +10,6 @@ type VariableConfig = {
 type WidgetVariableConfigMap = Record<string, Record<string, VariableConfig>>
 
 const longModeEnumMap = { 0: 'wrap', 1: 'dot', 2: 'scroll', 3: 'circular', 4: 'clip' }
-const dropdownDirectionEnumMap = { 1: 'left', 2: 'top', 4: 'bottom', 8: 'right' }
 const barModeEnumMap = { 0: 'normal', 1: 'symmetrical', 2: 'range' }
 const tabviewPositionEnumMap = { 1: 'left', 2: 'top', 4: 'bottom', 8: 'right' }
 const arcModeEnumMap = { 0: 'normal', 1: 'symmetrical', 2: 'reverse' }
@@ -93,6 +92,13 @@ const widgetVariableConfigMap: WidgetVariableConfigMap = {
     'props.text': { type: 'char' },
     'props.longMode': { type: 'enum', enumMap: longModeEnumMap }
   },
+  lv_image: {
+    ...commonPositionSizeFields,
+    'props.pivot.x': { type: 'uint32_t' },
+    'props.pivot.y': { type: 'uint32_t' },
+    'props.rotation': { type: 'int32_t' },
+    'props.scale': { type: 'uint32_t' }
+  },
   lv_imagebutton: {
     ...commonPositionSizeFields,
     'props.text': { type: 'char' }
@@ -120,7 +126,15 @@ const widgetVariableConfigMap: WidgetVariableConfigMap = {
   },
   lv_dropdown: {
     ...commonPositionSizeFields,
-    'props.direction': { type: 'enum', enumMap: dropdownDirectionEnumMap }
+    'props.direction': {
+      type: 'enum',
+      enumMap: {
+        0: 'LV_DIR_BOTTOM',
+        1: 'LV_DIR_TOP',
+        2: 'LV_DIR_LEFT',
+        3: 'LV_DIR_RIGHT'
+      }
+    }
   },
   lv_checkbox: {
     'props.x': { type: 'int32_t' },

+ 58 - 41
src/renderer/src/store/modules/action.ts

@@ -4,8 +4,6 @@ import { defineStore } from 'pinia'
 import { bfsWalk } from 'simple-mind-map/src/utils'
 import { moveToPosition } from '@/utils'
 import { klona } from 'klona'
-import { v4 } from 'uuid'
-import { createWidgetNameGenerator } from '@/utils/widgetName'
 
 import { useKeyPress } from 'vue-hooks-plus'
 import { useProjectStore } from '@/store/modules/project'
@@ -312,12 +310,25 @@ export const useActionStore = defineStore('action', () => {
    * @param widgetId 控件ID
    */
   const onDeleteById = (widgetId: string) => {
+    let deleted = false
+
     projectStore.project?.screens.forEach((screen) => {
+      if (deleted) return
+
       screen.pages.forEach((page) => {
-        bfsWalk(page, (child) => {
-          const index = child?.children?.findIndex((item) => item.id === widgetId) ?? -1
-          if (index !== -1 && !isWidgetTreeLocked(child.children[index])) {
-            child.children.splice(index, 1)
+        if (deleted) return // 提前退出页面循环
+
+        bfsWalk(page, (parent) => {
+          if (deleted) return // 提前退出遍历
+
+          if (!parent.children) return
+
+          const index = parent.children.findIndex((item) => item.id === widgetId)
+          if (index !== -1) {
+            if (!isWidgetTreeLocked(parent.children[index])) {
+              parent.children.splice(index, 1)
+              deleted = true
+            }
           }
         })
       })
@@ -328,23 +339,39 @@ export const useActionStore = defineStore('action', () => {
    * 删除控件
    */
   const onDelete = () => {
-    const widgetIds = getEditableWidgets().map((widget) => widget.id)
-    if (!widgetIds.length) return
+    const widgets = getEditableWidgets()
+    if (!widgets.length) return
+
+    const idsToDelete = new Set(widgets.map((w) => w.id))
+
+    // 标记是否发生了删除,用于优化后续操作
+    let hasDeleted = false
 
     projectStore.project?.screens.forEach((screen) => {
       screen.pages.forEach((page) => {
-        bfsWalk(page, (child) => {
-          const indexs =
-            child?.children?.map((item, index) => {
-              if (widgetIds.includes(item.id)) return index
-            }) ?? []
-          if (indexs.length) {
-            child.children = child.children.filter((_item, index) => !indexs.includes(index))
+        bfsWalk(page, (parent) => {
+          if (!parent.children?.length) return
+
+          // 快速检查:如果当前父节点的子节点中没有待删除的ID,跳过
+          // some 在找到第一个 true 时会停止,比 filter 快
+          if (!parent.children.some((c) => idsToDelete.has(c.id))) return
+
+          // 执行删除
+          const newChildren = parent.children.filter((child) => !idsToDelete.has(child.id))
+
+          // 只有当长度变化时才赋值,减少不必要的响应式触发
+          if (newChildren.length !== parent.children.length) {
+            parent.children = newChildren
+            hasDeleted = true
           }
         })
       })
     })
-    projectStore.activeWidgets = projectStore.activeWidgets.filter((widget) => widget.locked)
+
+    if (hasDeleted) {
+      // 清除选中状态,或者重新计算选中状态
+      projectStore.setSelectWidgets([])
+    }
   }
 
   /**
@@ -365,19 +392,22 @@ export const useActionStore = defineStore('action', () => {
     bfsWalk(projectStore.activePage, (child) => {
       const obj = child?.children?.find((item) => widgetIds.includes(item.id))
       if (obj) {
-        const newWidget = klona(obj)
-        newWidget.id = v4()
-        if (!newWidget.isCopy) {
-          newWidget.isCopy = true
-          projectStore.project?.widgets?.push(newWidget)
+        let copyFromId = obj.copyFrom
+        if (!obj.isCopy || !copyFromId) {
+          const templateWidget = projectStore.cloneWidgetTreeWithNewIdentity(obj)
+          templateWidget.isCopy = true
+          templateWidget.copyFrom = ''
+          projectStore.project?.widgets?.push(templateWidget)
+
+          copyFromId = templateWidget.id
+          obj.copyFrom = copyFromId
+          obj.isCopy = true
         }
-        obj.copyFrom = newWidget.id
-        obj.isCopy = true
         // 复制一份复用的
-        child.children.push({
-          ...newWidget,
-          id: v4()
-        })
+        const newWidget = projectStore.cloneWidgetTreeWithNewIdentity(obj)
+        newWidget.isCopy = true
+        newWidget.copyFrom = copyFromId
+        child.children.push(newWidget)
       }
     })
   }
@@ -408,24 +438,11 @@ export const useActionStore = defineStore('action', () => {
       ? target?.children || projectStore.activePage?.children || []
       : projectStore.activePage?.children || []
     const newArr: BaseWidget[] = []
-    const generateWidgetName = createWidgetNameGenerator(projectStore.activePage as Page)
 
     clipboard.value.forEach((obj) => {
       obj.props.x += 10
       obj.props.y += 10
-      const newWidget = klona({
-        ...obj,
-        // 最后一个_加索引
-        name: generateWidgetName(obj),
-        id: v4()
-      })
-      // 修改子节点ID
-      bfsWalk(newWidget, (child) => {
-        if (child?.id) {
-          child.name = generateWidgetName(child)
-          child.id = v4()
-        }
-      })
+      const newWidget = projectStore.cloneWidgetTreeWithNewIdentity(obj)
       list.push(newWidget)
       newArr.push(newWidget)
     })

+ 120 - 4
src/renderer/src/store/modules/project.ts

@@ -14,20 +14,29 @@ import type { Method } from '@/types/method'
 import type { BaseWidget } from '@/types/baseWidget'
 import type { Screen } from '@/types/screen'
 import type { Page } from '@/types/page'
+import type { IComponentModelConfig } from '@/lvgl-widgets/type'
+import type { WidgetNameIndexes } from '@/utils/widgetName'
 
 import { computed, ref, watch } from 'vue'
 import { defineStore } from 'pinia'
 import { klona } from 'klona'
-import { createBin, createScreen } from '@/model'
+import { createBin, createScreen, createWidget } from '@/model'
 import { useRecentProject } from './recentProject'
 import { v4 } from 'uuid'
 import dayjs from 'dayjs'
-import { ElMessage } from 'element-plus'
+import { ElMessage, ElMessageBox } from 'element-plus'
 import { useI18n } from 'vue-i18n'
 import { ComponentArray } from '@/lvgl-widgets'
 import { bfsWalk } from 'simple-mind-map/src/utils'
 import { useHistory } from '@/hooks/useHistory'
 import { DEFAULT_THEME_KEY } from '@/constants'
+import {
+  collectDuplicateProjectNames,
+  collectProjectNamedItems,
+  createProjectWidgetName,
+  normalizeProjectWidgetNameIndexes,
+  walkWidgetTree
+} from '@/utils/widgetName'
 
 export interface IProject {
   version: string
@@ -46,6 +55,7 @@ export interface IProject {
   languages: Language[]
   methods: Method[]
   screens: Screen[]
+  widgetNameIndexes?: WidgetNameIndexes
   currentLanguage?: string
   currentTheme?: string
 }
@@ -274,6 +284,99 @@ export const useProjectStore = defineStore('project', () => {
     project.value.resources.fonts ||= []
     project.value.resources.others ||= []
     project.value.resources.bezierAnimations ||= []
+    project.value.widgets ||= []
+    project.value.widgetNameIndexes ||= {}
+  }
+
+  const createUniqueProjectName = (base: string, currentId?: string) => {
+    const normalizedBase = base.trim() || 'name'
+    const usedNames = new Set(
+      collectProjectNamedItems(project.value)
+        .filter((item) => item.id !== currentId)
+        .map((item) => item.name.trim())
+    )
+
+    let index = 1
+    let name = `${normalizedBase}_${index}`
+    while (usedNames.has(name)) {
+      index += 1
+      name = `${normalizedBase}_${index}`
+    }
+
+    return name
+  }
+
+  const escapeHtml = (value: string) => {
+    return value.replace(/[&<>"']/g, (char) => {
+      const map: Record<string, string> = {
+        '&': '&amp;',
+        '<': '&lt;',
+        '>': '&gt;',
+        '"': '&quot;',
+        "'": '&#39;'
+      }
+
+      return map[char]
+    })
+  }
+
+  const resetWidgetTreeIdentity = (widget: BaseWidget) => {
+    walkWidgetTree(widget, (child) => {
+      child.id = v4()
+      child.name = createProjectWidgetName(project.value, child)
+    })
+
+    return widget
+  }
+
+  const createProjectWidget = (
+    schema: IComponentModelConfig | BaseWidget,
+    index = 1,
+    isCustom?: boolean
+  ) => {
+    return resetWidgetTreeIdentity(createWidget(schema, index, isCustom))
+  }
+
+  const cloneWidgetTreeWithNewIdentity = (widget: BaseWidget) => {
+    return resetWidgetTreeIdentity(klona(widget))
+  }
+
+  const confirmDuplicateNamesBeforeSave = async () => {
+    const duplicateNames = collectDuplicateProjectNames(project.value)
+    if (!duplicateNames.length) return true
+
+    const details = duplicateNames
+      .slice(0, 20)
+      .map(({ name, items }) => {
+        const itemDetails = items
+          .map((item) => {
+            return `<div class="mt-4px">- ${escapeHtml(item.kind)} / ${escapeHtml(item.type)} / ${escapeHtml(item.path)} / ${escapeHtml(item.id)}</div>`
+          })
+          .join('')
+
+        return `<div class="mb-10px"><div><b>${escapeHtml(name)}</b></div>${itemDetails}</div>`
+      })
+      .join('')
+    const more =
+      duplicateNames.length > 20
+        ? `<div>还有 ${duplicateNames.length - 20} 组重复名称未展示。</div>`
+        : ''
+
+    try {
+      await ElMessageBox.confirm(
+        `<div>项目中存在同名 name,请确认是否继续保存。点击确认将继续保存,点击取消可返回修改。</div><div class="mt-10px max-h-360px overflow-auto text-left">${details}${more}</div>`,
+        'name 重复',
+        {
+          type: 'warning',
+          confirmButtonText: '继续保存',
+          cancelButtonText: '返回修改',
+          dangerouslyUseHTMLString: true
+        }
+      )
+      return true
+    } catch {
+      return false
+    }
   }
 
   const removeThemeStyles = (themeKey: string) => {
@@ -357,7 +460,8 @@ export const useProjectStore = defineStore('project', () => {
       animations: [],
       languages: [],
       methods: [],
-      screens: []
+      screens: [],
+      widgetNameIndexes: {}
     }
     // 2、构建屏幕信息
     activePageId.value = ''
@@ -367,6 +471,7 @@ export const useProjectStore = defineStore('project', () => {
     meta.screens.forEach((screen, index) => {
       const newScreen = createScreen(screen)
       newScreen.name += `_${index + 1}`
+      newScreen.pages[0].name = createUniqueProjectName('new_page')
       project.value?.screens.push(newScreen)
       openPageIds.value.push(newScreen.pages[0].id)
     })
@@ -448,6 +553,7 @@ export const useProjectStore = defineStore('project', () => {
     project.value = newProject
     normalizeResources()
     normalizeThemes()
+    normalizeProjectWidgetNameIndexes(project.value)
     globalStyle.value = style
     currentMaxScreen.value = null
     const projectPath = project.value.meta.path + '\\' + project.value.meta.name
@@ -502,6 +608,7 @@ export const useProjectStore = defineStore('project', () => {
       for (let i = oldScreenCount; i < newScreenCount; i += 1) {
         const newScreen = createScreen(meta.screens[i])
         newScreen.name = `screen_${i + 1}`
+        newScreen.pages[0].name = createUniqueProjectName('new_page')
         project.value.screens.push(newScreen)
         openPageIds.value[i] = newScreen.pages[0].id
       }
@@ -522,7 +629,9 @@ export const useProjectStore = defineStore('project', () => {
       screen.name = `screen_${index + 1}`
 
       if (!screen.pages.length) {
-        screen.pages.push(createScreen(screenMeta).pages[0])
+        const newPage = createScreen(screenMeta).pages[0]
+        newPage.name = createUniqueProjectName('new_page')
+        screen.pages.push(newPage)
       }
 
       if (!openPageIds.value[index] || !screen.pages.some((page) => page.id === openPageIds.value[index])) {
@@ -560,6 +669,9 @@ export const useProjectStore = defineStore('project', () => {
       ElMessage.error(t('projectNotExist'))
       return
     }
+    normalizeProjectWidgetNameIndexes(project.value)
+    if (!(await confirmDuplicateNamesBeforeSave())) return
+
     // 1、保存项目
     await window.electron.ipcRenderer.invoke(
       'write-file',
@@ -674,6 +786,10 @@ export const useProjectStore = defineStore('project', () => {
     removeThemeStyles,
     renameThemeStyles,
     editProject,
+    createProjectWidget,
+    cloneWidgetTreeWithNewIdentity,
+    resetWidgetTreeIdentity,
+    createUniqueProjectName,
 
     // 历史记录
     history,

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

@@ -1,4 +1,4 @@
-export type AppType = 'analog_display' | 'chip' | 'board'
+export type AppType = 'simulator' | 'chip' | 'board'
 export type ScreenType = 'single' | 'double'
 export type ResourcePackaging = 'c' | 'c_bin'
 

+ 184 - 19
src/renderer/src/utils/widgetName.ts

@@ -3,14 +3,34 @@ import type { BaseWidget } from '@/types/baseWidget'
 import type { Page } from '@/types/page'
 
 import componentMap from '@/lvgl-widgets'
-import { customAlphabet } from 'nanoid'
-import { bfsWalk } from 'simple-mind-map/src/utils'
 
-type WidgetNameSource =
+export type WidgetNameIndexes = Record<string, number>
+
+type NamedProjectItemKind = 'screen' | 'page' | 'widget' | 'widgetTemplate'
+
+export type NamedProjectItem = {
+  id: string
+  name: string
+  type: string
+  kind: NamedProjectItemKind
+  path: string
+}
+
+type ProjectLike = {
+  widgetNameIndexes?: WidgetNameIndexes
+  screens?: Array<{
+    id: string
+    name: string
+    type: string
+    pages?: Page[]
+  }>
+  widgets?: BaseWidget[]
+}
+
+export type WidgetNameSource =
   | Pick<BaseWidget, 'type' | 'name'>
   | Pick<IComponentModelConfig, 'key' | 'defaultSchema'>
 
-const createNameSuffix = customAlphabet('0123456789abcdefghijklmnopqrstuvwxyz', 5)
 const widgetNameSuffixPattern = /_\d+(?:_[0-9A-Za-z]+)?$/
 
 const getWidgetType = (source: WidgetNameSource) => {
@@ -31,31 +51,176 @@ export const getWidgetNameBase = (source: WidgetNameSource) => {
 }
 
 export const createWidgetName = (source: WidgetNameSource, index: number) => {
-  return `${getWidgetNameBase(source)}_${index}_${createNameSuffix()}`
+  return `${getWidgetNameBase(source)}_${index}`
+}
+
+const escapeRegExp = (value: string) => {
+  return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
+}
+
+const getWidgetNameIndex = (name: string | undefined, base: string) => {
+  const match = String(name || '')
+    .trim()
+    .match(new RegExp(`^${escapeRegExp(base)}_(\\d+)(?:_[0-9A-Za-z]+)?$`))
+  const index = Number(match?.[1])
+
+  return Number.isSafeInteger(index) && index > 0 ? index : 0
+}
+
+const isWidgetNode = (node: any): node is BaseWidget => {
+  return !!node?.id && !!node?.type && !!node?.props
+}
+
+const walkWidgetChildren = (
+  children: any[] | undefined,
+  visitor: (widget: BaseWidget, path: string) => void,
+  parentPath: string
+) => {
+  children?.forEach((child, index) => {
+    if (isWidgetNode(child)) {
+      const childPath = `${parentPath}/${child.name || child.type || index + 1}`
+      visitor(child, childPath)
+      walkWidgetChildren(child.children, visitor, childPath)
+      return
+    }
+
+    walkWidgetChildren(child?.children, visitor, parentPath)
+  })
+}
+
+export const walkWidgetTree = (
+  widget: BaseWidget,
+  visitor: (widget: BaseWidget, path: string) => void,
+  parentPath = ''
+) => {
+  const path = parentPath ? `${parentPath}/${widget.name || widget.type}` : widget.name || widget.type
+  visitor(widget, path)
+  walkWidgetChildren(widget.children, visitor, path)
+}
+
+export const collectProjectNamedItems = (project?: ProjectLike) => {
+  const items: NamedProjectItem[] = []
+  if (!project) return items
+
+  project.screens?.forEach((screen, screenIndex) => {
+    const screenPath = screen.name || `screen_${screenIndex + 1}`
+    if (screen.id && screen.name) {
+      items.push({
+        id: screen.id,
+        name: screen.name,
+        type: screen.type,
+        kind: 'screen',
+        path: screenPath
+      })
+    }
+
+    screen.pages?.forEach((page, pageIndex) => {
+      const pagePath = `${screenPath}/${page.name || `page_${pageIndex + 1}`}`
+      if (page.id && page.name) {
+        items.push({
+          id: page.id,
+          name: page.name,
+          type: page.type,
+          kind: 'page',
+          path: pagePath
+        })
+      }
+
+      walkWidgetChildren(
+        page.children,
+        (widget, path) => {
+          if (!widget.name) return
+          items.push({
+            id: widget.id,
+            name: widget.name,
+            type: widget.type,
+            kind: 'widget',
+            path
+          })
+        },
+        pagePath
+      )
+    })
+  })
+
+  project.widgets?.forEach((widget, index) => {
+    walkWidgetTree(
+      widget,
+      (child, path) => {
+        if (!child.name) return
+        items.push({
+          id: child.id,
+          name: child.name,
+          type: child.type,
+          kind: 'widgetTemplate',
+          path
+        })
+      },
+      `widgets/${widget.name || index + 1}`
+    )
+  })
+
+  return items
+}
+
+export const collectDuplicateProjectNames = (project?: ProjectLike) => {
+  const groups: Record<string, NamedProjectItem[]> = {}
+
+  collectProjectNamedItems(project).forEach((item) => {
+    const name = item.name.trim()
+    if (!name) return
+    groups[name] ||= []
+    groups[name].push(item)
+  })
+
+  return Object.entries(groups)
+    .filter(([, items]) => items.length > 1)
+    .map(([name, items]) => ({
+      name,
+      items
+    }))
 }
 
-const collectWidgetTypeCounts = (page?: Page) => {
-  const counts: Record<string, number> = {}
-  if (!page) return counts
+export const normalizeProjectWidgetNameIndexes = (project?: ProjectLike) => {
+  const indexes: WidgetNameIndexes = {
+    ...(project?.widgetNameIndexes || {})
+  }
 
-  bfsWalk(page, (widget) => {
-    if (!widget?.type) return
-    counts[widget.type] = (counts[widget.type] || 0) + 1
+  if (!project) return indexes
+
+  const syncWidget = (widget: BaseWidget) => {
+    const base = getWidgetNameBase(widget)
+    indexes[base] = Math.max(indexes[base] || 0, getWidgetNameIndex(widget.name, base))
+  }
+
+  project.screens?.forEach((screen) => {
+    screen.pages?.forEach((page) => {
+      walkWidgetChildren(page.children, syncWidget, page.name)
+    })
   })
+  project.widgets?.forEach((widget) => walkWidgetTree(widget, syncWidget))
 
-  return counts
+  project.widgetNameIndexes = indexes
+  return indexes
 }
 
-export const createWidgetNameGenerator = (page?: Page) => {
-  const widgetTypeCounts = collectWidgetTypeCounts(page)
+export const createProjectWidgetName = (project: ProjectLike | undefined, source: WidgetNameSource) => {
+  if (!project) return createWidgetName(source, 1)
 
-  return (source: WidgetNameSource) => {
-    const widgetType = getWidgetType(source) || 'widget'
-    const nextIndex = (widgetTypeCounts[widgetType] || 0) + 1
-    widgetTypeCounts[widgetType] = nextIndex
+  const indexes = normalizeProjectWidgetNameIndexes(project)
+  const usedNames = new Set(collectProjectNamedItems(project).map((item) => item.name.trim()))
+  const base = getWidgetNameBase(source)
+  let nextIndex = (indexes[base] || 0) + 1
+  let nextName = createWidgetName(source, nextIndex)
 
-    return createWidgetName(source, nextIndex)
+  while (usedNames.has(nextName)) {
+    nextIndex += 1
+    nextName = createWidgetName(source, nextIndex)
   }
+
+  indexes[base] = nextIndex
+  project.widgetNameIndexes = indexes
+  return nextName
 }
 
 export const isDuplicateWidgetName = (

+ 158 - 132
src/renderer/src/views/designer/config/VariableConfig.vue

@@ -1,141 +1,149 @@
 <template>
-  <el-scrollbar height="calc(100vh - 130px)" class="config">
-    <ViewTitle :title="t('pageVariables')">
-      <template #right>
-        <el-button text @click="addPageVariables"
-          ><el-icon class="cursor-pointer"> <Plus /> </el-icon
-        ></el-button>
-      </template>
-    </ViewTitle>
-    <div class="p-10px">
-      <!-- 变量表格 -->
-      <el-table :data="pageVariables || []">
-        <el-table-column :label="t('variableName')">
-          <template #default="{ row }">
-            <el-input
-              spellcheck="false"
-              v-model="row.name"
-              placeholder="请输入"
-              @input="(val) => handleVariableNameInput(row, val)"
-            />
-          </template>
-        </el-table-column>
-        <el-table-column :label="t('variableType')">
-          <template #default="{ row }">
-            <el-select v-model="row.type" placeholder="请选择">
-              <el-option
-                v-for="type in variableType"
-                :key="type.value"
-                :label="type.label"
-                :value="type.value"
-              />
-            </el-select>
-          </template>
-        </el-table-column>
-        <el-table-column :label="t('initialValue')">
-          <template #default="{ row }">
-            <el-input spellcheck="false" v-model="row.value" placeholder="请输入" />
-          </template>
-        </el-table-column>
-        <el-table-column width="40">
-          <template #default="{ row }">
-            <el-icon
-              class="cursor-pointer"
-              @click="handleVariablesRemove(pageVariables || [], row, 'page')"
-            >
-              <Delete />
-            </el-icon>
-          </template>
-        </el-table-column>
-      </el-table>
-    </div>
-
-    <ViewTitle :title="t('globalVariables')">
-      <template #right>
-        <el-button text @click="addVariableGroup"
-          ><el-icon class="cursor-pointer"> <Plus /> </el-icon
-        ></el-button>
-      </template>
-    </ViewTitle>
-    <el-form label-position="top">
-      <el-collapse v-for="item in globalVariables" :key="item.id" v-model="activeNames">
-        <el-collapse-item :name="item.id">
-          <template #title>
-            <div class="collapse-title">
-              <el-icon class="arrow" :class="{ active: activeNames.includes(item.id) }">
-                <ArrowRight />
-              </el-icon>
-
-              <span class="title-text mr-12px flex items-center">
+  <div class="w-full h-full" style="height: calc(100vh - 130px)">
+    <SplitterCollapse>
+      <SplitterCollapseItem :title="t('pageVariables')">
+        <template #header-right>
+          <el-button text @click.stop="addPageVariables">
+            <el-icon class="cursor-pointer"> <Plus /> </el-icon>
+          </el-button>
+        </template>
+        <div class="p-10px">
+          <!-- 变量表格 -->
+          <el-table :data="pageVariables || []">
+            <el-table-column :label="t('variableName')">
+              <template #default="{ row }">
                 <el-input
-                  v-model="item.name"
-                  placeholder="请输入组名"
-                  size="small"
-                  class="mt-10px mb-10px"
+                  spellcheck="false"
+                  v-model="row.name"
+                  placeholder="请输入"
+                  @input="(val) => handleVariableNameInput(row, val)"
                 />
-              </span>
-
-              <el-icon class="mr-10px" @click.stop="addVariables(item.variables)">
-                <Plus />
-              </el-icon>
-              <el-icon @click.stop="removeVariables(item)">
-                <Delete />
-              </el-icon>
-            </div>
-          </template>
-          <div class="p-10px">
-            <!-- 变量表格 -->
-            <el-table :data="item.variables">
-              <el-table-column :label="t('variableName')">
-                <template #default="{ row }">
-                  <el-input
-                    spellcheck="false"
-                    v-model="row.name"
-                    placeholder="请输入"
-                    @input="(val) => handleVariableNameInput(row, val)"
+              </template>
+            </el-table-column>
+            <el-table-column :label="t('variableType')">
+              <template #default="{ row }">
+                <el-select
+                  v-model="row.type"
+                  placeholder="请选择"
+                  :disabled="isVariableUsed(row.id)"
+                >
+                  <el-option
+                    v-for="type in variableType"
+                    :key="type.value"
+                    :label="type.label"
+                    :value="type.value"
                   />
-                </template>
-              </el-table-column>
-              <el-table-column :label="t('variableType')">
-                <template #default="{ row }">
-                  <el-select v-model="row.type" placeholder="请选择">
-                    <el-option
-                      v-for="type in variableType"
-                      :key="type.value"
-                      :label="type.label"
-                      :value="type.value"
+                </el-select>
+              </template>
+            </el-table-column>
+            <el-table-column :label="t('initialValue')">
+              <template #default="{ row }">
+                <el-input spellcheck="false" v-model="row.value" placeholder="请输入" />
+              </template>
+            </el-table-column>
+            <el-table-column width="40">
+              <template #default="{ row }">
+                <el-icon
+                  class="cursor-pointer"
+                  @click="handleVariablesRemove(pageVariables || [], row, 'page')"
+                >
+                  <Delete />
+                </el-icon>
+              </template>
+            </el-table-column>
+          </el-table>
+        </div>
+      </SplitterCollapseItem>
+
+      <SplitterCollapseItem :title="t('globalVariables')">
+        <template #header-right>
+          <el-button text @click.stop="addVariableGroup">
+            <el-icon class="cursor-pointer"> <Plus /> </el-icon>
+          </el-button>
+        </template>
+        <el-form label-position="top">
+          <el-collapse v-for="item in globalVariables" :key="item.id" v-model="activeNames">
+            <el-collapse-item :name="item.id">
+              <template #title>
+                <div class="collapse-title">
+                  <el-icon class="arrow" :class="{ active: activeNames.includes(item.id) }">
+                    <ArrowRight />
+                  </el-icon>
+
+                  <span class="title-text mr-12px flex items-center">
+                    <el-input
+                      v-model="item.name"
+                      placeholder="请输入组名"
+                      size="small"
+                      class="mt-10px mb-10px"
                     />
-                  </el-select>
-                </template>
-              </el-table-column>
-              <el-table-column :label="t('initialValue')">
-                <template #default="{ row }">
-                  <el-input spellcheck="false" v-model="row.value" placeholder="请输入" />
-                </template>
-              </el-table-column>
-              <el-table-column width="40">
-                <template #default="{ row }">
-                  <el-icon
-                    class="cursor-pointer"
-                    @click="handleVariablesRemove(item.variables, row, 'global')"
-                  >
+                  </span>
+
+                  <el-icon class="mr-10px" @click.stop="addVariables(item.variables)">
+                    <Plus />
+                  </el-icon>
+                  <el-icon @click.stop="removeVariables(item)">
                     <Delete />
                   </el-icon>
-                </template>
-              </el-table-column>
-            </el-table>
-          </div>
-        </el-collapse-item>
-      </el-collapse>
-      <el-empty v-if="globalVariables.length === 0" description="暂无全局变量" />
-    </el-form>
-  </el-scrollbar>
+                </div>
+              </template>
+              <div class="p-10px">
+                <!-- 变量表格 -->
+                <el-table :data="item.variables">
+                  <el-table-column :label="t('variableName')">
+                    <template #default="{ row }">
+                      <el-input
+                        spellcheck="false"
+                        v-model="row.name"
+                        placeholder="请输入"
+                        @input="(val) => handleVariableNameInput(row, val)"
+                      />
+                    </template>
+                  </el-table-column>
+                  <el-table-column :label="t('variableType')">
+                    <template #default="{ row }">
+                      <el-select
+                        v-model="row.type"
+                        placeholder="请选择"
+                        :disabled="isVariableUsed(row.id)"
+                      >
+                        <el-option
+                          v-for="type in variableType"
+                          :key="type.value"
+                          :label="type.label"
+                          :value="type.value"
+                        />
+                      </el-select>
+                    </template>
+                  </el-table-column>
+                  <el-table-column :label="t('initialValue')">
+                    <template #default="{ row }">
+                      <el-input spellcheck="false" v-model="row.value" placeholder="请输入" />
+                    </template>
+                  </el-table-column>
+                  <el-table-column width="40">
+                    <template #default="{ row }">
+                      <el-icon
+                        class="cursor-pointer"
+                        @click="handleVariablesRemove(item.variables, row, 'global')"
+                      >
+                        <Delete />
+                      </el-icon>
+                    </template>
+                  </el-table-column>
+                </el-table>
+              </div>
+            </el-collapse-item>
+          </el-collapse>
+          <el-empty v-if="globalVariables.length === 0" description="暂无全局变量" />
+        </el-form>
+      </SplitterCollapseItem>
+    </SplitterCollapse>
+  </div>
 </template>
 
 <script setup lang="ts">
 import { computed, ref } from 'vue'
-import type { Variable, VariableGroup } from '@/types/variables'
-import type { BaseWidget } from '@/types/baseWidget'
 import { ArrowRight, Plus, Delete } from '@element-plus/icons-vue'
 import { ElMessageBox } from 'element-plus'
 import { variableType } from '@/constants'
@@ -143,6 +151,10 @@ import { v4 } from 'uuid'
 import { useI18n } from 'vue-i18n'
 import { useProjectStore } from '@/store/modules/project'
 import { findVariableUsages } from '@/utils/variableBinding'
+import { SplitterCollapse, SplitterCollapseItem } from '@/components/SplitterCollapse'
+
+import type { Variable, VariableGroup } from '@/types/variables'
+import type { BaseWidget } from '@/types/baseWidget'
 
 interface Emits {
   (e: 'update:variables', val: VariableGroup[]): void
@@ -178,7 +190,7 @@ const handleVariableNameInput = (row: Variable, value: string) => {
 }
 
 const addVariables = (variables) => {
-  variables.push({
+  variables.unshift({
     id: v4(),
     name: `var_${variables.length + 1}`,
     value: '',
@@ -199,7 +211,7 @@ const walkWidgets = (widgets: BaseWidget[] = [], callback: (widget: BaseWidget)
 const addPageVariables = () => {
   // todo: 打开弹窗,列出当前页面全部控件,然后根据控件属性选择变量
   // 选择属性后会自动绑定属性的变量类型,初始值使用属性的当前值
-  props.pageVariables?.push({
+  props.pageVariables?.unshift({
     id: v4(),
     name: '',
     value: '',
@@ -207,6 +219,18 @@ const addPageVariables = () => {
   })
 }
 
+/**
+ * 变量是否被使用
+ */
+const isVariableUsed = (varId: string) => {
+  const pages = projectStore.project?.screens.flatMap((item) => item.pages) || []
+  const usageCount = findVariableUsages(varId, [
+    { children: pages }
+  ] as unknown as BaseWidget[]).length
+
+  return usageCount > 0
+}
+
 /**
  * 删除变量操作
  * @variables 变量列表
@@ -226,8 +250,10 @@ const handleVariablesRemove = (
     }
   }
 
-  const pageChildren = projectStore.activePage?.children || []
-  const usageCount = findVariableUsages(varItem.id, pageChildren).length
+  const pages = projectStore.project?.screens.flatMap((item) => item.pages) || []
+  const usageCount = findVariableUsages(varItem.id, [
+    { children: pages }
+  ] as unknown as BaseWidget[]).length
 
   if (usageCount === 0) {
     deleteVariable()
@@ -270,7 +296,7 @@ const removeVariables = (variables) => {
  */
 const addVariableGroup = () => {
   const id = v4()
-  globalVariables.value.push({
+  globalVariables.value.unshift({
     id,
     name: `vargroup_${globalVariables.value.length + 1}`,
     variables: []

+ 1 - 1
src/renderer/src/views/designer/config/property/components/StyleBackground.vue

@@ -39,7 +39,7 @@
     </el-form-item>
     <el-form-item v-if="!onlyColor" label="图片遮罩" label-position="left" label-width="70px">
       <!-- 支持变量 -->
-      <VariableBindWrapper v-model="imageAlpha" :variable-config="{ type: 'uint8_t' }">
+      <VariableBindWrapper v-model="imageColor" :variable-config="{ type: 'int32_t' }">
         <div class="flex items-center">
           <ColorPicker
             use-type="pure"

+ 1 - 1
src/renderer/src/views/designer/config/property/components/StyleBorder.vue

@@ -73,7 +73,7 @@
     <el-form-item label-position="left" label-width="0px" v-if="!onlyRadius">
       <!-- 支持变量 -->
       <VariableBindWrapper
-        v-model="radius"
+        v-model="side"
         :variable-config="{
           type: 'enum',
           enumMap: {

+ 2 - 1
src/renderer/src/views/designer/config/property/components/VariableBindWrapper.vue

@@ -141,6 +141,7 @@ const addPageVariables = () => {
     inputErrorMessage: '变量名称只能包含字母、数字和下划线,且必须以字母或下划线开头'
   })
     .then(({ value }) => {
+      const originValue = modelValue.value
       let val = modelValue.value
       // 如果是枚举
       if (props.variableConfig?.type === 'enum' && props.variableConfig.enumMap) {
@@ -161,7 +162,7 @@ const addPageVariables = () => {
       projectStore.activePage?.variables?.push(newVar)
       modelValue.value = {
         varId: newVar.id,
-        originValue: val,
+        originValue,
         source: 'page',
         ...(props.variableConfig?.type === 'enum' && props.variableConfig.enumMap
           ? { enumMap: props.variableConfig.enumMap }

+ 16 - 9
src/renderer/src/views/designer/modals/projectModal/index.vue

@@ -8,7 +8,9 @@
     :close-on-click-modal="false"
     :before-close="close"
     align-center
-   append-to-body draggable>
+    append-to-body
+    draggable
+  >
     <div class="w-full h-full flex">
       <el-scrollbar wrap-class="pr-12px" class="flex-1">
         <el-form
@@ -46,7 +48,11 @@
             </el-input>
           </el-form-item>
           <el-form-item :label="$t('projectType')" prop="type">
-            <el-select v-model="formData.type" @change="handlChangeType" :disabled="mode === 'edit'">
+            <el-select
+              v-model="formData.type"
+              @change="handlChangeType"
+              :disabled="mode === 'edit'"
+            >
               <el-option
                 v-for="item in typeOptions"
                 :key="item.value"
@@ -120,7 +126,6 @@
                 size="small"
                 fill="#6cf"
                 @change="handleChangeScreenTypeByChip"
-                
               >
                 <el-radio-button :label="$t('singleScreen')" value="single" />
                 <el-radio-button :label="$t('doubleScreen')" value="dual" />
@@ -357,7 +362,7 @@
             </el-row>
           </template>
           <!-- 虚拟显示 -->
-          <template v-if="formData.type === 'analog_display'">
+          <template v-if="formData.type === 'simulator'">
             <div class="flex items-center justify-center gap-12px">
               <el-radio-group
                 v-model="formData.screenType"
@@ -493,7 +498,9 @@
       width="440px"
       :modal="false"
       align-center
-     append-to-body draggable>
+      append-to-body
+      draggable
+    >
       <el-form ref="resolutionFormRef" :model="customScreen" hide-required-asterisk>
         <el-form-item
           :label="$t('width')"
@@ -640,7 +647,7 @@ const typeOptions = computed(() => {
     appStore.lang && [
       { label: t('chip'), value: 'chip' },
       { label: t('board'), value: 'board' },
-      { label: t('analogDisplay'), value: 'analog_display' }
+      { label: t('analogDisplay'), value: 'simulator' }
     ]
   )
 })
@@ -693,7 +700,7 @@ const rules = computed(() => {
 
 // 切换项目类型
 const handlChangeType = (type: string) => {
-  if (type === 'analog_display') {
+  if (type === 'simulator') {
     formData.imageCompress = getImageCompress(displayConfig.simulation_display.image_compression)
     formData.videoFormats = displayConfig.simulation_display.video_formats
   }
@@ -804,11 +811,11 @@ const handleSetScreenParams = (index: number) => {
 
 // 选择芯片屏幕类型
 const handleChangeScreenTypeByChip = async (type: any) => {
-  const canChange = await handleChangeScreenType(type);
+  const canChange = await handleChangeScreenType(type)
   if (!canChange) {
     formData.screenType = type === 'single' ? 'dual' : 'single'
     return
-  };
+  }
 
   let screens =
     type === 'single'

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

@@ -23,7 +23,6 @@ import { ref, onMounted, onBeforeUnmount } from 'vue'
 import { useEventBus } from '@vueuse/core'
 import { getAllIDBData } from '@/utils/database'
 import { useProjectStore } from '@/store/modules/project'
-import { createWidget } from '@/model'
 
 import CompLibaryItem from './CompLibaryItem.vue'
 
@@ -39,7 +38,7 @@ const getAllData = async () => {
 }
 
 const handleAdd = (item: ICustomWidget) => {
-  const newWidget = createWidget(item.widget, 1, true)
+  const newWidget = projectStore.createProjectWidget(item.widget, 1, true)
   const page = projectStore.activePage
   // 查找当前screen
   const screen = projectStore.project?.screens.find(

+ 2 - 2
src/renderer/src/views/designer/sidebar/components/ScreenTreeItem.vue

@@ -83,9 +83,9 @@ const actionStore = useActionStore()
 
 const addPage = (screen: Screen) => {
   const newPage = createPage()
+  newPage.name = projectStore.createUniqueProjectName(newPage.name)
   screen.pages.push({
-    ...newPage,
-    name: `${newPage.name}_${screen.pages.length + 1}`
+    ...newPage
   })
 
   projectStore.activePageId = newPage.id

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

@@ -33,8 +33,6 @@
 import { computed, ref } from 'vue'
 import { ComponentArray } from '@/lvgl-widgets'
 import LibaryItem from './LibaryItem.vue'
-import { createWidget } from '@/model'
-import { getAddWidgetIndex } from '@/utils'
 import { useProjectStore } from '@/store/modules/project'
 
 import type { IComponentModelConfig } from '@/lvgl-widgets/type'
@@ -86,8 +84,7 @@ const getGroups = computed(() => {
 // 处理点击添加控件
 function handleAdd(item: IComponentModelConfig) {
   const page = projectStore.activePage
-  const index = getAddWidgetIndex(page!, item.key)
-  const newWidget = createWidget(item, index)
+  const newWidget = projectStore.createProjectWidget(item)
   // 查找当前screen
   const screen = projectStore.project?.screens.find(
     (screen) => !!screen.pages.find((p) => page?.id === p.id)

+ 2 - 5
src/renderer/src/views/designer/workspace/stage/Node.vue

@@ -48,7 +48,6 @@ import type { CSSProperties } from 'vue'
 
 import { computed, inject, nextTick, onBeforeUnmount, ref, watch } from 'vue'
 import { useDrop, useMouse } from 'vue-hooks-plus'
-import { createWidget } from '@/model'
 import LvglWidgets from '@/lvgl-widgets'
 import { useProjectStore } from '@/store/modules/project'
 import { useAppStore } from '@/store/modules/app'
@@ -57,14 +56,13 @@ import { get, has, isEmpty } from 'lodash-es'
 import { getAllVariables, resolveVariableBoundValue } from '@/utils/variableBinding'
 
 import ContextMenu from './ContextMenu.vue'
-import { getAddWidgetIndex, isDescendant } from '@/utils'
+import { isDescendant } from '@/utils'
 import { klona } from 'klona'
 
 defineOptions({
   name: 'NodeItem'
 })
 
-const page = inject<Page>('page')
 const pageState = inject<StageState>('state')
 
 const props = defineProps<{
@@ -283,8 +281,7 @@ useDrop(nodeRef, {
     if (!content || !schema?.children) return
 
     const { x, y } = getDropPosition(event)
-    const index = getAddWidgetIndex(page!, content?.key)
-    const newWidget = createWidget(content, index, !!content?.id)
+    const newWidget = projectStore.createProjectWidget(content, 1, !!content?.id)
     newWidget.props.x = x
     newWidget.props.y = y