Просмотр исходного кода

feat: 完善编辑项目功能

Co-authored-by: Copilot <copilot@github.com>
jiaxing.liao недель назад: 4
Родитель
Сommit
cc1b3c4d37

+ 2 - 0
.npmrc

@@ -1,3 +1,5 @@
 electron_mirror=https://npmmirror.com/mirrors/electron/
 electron_builder_binaries_mirror=https://npmmirror.com/mirrors/electron-builder-binaries/
+public-hoist-pattern[]=*esbuild*
 shamefully-hoist=true
+ignore-scripts=false

+ 2 - 0
package.json

@@ -23,12 +23,14 @@
   "dependencies": {
     "@electron-toolkit/preload": "^3.0.2",
     "@electron-toolkit/utils": "^4.0.0",
+    "@element-plus/icons-vue": "^2.3.2",
     "@lottiefiles/dotlottie-vue": "^0.9.5",
     "@types/fs-extra": "^11.0.4",
     "@vueuse/components": "^14.0.0",
     "@vueuse/core": "^14.0.0",
     "@zumer/snapdom": "^2.0.2",
     "barcode": "^0.1.0",
+    "dayjs": "^1.11.20",
     "deep-diff": "^1.0.2",
     "element-plus": "^2.11.4",
     "fs-extra": "^11.3.2",

Разница между файлами не показана из-за своего большого размера
+ 2950 - 2350
pnpm-lock.yaml


+ 4 - 0
pnpm-workspace.yaml

@@ -0,0 +1,4 @@
+allowBuilds:
+  electron: true
+  esbuild: true
+  vue-demi: true

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

@@ -175,5 +175,8 @@
   "globalVariables": "Global Variables",
   "variableName": "Variable Name",
   "variableType": "Variable Type",
-  "initialValue": "Initial Value"
+  "initialValue": "Initial Value",
+  "switchToSingleScreenWarning": "Switching to single screen will delete all pages and widgets on the second screen. This action cannot be undone. Continue?",
+  "warning": "Warning",
+  "editSuccess": "Edit Success"
 }

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

@@ -174,5 +174,8 @@
   "globalVariables": "全局变量",
   "variableName": "变量名",
   "variableType": "变量类型",
-  "initialValue": "初始值"
+  "initialValue": "初始值",
+  "switchToSingleScreenWarning": "切换到单屏幕将删除第二屏幕的所有页面和组件,此操作不可撤销,是否继续?",
+  "warning": "警告",
+  "editSuccess": "编辑成功"
 }

+ 75 - 0
src/renderer/src/store/modules/project.ts

@@ -457,6 +457,80 @@ export const useProjectStore = defineStore('project', () => {
     window.electron.ipcRenderer.invoke('lock-project-folder', path)
   }
 
+  /**
+   * 编辑项目
+   * 1、如果元信息屏幕增加了创建一个屏幕
+   * 2、如果元信息屏幕减少删除第二屏幕
+   * 3、屏幕分辨率、颜色格式、颜色深度、方向数据变化了需要同步到screens对应的屏幕中
+   */
+  const editProject = async (newMeta: AppMeta) => {
+    if (!project.value) return
+
+    const meta = klona(newMeta)
+    meta.modifyTime = dayjs().format('YYYY-MM-DD HH:mm:ss')
+
+    const oldScreenCount = project.value.meta.screens.length
+    const newScreenCount = meta.screens.length
+
+    project.value.meta = klona(meta)
+    imageCompressFormat.value = meta.imageCompress
+
+    if (newScreenCount > oldScreenCount) {
+      for (let i = oldScreenCount; i < newScreenCount; i += 1) {
+        const newScreen = createScreen(meta.screens[i])
+        newScreen.name = `screen_${i + 1}`
+        project.value.screens.push(newScreen)
+        openPageIds.value[i] = newScreen.pages[0].id
+      }
+    } else if (newScreenCount < oldScreenCount) {
+      project.value.screens.splice(newScreenCount)
+      openPageIds.value = openPageIds.value.slice(0, newScreenCount)
+    }
+
+    meta.screens.forEach((screenMeta, index) => {
+      const screen = project.value?.screens[index]
+      if (!screen) return
+
+      screen.width = screenMeta.width
+      screen.height = screenMeta.height
+      screen.colorFormat = screenMeta.colorFormat
+      screen.colorDepth = screenMeta.colorDepth
+      screen.screenDirection = screenMeta.screenDirection
+      screen.name = `screen_${index + 1}`
+
+      if (!screen.pages.length) {
+        screen.pages.push(createScreen(screenMeta).pages[0])
+      }
+
+      if (!openPageIds.value[index] || !screen.pages.some((page) => page.id === openPageIds.value[index])) {
+        openPageIds.value[index] = screen.pages[0].id
+      }
+    })
+
+    if (!project.value.screens.some((screen) => screen.pages.some((page) => page.id === activePageId.value))) {
+      activePageId.value = project.value.screens[0]?.pages[0]?.id
+      activeWidgets.value = []
+    }
+
+    if (recentProjectStore.currentRecord) {
+      const currentProjectPath = `${project.value.meta.path}\\${projectDirectory.value || project.value.meta.name}`
+      const nextRecord = {
+        ...recentProjectStore.currentRecord,
+        projectName: project.value.meta.name,
+        projectPath: currentProjectPath,
+        modifyTime: meta.modifyTime
+      }
+      recentProjectStore.currentRecord = nextRecord
+      recentProjectStore.updateProject(nextRecord)
+    }
+
+    await window.electron.ipcRenderer.invoke(
+      'write-file',
+      `${projectPath.value}\\project.ui`,
+      JSON.stringify(project.value, null, 2)
+    )
+  }
+
   // 保存当前项目
   const saveProject = async () => {
     if (!projectPath.value) {
@@ -576,6 +650,7 @@ export const useProjectStore = defineStore('project', () => {
     normalizeThemes,
     removeThemeStyles,
     renameThemeStyles,
+    editProject,
 
     // 历史记录
     history,

+ 119 - 59
src/renderer/src/views/designer/modals/projectModal/index.vue

@@ -23,11 +23,12 @@
             <el-input
               v-model="formData.name"
               :placeholder="$t('pleaseEnter')"
+              :disabled="mode === 'edit'"
               spellcheck="false"
             ></el-input>
           </el-form-item>
           <el-form-item :label="$t('projectPath')" prop="path">
-            <el-input spellcheck="false" v-model="formData.path">
+            <el-input spellcheck="false" v-model="formData.path" :disabled="mode === 'edit'">
               <template #append>
                 <el-button @click="selectPath('path')" :disabled="mode === 'edit'"
                   ><LuFolder :size="16" :disabled="mode === 'edit'"
@@ -36,16 +37,16 @@
             </el-input>
           </el-form-item>
           <el-form-item :label="$t('codePath')" prop="codePath">
-            <el-input spellcheck="false" v-model="formData.codePath">
+            <el-input spellcheck="false" v-model="formData.codePath" :disabled="mode === 'edit'">
               <template #append>
-                <el-button @click="selectPath('codePath')"
+                <el-button @click="selectPath('codePath')" :disabled="mode === 'edit'"
                   ><LuFolder :size="16" :disabled="mode === 'edit'"
                 /></el-button>
               </template>
             </el-input>
           </el-form-item>
           <el-form-item :label="$t('projectType')" prop="type">
-            <el-select v-model="formData.type" @change="handlChangeType">
+            <el-select v-model="formData.type" @change="handlChangeType" :disabled="mode === 'edit'">
               <el-option
                 v-for="item in typeOptions"
                 :key="item.value"
@@ -55,7 +56,7 @@
             </el-select>
           </el-form-item>
           <el-divider />
-          <!-- 鑺墖閰嶇疆 -->
+          <!-- 芯片配置 -->
           <template v-if="formData.type === 'chip'">
             <el-row :gutter="12">
               <el-col :span="8">
@@ -119,6 +120,7 @@
                 size="small"
                 fill="#6cf"
                 @change="handleChangeScreenTypeByChip"
+                
               >
                 <el-radio-button :label="$t('singleScreen')" value="single" />
                 <el-radio-button :label="$t('doubleScreen')" value="dual" />
@@ -275,7 +277,7 @@
               </el-row>
             </div>
           </template>
-          <!-- 鏉垮崱閰嶇疆 -->
+          <!-- 板卡配置 -->
           <template v-if="formData.type === 'board'">
             <div class="flex items-center justify-end gap-12px mb-12px">
               <el-input
@@ -354,7 +356,7 @@
               </template>
             </el-row>
           </template>
-          <!-- 铏氭嫙鏄剧ず -->
+          <!-- 虚拟显示 -->
           <template v-if="formData.type === 'analog_display'">
             <div class="flex items-center justify-center gap-12px">
               <el-radio-group
@@ -481,7 +483,7 @@
     <template #footer>
       <el-button @click="close()">{{ $t('cancel') }}</el-button>
       <el-button type="primary" @click="mode === 'add' ? handleSubmit() : handleEdit()">{{
-        $t('create')
+        mode === 'add' ? $t('create') : $t('edit')
       }}</el-button>
     </template>
 
@@ -517,7 +519,7 @@
 
 <script setup lang="ts">
 import type { AppMeta } from '@/types/appMeta'
-import { ElMessage, type FormInstance } from 'element-plus'
+import { ElMessage, ElMessageBox, type FormInstance } from 'element-plus'
 import { computed, reactive, ref, nextTick, watch } from 'vue'
 import { LuFolder } from 'vue-icons-plus/lu'
 import { useProjectStore } from '@/store/modules/project'
@@ -565,7 +567,7 @@ const formData = reactive<
       capacity: '16',
       unit: 'M'
     },
-    // 鍐呭瓨澶у皬
+    // 内存大小
     ram_size: {
       capacity: '4',
       unit: 'M'
@@ -609,7 +611,7 @@ const formData = reactive<
 })
 
 /**
- * 璁剧疆榛樿椤圭洰鍚?
+ * 设置默认项目名
  */
 const setProjectDefaultName = () => {
   const index = getNextIndex(recentProject.recentProjects || [], 'projectName')
@@ -621,17 +623,18 @@ watch(() => recentProject.recentProjects, setProjectDefaultName, { immediate: tr
 
 const form = ref<FormInstance>()
 const resolutionFormRef = ref<FormInstance>()
-// 鏄剧ず妯℃€佹
+// 显示模态框
 const showModal = ref(!props.initHide)
-// 鏄剧ず鑷畾涔夊睆骞曟ā鎬佹
+// 显示自定义屏幕模态框
 const showScreenModal = ref(false)
-// 鑷畾涔夊睆骞?
+const hasConfirmedSingleScreenRisk = ref(false)
+// 自定义屏幕
 const customScreen = ref({
   width: 0,
   height: 0,
   index: 0
 })
-// 椤圭洰绫诲瀷閫夐」
+// 项目类型选项
 const typeOptions = computed(() => {
   return (
     appStore.lang && [
@@ -688,7 +691,7 @@ const rules = computed(() => {
   }
 })
 
-// 鍒囨崲椤圭洰绫诲瀷
+// 切换项目类型
 const handlChangeType = (type: string) => {
   if (type === 'analog_display') {
     formData.imageCompress = getImageCompress(displayConfig.simulation_display.image_compression)
@@ -700,7 +703,7 @@ const handlChangeType = (type: string) => {
   handleChangeScreenTypeByAnalog(formData.screenType)
 }
 
-// 閫夋嫨鏂囦欢澶硅矾寰?
+// 选择文件夹路径
 const selectPath = async (key: string) => {
   const path = await window.electron.ipcRenderer.invoke('get-directory')
 
@@ -709,7 +712,7 @@ const selectPath = async (key: string) => {
   }
 }
 
-// 閫夋嫨鍒嗚鲸鐜?
+// 选择分辨率
 const handleSetResolution = (str: string, index: number) => {
   const arr = str.split('x')
   if (arr.length === 2) {
@@ -726,7 +729,7 @@ const handleSetResolution = (str: string, index: number) => {
   }
 }
 
-// 璁剧疆鑷畾涔夊垎杈ㄧ巼
+// 设置自定义分辨率
 const handleSetScreen = () => {
   resolutionFormRef.value?.validate()
   if (!customScreen.value.width || !customScreen.value.height) return
@@ -737,16 +740,16 @@ const handleSetScreen = () => {
   handleSetScreenParams(customScreen.value.index)
 }
 
-// 鑾峰彇鍥剧墖鍘嬬缉鏂瑰紡
+// 获取图片压缩方式
 const getImageCompress = (params: Record<string, boolean>) => {
   return Object.keys(params || {}).filter((key) => params[key])
 }
-/******************************鑺墖鐩稿叧*******************************/
-// 鑺墖閫夐」
+/******************************芯片相关*******************************/
+// 芯片选项
 const chipOptions = Object.keys(chipConfig?.chips || {})
-// 宸查€夎姱鐗囬厤缃?
+// 已选芯片配置
 const selectedChipConfig = computed(() => chipConfig?.chips?.[formData.chip.model])
-// 灞忓箷鎺ュ彛閫夐」
+// 屏幕接口选项
 const getScreenOptions = (type: string) => {
   if (formData.screenType === 'single') {
     return [selectedChipConfig.value?.single_screen?.[type].options]
@@ -758,11 +761,11 @@ const getScreenOptions = (type: string) => {
   }
 }
 
-// 閫夋嫨鑺墖
+// 选择芯片
 const handleSelectChip = () => {
-  // 璁剧疆榛樿鍊?
+  // 设置默认值
 
-  // 鍥剧墖鍘嬬缉鏂瑰紡
+  // 图片压缩方式
   formData.imageCompress = getImageCompress(selectedChipConfig.value?.image_compression)
   formData.videoFormats = selectedChipConfig.value?.video_formats
 
@@ -774,18 +777,18 @@ const handleSelectChip = () => {
   handleChangeScreenTypeByChip(formData.screenType)
 }
 
-// 闂瓨閫夐」
+// 闪存选项
 const querySearchFlash = (_query: string, cb: (data: any) => void) => {
   const list = selectedChipConfig.value?.flash_size?.capacity_options || []
   cb(list.map((item) => ({ value: item })))
 }
 
-// 鍐呭瓨閫夐」
+// 内存选项
 const querySearchRam = (_query: string, cb: (data: any) => void) => {
   const list = selectedChipConfig.value?.ram_size?.capacity_options || []
   cb(list.map((item) => ({ value: item })))
 }
-// 璁剧疆灞忓箷鍙傛暟
+// 设置屏幕参数
 const handleSetScreenParams = (index: number) => {
   const params =
     selectedChipConfig.value?.resolution_presets?.[
@@ -799,13 +802,19 @@ const handleSetScreenParams = (index: number) => {
   }
 }
 
-// 閫夋嫨鑺墖灞忓箷绫诲瀷
-const handleChangeScreenTypeByChip = (type: any) => {
+// 选择芯片屏幕类型
+const handleChangeScreenTypeByChip = async (type: any) => {
+  const canChange = await handleChangeScreenType(type);
+  if (!canChange) {
+    formData.screenType = type === 'single' ? 'dual' : 'single'
+    return
+  };
+
   let screens =
     type === 'single'
       ? selectedChipConfig.value?.single_screen
       : selectedChipConfig.value?.dual_screen
-  // 鏋勫缓鏁扮粍
+  // 构建数组
   if (screens && type === 'single') {
     screens = [screens]
   }
@@ -818,21 +827,21 @@ const handleChangeScreenTypeByChip = (type: any) => {
   screens?.forEach((screen, index) => {
     const resolution = selectedChipConfig.value?.resolution_presets[screen?.resolutions.default]
     formData.screens.push({
-      // 灞忓箷绫诲瀷
+      // 屏幕类型
       type: index + 1,
-      // 鎺ュ彛绫诲瀷
+      // 接口类型
       interface: screen?.interface?.default,
-      // 灞忓箷鏂瑰悜
+      // 屏幕方向
       screenDirection: screen?.screen_direction?.default,
-      // 灞忓箷瀹?
+      // 屏幕宽
       width: resolution?.width,
-      // 灞忓箷楂?
+      // 屏幕高
       height: resolution?.height,
-      // 棰滆壊娣卞害
+      // 颜色深度
       colorDepth: screen?.color_depth?.default,
-      // 棰滆壊鏍煎紡
+      // 颜色格式
       colorFormat: screen?.color_format?.default,
-      // 灞忓箷鍙傛暟
+      // 屏幕参数
       params: {
         PCLK: resolution?.PCLK,
         HFP: resolution?.HFP,
@@ -848,18 +857,18 @@ const handleChangeScreenTypeByChip = (type: any) => {
   })
 }
 
-/******************************鏉垮崱鐩稿叧*******************************/
-// 鏌ヨ
+/******************************板卡相关*******************************/
+// 查询
 const query = ref('')
-// 鏉垮崱鍒楄〃
+// 板卡列表
 const boardOptions = ref(boardConfig.boards || [])
-// 棰滆壊娣卞害閫夐」
+// 颜色深度选项
 const colorDepthOptions = ref<string[][]>([])
-// 鎼滅储鏉垮崱
+// 搜索板卡
 const searchBoard = () => {
   boardOptions.value = boardConfig.boards.filter((item) => item.board_name.includes(query.value))
 }
-// 鑾峰彇鏉垮崱灞忓箷閫夐」
+// 获取板卡屏幕选项
 const getBoardOptions = (type: string) => {
   if (formData.screenType === 'single') {
     return [
@@ -876,15 +885,18 @@ const getBoardOptions = (type: string) => {
     return [options1, options2]
   }
 }
-// 閫夋嫨鏉垮崱
-const handleSetBoard = (board: any) => {
+// 选择板卡
+const handleSetBoard = async (board: any) => {
+  const canChange = await handleChangeScreenType(board.screen_mode)
+  if (!canChange) return
+
   formData.board.model = board.board_name
   formData.imageCompress = getImageCompress(board.image_compression)
   formData.videoFormats = board.video_formats
   formData.screenType = board.screen_mode
   handleChangeScreenTypeByBoard(board)
 }
-// 璁剧疆鏉垮崱鏄剧ず灞忓箷绫诲瀷
+// 设置板卡显示屏幕类型
 const handleChangeScreenTypeByBoard = (board: any) => {
   const screens =
     board.screen_mode === 'single'
@@ -929,8 +941,8 @@ const handleChangeScreenTypeByBoard = (board: any) => {
   })
 }
 
-/******************************铏氭嫙鏄剧ず鐩稿叧*******************************/
-// 鑾峰彇铏氭嫙鏄剧ず灞忓箷閫夐」
+/******************************虚拟显示相关*******************************/
+// 获取虚拟显示屏幕选项
 const getAnalogDisplayOptions = (type: string) => {
   if (formData.screenType === 'single') {
     return [displayConfig.simulation_display.single_screen[type].options]
@@ -942,8 +954,14 @@ const getAnalogDisplayOptions = (type: string) => {
   }
 }
 
-// 閫夋嫨铏氭嫙鏄剧ず灞忓箷绫诲瀷
-const handleChangeScreenTypeByAnalog = (type: any) => {
+// 选择虚拟显示屏幕类型
+const handleChangeScreenTypeByAnalog = async (type: any) => {
+  const canChange = await handleChangeScreenType(type)
+  if (!canChange) {
+    formData.screenType = type === 'single' ? 'dual' : 'single'
+    return
+  }
+
   const screens =
     type === 'single'
       ? [displayConfig.simulation_display.single_screen]
@@ -981,14 +999,14 @@ const handleChangeScreenTypeByAnalog = (type: any) => {
   })
 }
 
-// 鍒涘缓椤圭洰
+// 创建项目
 const handleSubmit = async () => {
   await form.value?.validate()
   if (formData.type === 'board' && !formData.board.model) {
     ElMessage.warning(t('selectBoard'))
     return
   }
-  // 璁颁綇椤圭洰璺緞
+  // 记住项目路径
   setProjectPath(formData.path)
   setCodePath(formData.codePath)
 
@@ -997,12 +1015,50 @@ const handleSubmit = async () => {
   showModal.value = false
 }
 
-// 缂栬緫椤圭洰
+/**
+ * 切换单双屏幕处理
+ * ⚠️ 如果是编辑项目,双屏切单屏需要提示数据丢失风险
+ * @param type 'single' | 'dual'
+ * @returns Promise<boolean> true 表示允许切换,false 表示取消
+ */
+const handleChangeScreenType = async (type: string): Promise<boolean> => {
+  const isEditMode = mode.value === 'edit'
+  const wasDualScreen = projectStore.project?.meta.screenType === 'dual'
+  const willSingleScreen = type === 'single'
+
+  if (hasConfirmedSingleScreenRisk.value && willSingleScreen) return true
+  if (!isEditMode || !wasDualScreen || !willSingleScreen) return true
+
+  try {
+    await ElMessageBox.confirm(t('switchToSingleScreenWarning'), t('warning'), {
+      type: 'warning',
+      confirmButtonText: t('confirm'),
+      cancelButtonText: t('cancel')
+    })
+    hasConfirmedSingleScreenRisk.value = true
+    return true
+  } catch {
+    return false
+  }
+}
+
+// 编辑项目
 const handleEdit = async () => {
   await form.value?.validate()
+  if (formData.type === 'board' && !formData.board.model) {
+    ElMessage.warning(t('selectBoard'))
+    return
+  }
+
+  const canChange = await handleChangeScreenType(formData.screenType)
+  if (!canChange) return
+
+  await projectStore.editProject(klona(formData))
+  ElMessage.success(t('editSuccess'))
+  close()
 }
 
-// 鍏抽棴寮圭獥
+// 关闭弹窗
 const close = (done?: () => void) => {
   // if (!projectStore.project) {
   //   ElMessage.warning(t('createProjectFirst'))
@@ -1010,6 +1066,7 @@ const close = (done?: () => void) => {
   // }
 
   done?.()
+  hasConfirmedSingleScreenRisk.value = false
   form.value?.resetFields()
   showModal.value = false
 }
@@ -1018,6 +1075,7 @@ const close = (done?: () => void) => {
 defineExpose({
   create: async () => {
     mode.value = 'add'
+    hasConfirmedSingleScreenRisk.value = false
     showModal.value = true
     form.value?.resetFields()
     await nextTick()
@@ -1026,10 +1084,12 @@ defineExpose({
     setProjectDefaultName()
   },
   edit: () => {
-    const data = klona(projectStore.project?.meta || {})
+    const data = klona(projectStore.project?.meta || ({} as AppMeta))
     Object.entries(data).forEach(([key, value]) => {
       formData[key] = value
     })
+    formData.screens = klona(data.screens || [])
+    hasConfirmedSingleScreenRisk.value = false
     mode.value = 'edit'
     showModal.value = true
   }