From 4367fd6ed1c84587676e67e91c48e2de1a202831 Mon Sep 17 00:00:00 2001 From: Jasery Date: Tue, 28 Oct 2025 15:00:21 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E6=A8=A1=E6=9D=BF?= =?UTF-8?q?=E5=88=9B=E5=BB=BA=E7=95=8C=E9=9D=A2=EF=BC=8C=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E7=BC=96=E8=BE=91=E6=96=87=E4=BB=B6=E5=92=8C=E7=9B=AE=E5=BD=95?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=EF=BC=8C=E5=A2=9E=E5=BC=BA=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E4=BD=93=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- QWEN.md | 18 +- src/renderer/components/TemplateTree.vue | 26 +- src/renderer/screens/CreateTemplateScreen.vue | 369 ++++++++++++++---- 3 files changed, 327 insertions(+), 86 deletions(-) diff --git a/QWEN.md b/QWEN.md index a3933bd..a28acea 100644 --- a/QWEN.md +++ b/QWEN.md @@ -3,6 +3,7 @@ ## Project Overview Vutron is a preconfigured template for developing cross-platform desktop applications using modern web technologies. The project combines: + - **Vite** as the build tool and development server - **Vue 3** with TypeScript for the user interface - **Electron** for creating desktop applications @@ -18,17 +19,20 @@ The template provides a ready-to-use development environment with hot-reload, cr The project follows the typical Electron architecture with three main processes: ### Main Process (`src/main/`) + - Handles application lifecycle - Manages windows and system integration - Located in `src/main/index.ts` - Also includes utilities, tray functionality, and IPC handlers ### Preload Process (`src/preload/`) + - Acts as an isolated bridge between main and renderer processes - Uses `contextBridge` to securely expose APIs to the renderer - Located in `src/preload/index.ts` ### Renderer Process (`src/renderer/`) + - Contains the Vue 3 application - Uses Vuetify for UI components - Located in `src/renderer/` with: @@ -42,10 +46,12 @@ The project follows the typical Electron architecture with three main processes: ## Building and Running ### Prerequisites + - Node.js >= 20.0.0 - pnpm package manager (recommended) ### Development + ```bash # Install dependencies pnpm install @@ -61,6 +67,7 @@ pnpm dev:debug:force ``` ### Building + ```bash # Build for all platforms pnpm build:all @@ -78,6 +85,7 @@ pnpm build ``` ### Testing + ```bash # Run Playwright tests pnpm test @@ -87,6 +95,7 @@ pnpm test:linux ``` ### Linting and Formatting + ```bash # Check for linting issues pnpm lint @@ -153,7 +162,7 @@ pnpm format:fix ## Development Conventions -1. **Code Style**: +1. **Code Style**: - Uses ESLint with TypeScript and Vue plugin - Formatted with Prettier - Follows Vue 3 Composition API with TypeScript @@ -185,6 +194,7 @@ pnpm format:fix ## Internationalization The application supports multiple languages with the following locale files: + - English (`en`) - Korean (`ko`) - Simplified Chinese (`zh-hans`) @@ -200,6 +210,7 @@ The application supports multiple languages with the following locale files: ## Electron Integration The application integrates with Electron features through: + - IPC communication for sending messages between processes - Native file dialog support - External link opening @@ -213,8 +224,11 @@ The project includes Playwright for end-to-end testing, with configuration in `p ## Build Outputs Building creates different distribution formats based on the platform: + - **macOS**: DMG installer for x64, arm64, and universal architectures - **Windows**: APPX, ZIP, Portable, and NSIS installers (x64) - **Linux**: SNAP, DEB, and RPM packages (x64) -Artifacts are named with the format `${productName} ${version}_${arch}.${ext}` and stored in the `release/${version}` directory. \ No newline at end of file +Artifacts are named with the format `${productName} ${version}_${arch}.${ext}` and stored in the `release/${version}` directory. + +Always respond in 中文 diff --git a/src/renderer/components/TemplateTree.vue b/src/renderer/components/TemplateTree.vue index 189c569..b3527f9 100644 --- a/src/renderer/components/TemplateTree.vue +++ b/src/renderer/components/TemplateTree.vue @@ -61,18 +61,34 @@ class="d-flex align-center ga-1" > mdi-pencil - 编辑内容 + {{ item.type === 'file' ? '编辑文件' : '编辑目录' }} + + + + + mdi-file-plus + + 在目录下创建文件 @@ -192,9 +208,11 @@ const emit = defineEmits([ 'showContextMenu', 'addNewItemToCurrentDirectory', 'editFile', + 'editItem', 'deleteItem', 'addDirectory', - 'addFile' + 'addFile', + 'addFileToDirectory' ]) diff --git a/src/renderer/screens/CreateTemplateScreen.vue b/src/renderer/screens/CreateTemplateScreen.vue index 7133dee..770461a 100644 --- a/src/renderer/screens/CreateTemplateScreen.vue +++ b/src/renderer/screens/CreateTemplateScreen.vue @@ -151,9 +151,11 @@ @showContextMenu="showContextMenu" @add-new-item-to-current-directory="addNewItemToCurrentDirectory" @edit-file="editFile" + @edit-item="editItem" @delete-item="deleteItem" @add-directory="addDirectory" @add-file="addFile" + @add-file-to-directory="addFileToDirectory" /> @@ -164,13 +166,13 @@ max-width="500" > - 添加目录 + {{ currentEditingItem ? '编辑目录' : '添加目录' }} - 取消 + + 取消 + - 添加 + {{ currentEditingItem ? '保存' : '添加' }} @@ -199,13 +205,13 @@ max-width="600" > - 添加文件 + {{ currentEditingItem ? '编辑文件' : '添加文件' }} 选择文件 @@ -225,6 +232,7 @@ @click="selectSourceFolder" variant="outlined" class="mr-2" + :disabled="!!currentEditingItem" > 选择文件夹 @@ -250,17 +258,34 @@ 自动寻找文件夹内的文件 + + + +
+ mdi-information + 正在编辑现有文件路径,文件源不会改变 +
+
- 取消 + + 取消 + - 添加 + {{ currentEditingItem ? '保存' : '添加' }}
@@ -570,6 +595,15 @@ const addFile = (parentItem: TreeItem | null = null) => { autoFocusFileInput() } +// Add a file directly to a specific directory +const addFileToDirectory = (directoryItem: TreeItem) => { + showContextMenuVar.value = false + parentDirectoryForNewItems.value = directoryItem.name // Store the directory as parent + newFilePath.value = '' // Don't pre-fill the path + showFileDialog.value = true + autoFocusFileInput() +} + const addNewItemToCurrentDirectory = (directoryItem: TreeItem) => { currentContextItem.value = directoryItem parentDirectoryForNewItems.value = directoryItem.name // Store parent context @@ -591,23 +625,87 @@ const confirmAddDirectory = () => { fullPath = parentPath + newDirectoryPath.value } - // Check for duplicates - const exists = directories.value.some( - (d) => d.path === fullPath - ) - if (exists) { - showSnackbar('该目录路径已存在!') - return - } + // Check if we're editing an existing directory + if (currentEditingItem.value && currentEditingItem.value.type === 'directory') { + const index = directories.value.findIndex(d => d.path === currentEditingItem.value?.path) + if (index !== -1) { + // Update the existing directory path + directories.value[index].path = fullPath + + // If the directory path changes, update any files/dirs that were inside it + updatePathsForRenamedDirectory(currentEditingItem.value.path, fullPath) + } + } else { + // Check for duplicates only when creating a new directory + const exists = directories.value.some( + (d) => d.path === fullPath + ) + if (exists) { + showSnackbar('该目录路径已存在!') + return + } - directories.value.push({ - path: fullPath - }) + directories.value.push({ + path: fullPath + }) + } showDirectoryDialog.value = false newDirectoryPath.value = '' parentDirectoryForNewItems.value = null // Clear parent context after creation currentContextItem.value = null + currentEditingItem.value = null + + // If we added to a parent directory, make sure it's expanded + if (parentDirectoryForNewItems.value) { + if (!expandedNodes.value.includes(`dir-${parentDirectoryForNewItems.value}`)) { + expandedNodes.value.push(`dir-${parentDirectoryForNewItems.value}`) + } + } +} + +// Helper function to update paths when a directory is renamed +const updatePathsForRenamedDirectory = (oldPath: string, newPath: string) => { + // Update all files that were in the old directory + files.value = files.value.map(file => { + if (file.path.startsWith(oldPath + '/') || file.path === oldPath) { + return { + ...file, + path: file.path.replace(oldPath, newPath) + } + } + return file + }) + + // Update all directories that were under the old directory + directories.value = directories.value.map(dir => { + if (dir.path.startsWith(oldPath + '/') || dir.path === oldPath) { + return { + ...dir, + path: dir.path.replace(oldPath, newPath) + } + } + return dir + }) +} + +// Functions to close dialogs and reset state +const closeDirectoryDialog = () => { + showDirectoryDialog.value = false + newDirectoryPath.value = '' + parentDirectoryForNewItems.value = null + currentContextItem.value = null + currentEditingItem.value = null +} + +const closeFileDialog = () => { + showFileDialog.value = false + newFilePath.value = '' + parentDirectoryForNewItems.value = null + currentContextItem.value = null + currentEditingItem.value = null + selectedSourcePath.value = '' + isAutoFindMode.value = false } const selectAndAddFile = async () => { @@ -623,68 +721,93 @@ const selectAndAddFile = async () => { fullPath = parentPath + newFilePath.value } - // Check for duplicates - const exists = files.value.some((f) => f.path === fullPath) - if (exists) { - showSnackbar('该文件路径已存在!') - return - } - - // Check if we're in auto-find mode and have a selected folder - if (isAutoFindMode.value && selectedSourcePath.value) { - // Auto-find files in the selected folder - const foundFiles = await window.mainApi.invoke('msgFindFilesInFolder', selectedSourcePath.value) - - // Add all found files as template files - for (const foundFile of foundFiles) { - // Calculate relative path from selected folder to the found file - const relativePath = foundFile.replace(selectedSourcePath.value, '').substring(1) // Remove leading slash - const fullTargetPath = fullPath ? `${fullPath}/${relativePath}` : relativePath - - // Check for duplicates - const duplicate = files.value.some(f => f.path === fullTargetPath) - if (!duplicate) { - const newFile = { - path: fullTargetPath, - sourceFilePath: foundFile - } - files.value.push(newFile) + // Check if we're editing an existing file + if (currentEditingItem.value && currentEditingItem.value.type === 'file') { + const index = files.value.findIndex(f => f.path === currentEditingItem.value?.path) + if (index !== -1) { + // Update the existing file + files.value[index].path = fullPath + if (selectedSourcePath.value) { + files.value[index].sourceFilePath = selectedSourcePath.value } } - - // Reset selection state - selectedSourcePath.value = '' - isAutoFindMode.value = false - - showFileDialog.value = false - newFilePath.value = '' - parentDirectoryForNewItems.value = null // Clear parent context after creation - currentContextItem.value = null - return - } - - // If we have a selected file, just add it - if (selectedSourcePath.value) { - const newFile = { - path: fullPath, - sourceFilePath: selectedSourcePath.value + } else { + // Check for duplicates only when creating a new file + const exists = files.value.some((f) => f.path === fullPath) + if (exists) { + showSnackbar('该文件路径已存在!') + return } - files.value.push(newFile) + // Check if we're in auto-find mode and have a selected folder + if (isAutoFindMode.value && selectedSourcePath.value) { + // Auto-find files in the selected folder + const foundFiles = await window.mainApi.invoke('msgFindFilesInFolder', selectedSourcePath.value) + + // Add all found files as template files + for (const foundFile of foundFiles) { + // Calculate relative path from selected folder to the found file + const relativePath = foundFile.replace(selectedSourcePath.value, '').substring(1) // Remove leading slash + const fullTargetPath = fullPath ? `${fullPath}/${relativePath}` : relativePath + + // Check for duplicates + const duplicate = files.value.some(f => f.path === fullTargetPath) + if (!duplicate) { + const newFile = { + path: fullTargetPath, + sourceFilePath: foundFile + } + files.value.push(newFile) + } + } + + // Reset selection state + selectedSourcePath.value = '' + isAutoFindMode.value = false + + showFileDialog.value = false + newFilePath.value = '' + parentDirectoryForNewItems.value = null // Clear parent context after creation + currentContextItem.value = null + currentEditingItem.value = null + return + } + + // If we have a selected file, just add it + if (selectedSourcePath.value) { + const newFile = { + path: fullPath, + sourceFilePath: selectedSourcePath.value + } - // Reset selection state - selectedSourcePath.value = '' - isAutoFindMode.value = false + files.value.push(newFile) - showFileDialog.value = false - newFilePath.value = '' - parentDirectoryForNewItems.value = null // Clear parent context after creation - currentContextItem.value = null + // Reset selection state + selectedSourcePath.value = '' + isAutoFindMode.value = false + + showFileDialog.value = false + newFilePath.value = '' + parentDirectoryForNewItems.value = null // Clear parent context after creation + currentContextItem.value = null + currentEditingItem.value = null + return + } + + // Otherwise, show file selection dialog + showSnackbar('请先选择一个文件或文件夹', 'warning') return } + + // Reset selection state after editing + selectedSourcePath.value = '' + isAutoFindMode.value = false - // Otherwise, show file selection dialog - showSnackbar('请先选择一个文件或文件夹', 'warning') + showFileDialog.value = false + newFilePath.value = '' + parentDirectoryForNewItems.value = null + currentContextItem.value = null + currentEditingItem.value = null } const goBack = () => { @@ -698,7 +821,19 @@ const goBack = () => { } const expandAll = () => { - expandedNodes.value = treeItems.value.map((item) => item.id) + const allIds: string[] = [] + + const collectIds = (items: TreeItem[]) => { + items.forEach(item => { + if (item.children && item.children.length > 0) { + allIds.push(item.id) + collectIds(item.children) + } + }) + } + + collectIds(treeItems.value) + expandedNodes.value = allIds } const collapseAll = () => { @@ -710,18 +845,92 @@ const editFile = (item: TreeItem) => { showSnackbar('该文件是从源文件复制的,无法直接编辑', 'info') } +// Unified method to edit items (both files and directories) +// This function needs to find the actual path of the item in our hierarchical tree +const editItem = (item: TreeItem) => { + if (item.type === 'directory') { + // Find the directory path in our directories array + // We need to find a directory that ends with the same name as this item + const dir = directories.value.find(d => { + // If the directory path ends with the item name, or the directory path is the same as item name + return d.path === item.name || d.path.endsWith('/' + item.name) || d.path.endsWith('\\' + item.name) + }) + if (dir) { + // Show a dialog to edit the directory path + showEditDirectoryDialog(dir) + } else { + showSnackbar('无法找到对应的目录', 'error') + } + } else if (item.type === 'file') { + // Find the file in our files array + const file = files.value.find(f => { + // If the file path ends with the item name, or the file path is the same as item name + return f.path === item.name || f.path.endsWith('/' + item.name) || f.path.endsWith('\\' + item.name) + }) + if (file) { + // Show a dialog to edit the file path + showEditFileDialog(file) + } else { + showSnackbar('无法找到对应的文件', 'error') + } + } +} + +// Show dialog to edit directory +const showEditDirectoryDialog = (directory: { path: string }) => { + // Set up for editing an existing directory + currentEditingItem.value = { ...directory, type: 'directory' } + newDirectoryPath.value = directory.path // Set the current path for editing + showDirectoryDialog.value = true + autoFocusDirectoryInput() +} + +// Show dialog to edit file +const showEditFileDialog = (file: { path: string; content?: string; sourceFilePath?: string }) => { + // Set up for editing an existing file + currentEditingItem.value = { ...file, type: 'file' } + newFilePath.value = file.path // Set the current path for editing + + // If the file has a source file path, set it in the selection state + if (file.sourceFilePath) { + selectedSourcePath.value = file.sourceFilePath + isAutoFindMode.value = false + } + + showFileDialog.value = true + autoFocusFileInput() +} + +// Variable to track the current item being edited +const currentEditingItem = ref<{ path: string; type: 'directory' | 'file'; content?: string; sourceFilePath?: string } | null>(null) + const deleteItem = (item: TreeItem) => { if (!confirm('确定要删除此项吗?')) return if (item.type === 'directory') { - directories.value = directories.value.filter( - (d) => `dir-${d.path}` !== item.id - ) + // Find all directories that start with the item name (including the exact match) + // This matches the directory itself and all its subdirectories + directories.value = directories.value.filter(d => { + // Exclude if exact match or it's a subdirectory + return !isSubPath(d.path, item.name); + }); + + // Also remove files that are inside this directory + files.value = files.value.filter(f => { + // Exclude if it's inside the directory being deleted + return !isSubPath(f.path, item.name); + }); } else { - files.value = files.value.filter((f) => `file-${f.path}` !== item.id) + // For files, just remove the file + files.value = files.value.filter(f => f.path !== item.name); } } +// Helper function to check if pathA is a subpath of pathB +const isSubPath = (pathA: string, pathB: string) => { + return pathA === pathB || pathA.startsWith(pathB + '/') || pathA.startsWith(pathB + '\\'); +} + // Show snackbar message const showSnackbar = (message: string, color: string = 'error') => { snackbarMessage.value = message