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