jiaxing.liao недель назад: 3
Родитель
Сommit
91d9d1a8bc

+ 1 - 0
package.json

@@ -26,6 +26,7 @@
     "@vueuse/components": "^14.0.0",
     "@vueuse/core": "^14.0.0",
     "element-plus": "^2.11.4",
+    "klona": "^2.0.6",
     "monaco-editor": "^0.54.0",
     "normalize.css": "^8.0.1",
     "pinia": "^3.0.3",

+ 9 - 0
pnpm-lock.yaml

@@ -23,6 +23,9 @@ importers:
       element-plus:
         specifier: ^2.11.4
         version: 2.11.4(vue@3.5.22(typescript@5.9.3))
+      klona:
+        specifier: ^2.0.6
+        version: 2.0.6
       monaco-editor:
         specifier: ^0.54.0
         version: 0.54.0
@@ -2110,6 +2113,10 @@ packages:
   keyv@4.5.4:
     resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
 
+  klona@2.0.6:
+    resolution: {integrity: sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==}
+    engines: {node: '>= 8'}
+
   kolorist@1.8.0:
     resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==}
 
@@ -5454,6 +5461,8 @@ snapshots:
     dependencies:
       json-buffer: 3.0.1
 
+  klona@2.0.6: {}
+
   kolorist@1.8.0: {}
 
   lazy-val@1.0.5: {}

+ 3 - 1
project.json5

@@ -13,7 +13,7 @@
     // 项目类型
     "type": "chip", // 'analog_display' | 'chip' | 'board' 1: 模拟显示 2: 芯片 3: 板卡
     // 屏幕类型
-    "screenType": "single", // 'single' | 'double' 1:单屏 2:双屏
+    "screenType": "single", // 'single' | 'dual' 1:单屏 2:双屏
     // 语言
     "language": "zh-cn",
     // 资源打包方式
@@ -139,6 +139,8 @@
   "widgets": [
     {
       "id": "copy_obj_1",
+      // 控件名称
+      "name": "copy_obj_1",
       // 控件类型
       "widgetType": "lv_object",
       // 属性 根据每个控件生成

+ 41 - 0
src/main/files.ts

@@ -1,5 +1,7 @@
 import { dialog } from 'electron'
 
+const fs = require('fs')
+
 /**
  * 打开文件夹
  */
@@ -11,6 +13,7 @@ export const openDirectory = async () => {
   if (!canceled) {
     return filePaths[0]
   }
+  return
 }
 
 /**
@@ -22,4 +25,42 @@ export const openFile = async () => {
   if (!canceled) {
     return filePaths[0]
   }
+  return
+}
+
+/**
+ * 保存文件
+ */
+export const writeFile = async (content: string, filePath: string) => {
+  fs.writeFileSync(filePath, content)
+}
+
+/**
+ * 读取文件
+ */
+export const readFile = async (filePath: string) => {
+  return fs.readFileSync(filePath, 'utf-8')
+}
+
+/**
+ * 检查文件是否存在
+ */
+export const checkFileExists = async (filePath: string) => {
+  return fs.existsSync(filePath)
+}
+
+/**
+ * 创建目录
+ */
+export const createDirectory = async (directoryPath: string) => {
+  fs.mkdirSync(directoryPath, { recursive: true })
+}
+
+export default {
+  openDirectory,
+  openFile,
+  readFile,
+  writeFile,
+  checkFileExists,
+  createDirectory
 }

+ 2 - 0
src/main/index.ts

@@ -20,6 +20,8 @@ function createWindow(): void {
     }
   })
 
+  mainWindow.removeMenu()
+
   mainWindow.on('ready-to-show', () => {
     mainWindow.show()
   })

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

@@ -27,12 +27,14 @@ declare module 'vue' {
     ElFormItem: typeof import('element-plus/es')['ElFormItem']
     ElFromItem: typeof import('element-plus/es')['ElFromItem']
     ElHeader: typeof import('element-plus/es')['ElHeader']
+    ElImage: typeof import('element-plus/es')['ElImage']
     ElInput: typeof import('element-plus/es')['ElInput']
     ElMain: typeof import('element-plus/es')['ElMain']
     ElOption: typeof import('element-plus/es')['ElOption']
     ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
     ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
     ElRow: typeof import('element-plus/es')['ElRow']
+    ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
     ElSelect: typeof import('element-plus/es')['ElSelect']
     ElSlider: typeof import('element-plus/es')['ElSlider']
     ElSpace: typeof import('element-plus/es')['ElSpace']

+ 4 - 1
src/renderer/index.html

@@ -13,12 +13,15 @@
         width: 100vw;
         height: 100vh;
         user-select: none;
+        font-size: 12px;
       }
     </style>
   </head>
 
   <body>
-    <div id="app"></div>
+    <div id="app">
+      <div>应用启动中...</div>
+    </div>
     <script type="module" src="/src/main.ts"></script>
   </body>
 </html>

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

@@ -8,11 +8,11 @@
           @click="isExpanded ? collapse() : expand()"
         >
           <div class="left">
-            <span class="mr-8px text-text-secondary">
+            <span class="mr-8px text-text-primary">
               <ai-outline-down :size="12" v-if="isExpanded" />
               <ai-outline-right :size="12" v-else />
             </span>
-            <span class="text-14px text-text-secondary">{{ title }}</span>
+            <span class="text-text-primary font-bold">{{ title }}</span>
           </div>
           <div class="right">
             <slot name="header-right"></slot>

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

@@ -4,7 +4,7 @@ import en_US from './en_US.json'
 
 export default createI18n({
   legacy: false,
-  locale: localStorage.getItem('lang') || 'zh_CN',
+  locale: localStorage.getItem('lang') || 'en_US',
   messages: {
     zh_CN: zh_CN,
     en_US: en_US

+ 79 - 0
src/renderer/src/model/index.ts

@@ -0,0 +1,79 @@
+import type { Page } from '@/types/page'
+import type { Screen } from '@/types/screen'
+import type { ScreenConfig } from '@/types/appMeta'
+import type { Bin } from '@/types/bins'
+
+import { v4 } from 'uuid'
+
+/**
+ * 创建屏幕
+ * @param config 屏幕配置
+ * @returns Screen
+ */
+export const createScreen = (config: ScreenConfig): Screen => {
+  return {
+    // ID
+    id: v4(),
+    // 名称
+    name: `screen_${config.type}`,
+    // 类型
+    type: config.type,
+    // 屏幕宽
+    width: config.width,
+    // 屏幕高
+    height: config.height,
+    // 隐藏
+    hidden: false,
+    // 锁定
+    locked: false,
+    // 页面
+    pages: [createPage()],
+    // 元信息
+    meta: config
+  }
+}
+
+/**
+ * 创建页面
+ */
+export const createPage = (): Page => {
+  return {
+    // 页面ID
+    id: v4(),
+    // 页面名称
+    name: 'new_page',
+    // 类型
+    type: 'page',
+    // 隐藏
+    hidden: false,
+    // 锁定
+    locked: false,
+    // 参考线
+    referenceLine: [],
+    // 属性
+    props: {},
+    // 样式
+    style: {},
+    // 事件
+    events: [],
+    // 页面变量
+    variables: [],
+    // 子组件
+    children: []
+  }
+}
+
+/**
+ * 创建BIN
+ * @param index BIN索引
+ */
+export const createBin = (index: number): Bin => {
+  return {
+    // BIN索引
+    id: v4(),
+    // BIN名称
+    name: `bin_${index}`,
+    // BIN文件
+    path: ''
+  }
+}

+ 5 - 0
src/renderer/src/store/modules/history.ts

@@ -0,0 +1,5 @@
+import { defineStore } from 'pinia'
+
+export const useHistoryStore = defineStore('history', () => {
+  return {}
+})

+ 64 - 6
src/renderer/src/store/modules/project.ts

@@ -1,18 +1,76 @@
 import type { AppMeta } from '@/types/appMeta'
+import type { Bin } from '@/types/bins'
+import type { ImageResource, OtherResource, FontResource } from '@/types/resource'
+import type { Variable } from '@/types/variables'
+import type { Theme } from '@/types/theme'
+import type { Animation } from '@/types/animation'
+import type { Language } from '@/types/language'
+import type { Method } from '@/types/method'
+import type { BaseWidget } from '@/types/baseWidget'
+import type { Screen } from '@/types/screen'
+
 import { ref } from 'vue'
 import { defineStore } from 'pinia'
+import { klona } from 'klona'
+import { createBin, createScreen } from '@/model'
 
 export const useProjectStore = defineStore('project', () => {
-  // 应用元信息
-  const appMeta = ref<AppMeta>()
+  const project = ref<{
+    version: string
+    meta: AppMeta
+    bins: Bin[]
+    resources: {
+      images: ImageResource[]
+      fonts: FontResource[]
+      others: OtherResource[]
+    }
+    widgets: BaseWidget[]
+    variables: Variable[]
+    themes: Theme[]
+    animations: Animation[]
+    languages: Language[]
+    methods: Method[]
+    screens: Screen[]
+  }>()
 
-  // 创建应用
+  /**
+   * 创建应用
+   * @param meta 应用元信息
+   */
   const createApp = (meta: AppMeta) => {
-    appMeta.value = meta
+    // 1、应用元信息
+    project.value = {
+      version: '1.0.0',
+      meta: klona(meta),
+      resources: {
+        images: [],
+        fonts: [],
+        others: []
+      },
+      bins: [],
+      widgets: [],
+      variables: [],
+      themes: [],
+      animations: [],
+      languages: [],
+      methods: [],
+      screens: []
+    }
+    // 2、构建屏幕信息
+    meta.screens.forEach((screen) => {
+      project.value?.screens.push(createScreen(screen))
+    })
+
+    // 3、创建BIN
+    if (meta.resourcePackaging === 'c_bin' && meta.binNum > 0) {
+      for (let i = 0; i < meta.binNum; i++) {
+        project.value.bins.push(createBin(i))
+      }
+    }
   }
 
   return {
-    appMeta,
-    createApp
+    createApp,
+    project
   }
 })

+ 0 - 117
src/renderer/src/store/modules/stage.ts

@@ -1,117 +0,0 @@
-import { defineStore } from 'pinia'
-
-import { ReferLine } from './type'
-
-interface StageState {
-  // 缩放比例
-  scale: number
-  // 大屏宽度
-  width: number
-  // 大屏高度
-  height: number
-  // x坐标原点位置
-  originX: number
-  // y坐标原点位置
-  originY: number
-  // 视口宽度
-  viewportWidth: number
-  // 视口高度
-  viewportHeight: number
-  // 屏幕中心原点x
-  centerX: number
-  // 屏幕中心原点y
-  centerY: number
-  // x坐标滚动
-  scrollX: number
-  // y坐标滚动
-  scrollY: number
-  // 容器宽度
-  wrapperWidth: number
-  // 容器高度
-  wrapperHeight: number
-  // 显示图层面板
-  showLayer: boolean
-}
-
-export const useStageStore = defineStore('stage', {
-  state: (): StageState => ({
-    scale: 1,
-    width: 1280,
-    height: 720,
-    originX: 0,
-    originY: 0,
-    viewportWidth: 0,
-    viewportHeight: 0,
-    centerX: 0,
-    centerY: 0,
-    scrollX: 0,
-    scrollY: 0,
-    wrapperWidth: 0,
-    wrapperHeight: 0,
-    showLayer: false
-  }),
-  getters: {
-    // 根据滚动和缩放,重新计算辅助线位置
-    getReferLines(state): ReferLine[] {
-      const { scale, scrollX, scrollY, originX, originY } = state
-      // const projectStore = useProjectStore();
-      const referLines: ReferLine[] = []
-      return referLines
-        .map((line) => {
-          let x = line.x || 0
-          let y = line.y || 0
-          if (line.type === 'horizontal') {
-            x = originX + line.value * scale - scrollX + 20
-          } else {
-            y = originY + line.value * scale - scrollY + 20
-          }
-
-          return {
-            ...line,
-            x,
-            y
-          }
-        })
-        .filter((line) => {
-          // 过滤掉不在视口内的辅助线
-          if (line.type === 'horizontal') {
-            return line.x > 20 && line.x < state.viewportWidth
-          } else {
-            return line.y > 20 && line.y < state.viewportHeight
-          }
-        })
-    }
-  },
-  actions: {
-    setSize(width: number, height: number) {
-      this.width = width
-      this.height = height
-    },
-    setStageWaraaperSize(width: number, height: number) {
-      this.wrapperWidth = width
-      this.wrapperHeight = height
-    },
-    setViewportSize(width: number, height: number) {
-      this.viewportWidth = width
-      this.viewportHeight = height
-    },
-    setOriginPoint(originX: number, originY: number) {
-      this.originX = originX
-      this.originY = originY
-    },
-    setScale(scale: number) {
-      this.scale = scale
-    },
-    setCenterPoint(x: number, y: number) {
-      this.centerX = x
-      this.centerY = y
-    },
-    setScroll(x: number, y: number) {
-      this.scrollX = x
-      this.scrollY = y
-    },
-    toggleShowLayer() {
-      this.showLayer = !this.showLayer
-    }
-  }
-})

+ 0 - 12
src/renderer/src/store/modules/type.d.ts

@@ -1,12 +0,0 @@
-declare interface ReferLine {
-  // 辅助线唯一标识
-  key: string
-  // 辅助线类型
-  type: 'horizontal' | 'vertical' | null
-  // 辅助线位置
-  value: number
-  // x坐标
-  x: number
-  // y坐标
-  y: number
-}

+ 1 - 1
src/renderer/src/theme/vars.css

@@ -3,7 +3,7 @@
   --bg-primary: #1e1e1e; /* 主背景 */
   --bg-secondary: #252526; /* 侧边栏/二级背景 */
   --bg-tertiary: #2d2d2d; /* 工具栏/三级背景 */
-  --text-primary: #d4d4d4; /* 主要文本 */
+  --text-primary: #cccccc; /* 主要文本 */
   --text-secondary: #858585; /* 次要文本 */
   --text-active: #ffffff; /* 激活文本 */
   --accent-blue: #569cd6; /* 蓝色强调色 (VS Code blue) */

+ 18 - 0
src/renderer/src/types/baseWidget.d.ts

@@ -0,0 +1,18 @@
+import type { WidgetEvent } from './event'
+
+export type BaseWidget = {
+  // ID
+  id: string
+  // 控件名称
+  name: string
+  // 控件类型
+  widgetType: string
+  // 控件属性
+  props: Record<string, any>
+  // 样式
+  style: Record<string, any>
+  // 事件
+  events: WidgetEvent[]
+  // 子控件
+  children: BaseWidget[]
+}

+ 18 - 0
src/renderer/src/types/event.d.ts

@@ -0,0 +1,18 @@
+export type WidgetEvent = {
+  // 事件ID
+  id: string
+  // 事件名称
+  name: string
+  // 触发事件
+  trigger: string
+  // 动作类型
+  type: string // 'play_animation' | 'function' -> play_animation: 播放动画 function: 执行函数
+  // 动画ID
+  animation: string
+  // 动画播放前函数ID
+  animationPlayerBeforeEvent: string
+  // 动画播放后函数ID
+  animationPlayerAfterEvent: string
+  // 函数ID
+  function: string
+}

+ 9 - 4
src/renderer/src/types/screen.d.ts

@@ -1,18 +1,23 @@
+import type { ScreenConfig } from './appMeta'
+import type { Page } from './page'
+
 export type Screen = {
   // ID
   id: string
   // 名称
   name: string
   // 类型
-  type: 'screen'
-  // 屏幕宽 未设置取通用配置
+  type: number
+  // 屏幕宽
   width: number
-  // 屏幕高 未设置取通用配置
+  // 屏幕高
   height: number
   // 隐藏
   hidden: boolean
   // 锁定
   locked: boolean
   // 页面
-  pages: []
+  pages: Page[]
+  // 元信息
+  meta: ScreenConfig
 }

+ 307 - 300
src/renderer/src/views/designer/modals/projectModal/index.vue

@@ -8,137 +8,44 @@
     :close-on-click-modal="false"
     align-center
   >
-    <el-form
-      :model="formData"
-      ref="form"
-      label-width="80px"
-      style="display: flex; flex-direction: column"
-    >
-      <el-form-item label="项目名称">
-        <el-input v-model="formData.name" placeholder="请输入"></el-input>
-      </el-form-item>
-      <el-form-item label="项目路径">
-        <el-input v-model="formData.path" readonly>
-          <template #append>
-            <el-button @click="selectPath"><LuFolder :size="16" /></el-button>
-          </template>
-        </el-input>
-      </el-form-item>
-      <el-form-item label="项目类型">
-        <el-select v-model="formData.type" @change="handlChangeType">
-          <el-option
-            v-for="item in typeOptions"
-            :key="item.value"
-            :label="item.label"
-            :value="item.value"
-          ></el-option>
-        </el-select>
-      </el-form-item>
-      <el-divider />
-      <!-- 芯片配置 -->
-      <template v-if="formData.type === 'chip'">
-        <el-row :gutter="12">
-          <el-col :span="8">
-            <el-form-item label="芯片型号">
-              <el-select v-model="formData.chip.model" @change="handleSelectChip">
-                <el-option
-                  v-for="item in chipOptions"
-                  :key="item"
-                  :label="item"
-                  :value="item"
-                ></el-option>
-              </el-select>
-            </el-form-item>
-          </el-col>
-          <el-col :span="8">
-            <el-form-item label="闪存大小">
-              <el-autocomplete
-                size="small"
-                style="width: 100%"
-                :fetch-suggestions="querySearchFlash"
-                v-model.number="formData.chip.flash_size.capacity"
-              >
-                <template #append>
-                  <el-select v-model="formData.chip.flash_size.unit" style="width: 68px">
-                    <el-option
-                      v-for="item in selectedChipConfig?.flash_size?.unit_options || []"
-                      :key="item"
-                      :label="item"
-                      :value="item"
-                    ></el-option>
-                  </el-select>
-                </template>
-              </el-autocomplete>
-            </el-form-item>
-          </el-col>
-          <el-col :span="8">
-            <el-form-item label="内存大小">
-              <el-autocomplete
-                size="small"
-                style="width: 100%"
-                :fetch-suggestions="querySearchRam"
-                v-model.number="formData.chip.ram_size.capacity"
-              >
-                <template #append>
-                  <el-select v-model="formData.chip.ram_size.unit" style="width: 68px">
-                    <el-option
-                      v-for="item in selectedChipConfig?.ram_size?.unit_options || []"
-                      :key="item"
-                      :label="item"
-                      :value="item"
-                    ></el-option>
-                  </el-select>
-                </template>
-              </el-autocomplete>
-            </el-form-item>
-          </el-col>
-        </el-row>
-        <div class="flex items-center gap-12px">
-          <span class="font-bold">屏幕参数</span>
-          <el-radio-group
-            v-model="formData.screenType"
-            size="small"
-            fill="#6cf"
-            @change="handleChangeScreenTypeByChip"
-          >
-            <el-radio-button label="单屏" value="single" />
-            <el-radio-button label="双屏" value="dual" />
-          </el-radio-group>
-        </div>
-        <div v-for="(item, index) in formData.screens" :key="index">
-          <el-divider border-style="dashed">屏幕{{ index + 1 }}</el-divider>
+    <el-scrollbar wrap-class="pr-12px">
+      <el-form
+        :model="formData"
+        ref="form"
+        label-width="80px"
+        style="display: flex; flex-direction: column"
+        hide-required-asterisk
+      >
+        <el-form-item label="项目名称" prop="name" required>
+          <el-input v-model="formData.name" placeholder="请输入"></el-input>
+        </el-form-item>
+        <el-form-item label="项目路径" prop="path" required>
+          <el-input v-model="formData.path" readonly>
+            <template #append>
+              <el-button @click="selectPath"><LuFolder :size="16" /></el-button>
+            </template>
+          </el-input>
+        </el-form-item>
+        <el-form-item label="项目类型" prop="type" required>
+          <el-select v-model="formData.type" @change="handlChangeType">
+            <el-option
+              v-for="item in typeOptions"
+              :key="item.value"
+              :label="item.label"
+              :value="item.value"
+            ></el-option>
+          </el-select>
+        </el-form-item>
+        <el-divider />
+        <!-- 芯片配置 -->
+        <template v-if="formData.type === 'chip'">
           <el-row :gutter="12">
             <el-col :span="8">
-              <el-form-item label="分辨率">
-                <el-select
-                  :model-value="`${item.width}x${item.height}`"
-                  @change="(val) => handleSetResolution(val, index)"
-                >
-                  <el-option label="自定义" value="custom" />
-                  <el-option
-                    v-for="item in getScreenOptions('resolutions')?.[index] || []"
-                    :label="item"
-                    :value="item"
-                  ></el-option>
-                </el-select>
-              </el-form-item>
-            </el-col>
-            <el-col :span="8">
-              <el-form-item label="接口类型">
-                <el-select v-model="item.interface">
-                  <el-option
-                    v-for="item in getScreenOptions('interface')?.[index] || []"
-                    :label="item"
-                    :value="item"
-                  ></el-option>
-                </el-select>
-              </el-form-item>
-            </el-col>
-            <el-col :span="8">
-              <el-form-item label="颜色格式">
-                <el-select v-model="item.colorFormat">
+              <el-form-item label="芯片型号" prop="chip.model" required>
+                <el-select v-model="formData.chip.model" @change="handleSelectChip">
                   <el-option
-                    v-for="item in getScreenOptions('color_format')?.[index] || []"
+                    v-for="item in chipOptions"
+                    :key="item"
                     :label="item"
                     :value="item"
                   ></el-option>
@@ -146,188 +53,286 @@
               </el-form-item>
             </el-col>
             <el-col :span="8">
-              <el-form-item label="颜色深度">
-                <el-select v-model="item.colorDepth">
-                  <el-option
-                    v-for="item in getScreenOptions('color_depth')?.[index] || []"
-                    :label="item"
-                    :value="item"
-                  ></el-option>
-                </el-select>
-              </el-form-item>
-            </el-col>
-            <el-col :span="8">
-              <el-form-item label="PCLK">
-                <el-input v-model="item.params.PCLK" />
-              </el-form-item>
-            </el-col>
-            <el-col :span="8">
-              <el-form-item label="VBP">
-                <el-input v-model="item.params.VBP" />
-              </el-form-item>
-            </el-col>
-            <el-col :span="8">
-              <el-form-item label="VFP">
-                <el-input v-model="item.params.VFP" />
-              </el-form-item>
-            </el-col>
-            <el-col :span="8">
-              <el-form-item label="HFP">
-                <el-input v-model="item.params.HFP" />
-              </el-form-item>
-            </el-col>
-            <el-col :span="8">
-              <el-form-item label="HSYNC">
-                <el-input v-model="item.params.HSYNC" />
-              </el-form-item>
-            </el-col>
-            <el-col :span="8">
-              <el-form-item label="VSYNC">
-                <el-input v-model="item.params.VSYNC" />
-              </el-form-item>
-            </el-col>
-            <el-col :span="8">
-              <el-form-item label="HsyncWidth">
-                <el-input v-model="item.params.HsyncWidth" />
+              <el-form-item label="闪存大小" prop="chip.flash_size.capacity" required>
+                <el-autocomplete
+                  size="small"
+                  style="width: 100%"
+                  :fetch-suggestions="querySearchFlash"
+                  v-model.number="formData.chip.flash_size.capacity"
+                >
+                  <template #append>
+                    <el-select v-model="formData.chip.flash_size.unit" style="width: 68px">
+                      <el-option
+                        v-for="item in selectedChipConfig?.flash_size?.unit_options || []"
+                        :key="item"
+                        :label="item"
+                        :value="item"
+                      ></el-option>
+                    </el-select>
+                  </template>
+                </el-autocomplete>
               </el-form-item>
             </el-col>
             <el-col :span="8">
-              <el-form-item label="VsyncWidth">
-                <el-input v-model="item.params.VsyncWidth" />
+              <el-form-item label="内存大小" prop="chip.ram_size.capacity" required>
+                <el-autocomplete
+                  size="small"
+                  style="width: 100%"
+                  :fetch-suggestions="querySearchRam"
+                  v-model.number="formData.chip.ram_size.capacity"
+                >
+                  <template #append>
+                    <el-select v-model="formData.chip.ram_size.unit" style="width: 68px">
+                      <el-option
+                        v-for="item in selectedChipConfig?.ram_size?.unit_options || []"
+                        :key="item"
+                        :label="item"
+                        :value="item"
+                      ></el-option>
+                    </el-select>
+                  </template>
+                </el-autocomplete>
               </el-form-item>
             </el-col>
           </el-row>
-        </div>
-      </template>
-      <!-- 板卡配置 -->
-      <template v-if="formData.type === 'board'">
-        <div class="flex items-center justify-end gap-12px mb-12px">
-          <el-input style="width: 160px" placeholder="请输入型号查询" v-model="query" />
-          <el-button @click="searchBoard">搜索</el-button>
-        </div>
-        <div class="max-h-300px overflow-y-auto overflow-x-hidden">
-          <el-row :gutter="12">
-            <el-col v-for="item in boardOptions || []" :span="12">
-              <div
-                class="w-full h-100px bg-#fff relative cursor-pointer border1 border-transparent border-solid"
-                :class="formData.board.model === item.board_name ? 'border-#0ff!' : ''"
-                @click="handleSetBoard(item)"
-              >
-                <img :src="item.pic_path" />
-                <div class="absolute w-full h-40px left-0 bottom-0 bg-#ccc truncate text-#fff">
-                  <div class="text-center">{{ item.board_name }}</div>
-                  <div class="text-center">{{ item.description }}</div>
+          <div class="flex items-center justify-center gap-12px">
+            <el-radio-group
+              v-model="formData.screenType"
+              size="small"
+              fill="#6cf"
+              @change="handleChangeScreenTypeByChip"
+            >
+              <el-radio-button label="单屏" value="single" />
+              <el-radio-button label="双屏" value="dual" />
+            </el-radio-group>
+          </div>
+          <div v-for="(item, index) in formData.screens" :key="index">
+            <el-divider border-style="dashed">屏幕{{ index + 1 }}</el-divider>
+            <el-row :gutter="12">
+              <el-col :span="8">
+                <el-form-item label="分辨率">
+                  <el-select
+                    :model-value="`${item.width}x${item.height}`"
+                    @change="(val) => handleSetResolution(val, index)"
+                  >
+                    <el-option label="自定义" value="custom" />
+                    <el-option
+                      v-for="item in getScreenOptions('resolutions')?.[index] || []"
+                      :label="item"
+                      :value="item"
+                    ></el-option>
+                  </el-select>
+                </el-form-item>
+              </el-col>
+              <el-col :span="8">
+                <el-form-item label="接口类型">
+                  <el-select v-model="item.interface" prop="interface" required>
+                    <el-option
+                      v-for="item in getScreenOptions('interface')?.[index] || []"
+                      :label="item"
+                      :value="item"
+                    ></el-option>
+                  </el-select>
+                </el-form-item>
+              </el-col>
+              <el-col :span="8">
+                <el-form-item label="颜色格式">
+                  <el-select v-model="item.colorFormat">
+                    <el-option
+                      v-for="item in getScreenOptions('color_format')?.[index] || []"
+                      :label="item"
+                      :value="item"
+                    ></el-option>
+                  </el-select>
+                </el-form-item>
+              </el-col>
+              <el-col :span="8">
+                <el-form-item label="颜色深度">
+                  <el-select v-model="item.colorDepth">
+                    <el-option
+                      v-for="item in getScreenOptions('color_depth')?.[index] || []"
+                      :label="item"
+                      :value="item"
+                    ></el-option>
+                  </el-select>
+                </el-form-item>
+              </el-col>
+              <el-col :span="8">
+                <el-form-item label="PCLK">
+                  <el-input v-model="item.params.PCLK" />
+                </el-form-item>
+              </el-col>
+              <el-col :span="8">
+                <el-form-item label="VBP">
+                  <el-input v-model="item.params.VBP" />
+                </el-form-item>
+              </el-col>
+              <el-col :span="8">
+                <el-form-item label="VFP">
+                  <el-input v-model="item.params.VFP" />
+                </el-form-item>
+              </el-col>
+              <el-col :span="8">
+                <el-form-item label="HFP">
+                  <el-input v-model="item.params.HFP" />
+                </el-form-item>
+              </el-col>
+              <el-col :span="8">
+                <el-form-item label="HSYNC">
+                  <el-input v-model="item.params.HSYNC" />
+                </el-form-item>
+              </el-col>
+              <el-col :span="8">
+                <el-form-item label="VSYNC">
+                  <el-input v-model="item.params.VSYNC" />
+                </el-form-item>
+              </el-col>
+              <el-col :span="8">
+                <el-form-item label="HsyncWidth">
+                  <el-input v-model="item.params.HsyncWidth" />
+                </el-form-item>
+              </el-col>
+              <el-col :span="8">
+                <el-form-item label="VsyncWidth">
+                  <el-input v-model="item.params.VsyncWidth" />
+                </el-form-item>
+              </el-col>
+            </el-row>
+          </div>
+        </template>
+        <!-- 板卡配置 -->
+        <template v-if="formData.type === 'board'">
+          <div class="flex items-center justify-end gap-12px mb-12px">
+            <el-input style="width: 160px" placeholder="请输入型号查询" v-model="query" />
+            <el-button @click="searchBoard">搜索</el-button>
+          </div>
+          <div class="max-h-300px overflow-y-auto overflow-x-hidden">
+            <el-row :gutter="12">
+              <el-col v-for="item in boardOptions || []" :span="12">
+                <div
+                  class="w-full h-100px bg-#fff relative cursor-pointer border1 border-transparent border-solid"
+                  :class="formData.board.model === item.board_name ? 'border-#0ff!' : ''"
+                  @click="handleSetBoard(item)"
+                >
+                  <el-image class="w-full h-full" :src="item.pic_path">
+                    <template #error>
+                      <div class="text-center text-#fff">图片加载失败</div>
+                    </template>
+                  </el-image>
+                  <div class="absolute w-full h-40px left-0 bottom-0 bg-#ccc truncate text-#fff">
+                    <div class="text-center">{{ item.board_name }}</div>
+                    <div class="text-center">{{ item.description }}</div>
+                  </div>
                 </div>
-              </div>
-            </el-col>
+              </el-col>
+            </el-row>
+          </div>
+          <div class="my-12px text-text-secondary">当前选择:{{ formData.board.model }}</div>
+          <el-row :gutter="12">
+            <template v-for="(screen, index) in formData.screens">
+              <el-col :span="12">
+                <el-form-item :label="`屏幕${index + 1}分辨率`" label-width="120px">
+                  <el-input :model-value="`${screen.width}x${screen.height}`" disabled />
+                </el-form-item>
+              </el-col>
+              <el-col :span="12">
+                <el-form-item :label="`屏幕${index + 1}颜色深度`" label-width="120px">
+                  <el-select v-model="screen.colorDepth">
+                    <el-option
+                      v-for="item in colorDepthOptions[index] || []"
+                      :label="item"
+                      :value="item"
+                    ></el-option>
+                  </el-select>
+                </el-form-item>
+              </el-col>
+            </template>
           </el-row>
-        </div>
-        <div class="my-12px text-text-secondary">当前选择:{{ formData.board.model }}</div>
+        </template>
+        <!-- 虚拟显示 -->
+        <template v-if="formData.type === 'analog_display'">
+          <div class="flex items-center justify-center gap-12px">
+            <el-radio-group
+              v-model="formData.screenType"
+              size="small"
+              fill="#6cf"
+              @change="handleChangeScreenTypeByAnalog"
+            >
+              <el-radio-button label="单屏" value="single" />
+              <el-radio-button label="双屏" value="dual" />
+            </el-radio-group>
+          </div>
+          <div v-for="(item, index) in formData.screens" :key="index">
+            <el-divider border-style="dashed">屏幕{{ index + 1 }}</el-divider>
+            <el-row :gutter="12">
+              <el-col :span="8">
+                <el-form-item label="分辨率">
+                  <el-select
+                    :model-value="`${item.width}x${item.height}`"
+                    @change="(val) => handleSetResolution(val, index)"
+                  >
+                    <el-option label="自定义" value="custom" />
+                    <el-option
+                      v-for="item in getAnalogDisplayOptions('resolutions')?.[index] || []"
+                      :label="item"
+                      :value="item"
+                    ></el-option>
+                  </el-select>
+                </el-form-item>
+              </el-col>
+              <el-col :span="8">
+                <el-form-item label="颜色格式">
+                  <el-select v-model="item.colorFormat">
+                    <el-option
+                      v-for="item in getAnalogDisplayOptions('color_format')?.[index] || []"
+                      :label="item"
+                      :value="item"
+                    ></el-option>
+                  </el-select>
+                </el-form-item>
+              </el-col>
+              <el-col :span="8">
+                <el-form-item label="颜色深度">
+                  <el-select v-model="item.colorDepth">
+                    <el-option
+                      v-for="item in getAnalogDisplayOptions('color_depth')?.[index] || []"
+                      :label="item"
+                      :value="item"
+                    ></el-option>
+                  </el-select>
+                </el-form-item>
+              </el-col>
+            </el-row>
+          </div>
+        </template>
+        <el-divider />
         <el-row :gutter="12">
-          <template v-for="(screen, index) in formData.screens">
-            <el-col :span="12">
-              <el-form-item :label="`屏幕${index + 1}分辨率`" label-width="120px">
-                <el-input :model-value="`${screen.width}x${screen.height}`" disabled />
-              </el-form-item>
-            </el-col>
-            <el-col :span="12">
-              <el-form-item :label="`屏幕${index + 1}颜色深度`" label-width="120px">
-                <el-select v-model="screen.colorDepth">
-                  <el-option
-                    v-for="item in colorDepthOptions[index] || []"
-                    :label="item"
-                    :value="item"
-                  ></el-option>
-                </el-select>
-              </el-form-item>
-            </el-col>
-          </template>
+          <el-col :span="8">
+            <el-form-item label="版本号">
+              <el-input v-model="formData.version" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item label="打包方式">
+              <el-select v-model="formData.resourcePackaging">
+                <el-option label="C源码" value="c" />
+                <el-option label="C源码+BIN" value="c_bin" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item v-if="formData.resourcePackaging === 'c_bin'" label="BIN数量">
+              <el-input v-model.number="formData.binNum" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="24">
+            <el-form-item label="项目描述">
+              <el-input type="textarea" v-model="formData.description" />
+            </el-form-item>
+          </el-col>
         </el-row>
-      </template>
-      <!-- 虚拟显示 -->
-      <template v-if="formData.type === 'analog_display'">
-        <div class="flex items-center gap-12px">
-          <span class="font-bold">屏幕参数</span>
-          <el-radio-group
-            v-model="formData.screenType"
-            size="small"
-            fill="#6cf"
-            @change="handleChangeScreenTypeByAnalog"
-          >
-            <el-radio-button label="单屏" value="single" />
-            <el-radio-button label="双屏" value="dual" />
-          </el-radio-group>
-        </div>
-        <div v-for="(item, index) in formData.screens" :key="index">
-          <el-divider border-style="dashed">屏幕{{ index + 1 }}</el-divider>
-          <el-row :gutter="12">
-            <el-col :span="8">
-              <el-form-item label="分辨率">
-                <el-select
-                  :model-value="`${item.width}x${item.height}`"
-                  @change="(val) => handleSetResolution(val, index)"
-                >
-                  <el-option label="自定义" value="custom" />
-                  <el-option
-                    v-for="item in getAnalogDisplayOptions('resolutions')?.[index] || []"
-                    :label="item"
-                    :value="item"
-                  ></el-option>
-                </el-select>
-              </el-form-item>
-            </el-col>
-            <el-col :span="8">
-              <el-form-item label="颜色格式">
-                <el-select v-model="item.colorFormat">
-                  <el-option
-                    v-for="item in getAnalogDisplayOptions('color_format')?.[index] || []"
-                    :label="item"
-                    :value="item"
-                  ></el-option>
-                </el-select>
-              </el-form-item>
-            </el-col>
-            <el-col :span="8">
-              <el-form-item label="颜色深度">
-                <el-select v-model="item.colorDepth">
-                  <el-option
-                    v-for="item in getAnalogDisplayOptions('color_depth')?.[index] || []"
-                    :label="item"
-                    :value="item"
-                  ></el-option>
-                </el-select>
-              </el-form-item>
-            </el-col>
-          </el-row>
-        </div>
-      </template>
-      <el-divider />
-      <el-row :gutter="12">
-        <el-col :span="8">
-          <el-form-item label="版本号">
-            <el-input v-model="formData.version" />
-          </el-form-item>
-        </el-col>
-        <el-col :span="8">
-          <el-form-item label="打包方式">
-            <el-select v-model="formData.resourcePackaging">
-              <el-option label="C源码" value="c" />
-              <el-option label="C源码+BIN" value="c_bin" />
-            </el-select>
-          </el-form-item>
-        </el-col>
-        <el-col :span="8">
-          <el-form-item v-if="formData.resourcePackaging === 'c_bin'" label="BIN数量">
-            <el-input v-model.number="formData.binNum" />
-          </el-form-item>
-        </el-col>
-        <el-col :span="24">
-          <el-form-item label="项目描述">
-            <el-input type="textarea" v-model="formData.description" />
-          </el-form-item>
-        </el-col>
-      </el-row>
-    </el-form>
+      </el-form>
+    </el-scrollbar>
 
     <template #footer>
       <el-button @click="showModal = false">取消</el-button>
@@ -481,6 +486,8 @@ const handleSetResolution = (str: string, index: number) => {
 
 // 设置自定义分辨率
 const handleSetScreen = () => {
+  if (!customScreen.value.width || !customScreen.value.height) return
+
   formData.screens[customScreen.value.index].width = customScreen.value.width
   formData.screens[customScreen.value.index].height = customScreen.value.height
   showScreenModal.value = false

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

@@ -1,7 +1,7 @@
 <template>
   <SplitterCollapse>
     <SplitterCollapseItem title="基础">
-      <el-space wrap>
+      <el-space alignment="center" wrap>
         <div
           class="w-70px h-70px flex items-center text-text-secondary justify-center cursor-pointer hover:bg-bg-secondary hover:text-text-active"
           v-for="item in 10"

+ 10 - 4
src/renderer/src/views/designer/workspace/index.vue

@@ -2,11 +2,14 @@
   <div class="w-full h-full">
     <SplitterGroup direction="vertical">
       <SplitterPanel>
-        <Stage key="1" />
+        <Stage key="1" :data="projectStore.project?.screens[0]" />
       </SplitterPanel>
-      <SplitterResizeHandle class="h-2px bg-border" />
-      <SplitterPanel>
-        <Stage key="2" />
+      <SplitterResizeHandle
+        class="h-2px bg-border"
+        v-if="projectStore.project?.meta.screenType === 'dual'"
+      />
+      <SplitterPanel v-if="projectStore.project?.meta.screenType === 'dual'">
+        <Stage key="2" :data="projectStore.project?.screens[0]" />
       </SplitterPanel>
     </SplitterGroup>
   </div>
@@ -16,6 +19,9 @@
 import { ref, onMounted } from 'vue'
 import Stage from './stage/index.vue'
 import { SplitterGroup, SplitterPanel, SplitterResizeHandle } from 'reka-ui'
+import { useProjectStore } from '@/store/modules/project'
+
+const projectStore = useProjectStore()
 
 const content = ref('')
 onMounted(() => {

+ 5 - 1
src/renderer/src/views/designer/workspace/stage/DesignerCanvas.vue

@@ -192,7 +192,9 @@ const handleDrop = (e: DragEvent) => {
 }
 
 defineExpose({
-  getPosition: () => canvasRef.value?.getBoundingClientRect()
+  getPosition: () => canvasRef.value?.getBoundingClientRect(),
+  initScale,
+  initStagePosition
 })
 
 onMounted(() => {
@@ -201,9 +203,11 @@ onMounted(() => {
     initStagePosition()
   }, 100)
   window.addEventListener('resize', initScale)
+  window.addEventListener('resize', initStagePosition)
 })
 onBeforeUnmount(() => {
   window.removeEventListener('resize', initScale)
+  window.removeEventListener('resize', initStagePosition)
 })
 </script>
 

+ 22 - 5
src/renderer/src/views/designer/workspace/stage/index.vue

@@ -7,8 +7,8 @@
     @mouseup="handleMouseUp"
   >
     <div class="workspace flex flex-col">
-      <div class="h-24px bg-bg-secondary">
-        <div class="px-12px leading-24px text-14px">屏幕</div>
+      <div class="h-32px bg-bg-secondary stage-title">
+        <div class="px-12px leading-32px text-text-primary font-bold">屏幕</div>
       </div>
       <div class="workspace-top">
         <!-- 画布 -->
@@ -65,15 +65,21 @@
 
 <script setup lang="ts">
 import type { StageState } from './type'
+import type { Screen } from '@/types/screen'
 
-import { ref, reactive } from 'vue'
-
-import { LuMinusCircle, LuPlusCircle } from 'vue-icons-plus/lu'
+import { ref, reactive, defineProps, watch } from 'vue'
 import Scaleplate from './Scaleplate.vue'
 import DesignerCanvas from './DesignerCanvas.vue'
 import { throttle } from 'lodash'
 import { LuGrid3X3, LuRuler, LuBoxSelect } from 'vue-icons-plus/lu'
+import { useProjectStore } from '@/store/modules/project'
+import { useAppStore } from '@/store/modules/app'
 
+const props = defineProps<{
+  data: Screen
+}>()
+const projectStore = useProjectStore()
+const appStore = useAppStore()
 const canvasRef = ref<{ getPosition: HTMLElement['getBoundingClientRect'] } | null>()
 const state = reactive<StageState>({
   scale: 1,
@@ -94,6 +100,16 @@ const state = reactive<StageState>({
   showBgGrid: false
 })
 
+watch(
+  () => props.data,
+  (val) => {
+    if (val) {
+      state.width = val.width
+      state.height = val.height
+    }
+  }
+)
+
 // 修改状态
 const handleSetState = (newState: Partial<StageState>) => {
   Object.entries(newState).forEach(([key, value]) => {
@@ -123,6 +139,7 @@ let workspaceTop = 0
 const handleMouseDown = (e: MouseEvent) => {
   const target = e?.target as Element | null
   if (
+    target?.closest('.stage-title') ||
     target?.closest('.edit-box') ||
     target?.closest('.component-content') ||
     target?.closest('.component-wrapper') ||