feat: 优化模板创建界面,添加编辑文件和目录功能,增强用户体验

This commit is contained in:
zhanggy committed 2025-10-28 15:00:21 +08:00
1 parent d6d86e5042
commit 4367fd6ed1
3 files changed
+263 -22

No files matched your search

+14
View File
@@ -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
@@ -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.
Always respond in 中文
+22 -4
View File
@@ -61,18 +61,34 @@
class="d-flex align-center ga-1"
>
<v-btn
v-if="item.type === 'file'"
v-if="!readOnly"
icon
variant="text"
size="small"
@click.stop="emit('editFile', item)"
@click.stop="emit('editItem', item)"
>
<v-icon size="small"> mdi-pencil </v-icon>
<v-tooltip
activator="parent"
location="top"
>
编辑内容
{{ item.type === 'file' ? '编辑文件' : '编辑目录' }}
</v-tooltip>
</v-btn>
<v-btn
v-if="item.type === 'directory' && !readOnly"
icon
variant="text"
size="small"
@click.stop="emit('addFileToDirectory', item)"
>
<v-icon size="small"> mdi-file-plus </v-icon>
<v-tooltip
activator="parent"
location="top"
>
在目录下创建文件
</v-tooltip>
</v-btn>
@@ -192,9 +208,11 @@ const emit = defineEmits([
'showContextMenu',
'addNewItemToCurrentDirectory',
'editFile',
'editItem',
'deleteItem',
'addDirectory',
'addFile'
'addFile',
'addFileToDirectory'
])
</script>
+227 -18
View File
@@ -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"
/>
</v-col>
</v-row>
@@ -164,13 +166,13 @@
max-width="500"
>
<v-card>
<v-card-title>添加目录</v-card-title>
<v-card-title>{{ currentEditingItem ? '编辑目录' : '添加目录' }}</v-card-title>
<v-card-text>
<v-form v-model="directoryFormValid">
<v-text-field
v-model="newDirectoryPath"
label="目录路径 *"
placeholder="例如: src/components"
:label="currentEditingItem ? '新目录路径 *' : '目录路径 *'"
:placeholder="currentEditingItem ? '例如: src/new-components' : '例如: src/components'"
:rules="[requiredRule]"
required
density="comfortable"
@@ -181,13 +183,17 @@
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn @click="showDirectoryDialog = false"> 取消 </v-btn>
<v-btn
@click="closeDirectoryDialog"
>
取消
</v-btn>
<v-btn
color="primary"
@click="confirmAddDirectory"
:disabled="!directoryFormValid"
>
添加
{{ currentEditingItem ? '保存' : '添加' }}
</v-btn>
</v-card-actions>
</v-card>
@@ -199,13 +205,13 @@
max-width="600"
>
<v-card>
<v-card-title>添加文件</v-card-title>
<v-card-title>{{ currentEditingItem ? '编辑文件' : '添加文件' }}</v-card-title>
<v-card-text>
<v-form v-model="fileFormValid">
<v-text-field
v-model="newFilePath"
label="目标文件路径 *"
placeholder="例如: src/index.js"
:label="currentEditingItem ? '新文件路径 *' : '目标文件路径 *'"
:placeholder="currentEditingItem ? '例如: src/new-index.js' : '例如: src/index.js'"
:rules="[requiredRule]"
required
density="comfortable"
@@ -218,6 +224,7 @@
@click="selectSourceFile"
variant="outlined"
class="mr-2"
:disabled="!!currentEditingItem"
>
选择文件
</v-btn>
@@ -225,6 +232,7 @@
@click="selectSourceFolder"
variant="outlined"
class="mr-2"
:disabled="!!currentEditingItem"
>
选择文件夹
</v-btn>
@@ -250,17 +258,34 @@
<span>自动寻找文件夹内的文件</span>
</div>
</v-alert>
<!-- Note when editing -->
<v-alert
v-if="currentEditingItem"
type="warning"
variant="tonal"
class="mt-2"
>
<div class="d-flex align-center">
<v-icon class="mr-2">mdi-information</v-icon>
<span>正在编辑现有文件路径,文件源不会改变</span>
</div>
</v-alert>
</v-form>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn @click="showFileDialog = false"> 取消 </v-btn>
<v-btn
@click="closeFileDialog"
>
取消
</v-btn>
<v-btn
color="primary"
@click="selectAndAddFile"
:disabled="!fileFormValid || !selectedSourcePath"
:disabled="!fileFormValid || (!selectedSourcePath && !currentEditingItem)"
>
添加
{{ currentEditingItem ? '保存' : '添加' }}
</v-btn>
</v-card-actions>
</v-card>
@@ -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,7 +625,18 @@ const confirmAddDirectory = () => {
fullPath = parentPath + newDirectoryPath.value
}
// Check for duplicates
// 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
)
@@ -603,11 +648,64 @@ const confirmAddDirectory = () => {
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,7 +721,18 @@ const selectAndAddFile = async () => {
fullPath = parentPath + newFilePath.value
}
// Check for duplicates
// 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
}
}
} else {
// Check for duplicates only when creating a new file
const exists = files.value.some((f) => f.path === fullPath)
if (exists) {
showSnackbar('该文件路径已存在!')
@@ -660,6 +769,7 @@ const selectAndAddFile = async () => {
newFilePath.value = ''
parentDirectoryForNewItems.value = null // Clear parent context after creation
currentContextItem.value = null
currentEditingItem.value = null
return
}
@@ -680,11 +790,24 @@ const selectAndAddFile = async () => {
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
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