feat: 添加文件选择和自动查找功能,优化模板创建界面
This commit is contained in:
1 parent
de7af2f650
commit
d6d86e5042
4 files changed
+174
-46
No files matched your search
+50
-2
@@ -5,7 +5,8 @@ import {
|
|||||||
writeFileSync,
|
writeFileSync,
|
||||||
readFileSync,
|
readFileSync,
|
||||||
existsSync,
|
existsSync,
|
||||||
ensureDirSync
|
ensureDirSync,
|
||||||
|
copyFileSync
|
||||||
} from 'fs-extra'
|
} from 'fs-extra'
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -35,6 +36,8 @@ export default class IPCs {
|
|||||||
filters.push({ name: 'Text', extensions: ['txt', 'json'] })
|
filters.push({ name: 'Text', extensions: ['txt', 'json'] })
|
||||||
} else if (filter === 'zip') {
|
} else if (filter === 'zip') {
|
||||||
filters.push({ name: 'Zip', extensions: ['zip'] })
|
filters.push({ name: 'Zip', extensions: ['zip'] })
|
||||||
|
} else if (filter === 'any') {
|
||||||
|
filters.push({ name: 'All Files', extensions: ['*'] })
|
||||||
}
|
}
|
||||||
const dialogResult = await dialog.showOpenDialog({
|
const dialogResult = await dialog.showOpenDialog({
|
||||||
properties: ['openFile'],
|
properties: ['openFile'],
|
||||||
@@ -60,6 +63,43 @@ export default class IPCs {
|
|||||||
return { canceled: true }
|
return { canceled: true }
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Find all files in a folder recursively
|
||||||
|
ipcMain.handle(
|
||||||
|
'msgFindFilesInFolder',
|
||||||
|
async (event: IpcMainEvent, folderPath: string) => {
|
||||||
|
try {
|
||||||
|
const { readdirSync, statSync } = require('fs')
|
||||||
|
const { join } = require('path')
|
||||||
|
|
||||||
|
const foundFiles: string[] = []
|
||||||
|
|
||||||
|
const searchFiles = (dir: string) => {
|
||||||
|
const items = readdirSync(dir)
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
const fullPath = join(dir, item)
|
||||||
|
const stats = statSync(fullPath)
|
||||||
|
|
||||||
|
if (stats.isDirectory()) {
|
||||||
|
// Recursively search in subdirectories
|
||||||
|
searchFiles(fullPath)
|
||||||
|
} else if (stats.isFile()) {
|
||||||
|
// Add file to the list
|
||||||
|
foundFiles.push(fullPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
searchFiles(folderPath)
|
||||||
|
|
||||||
|
return foundFiles
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error finding files in folder:', error)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
// Load data from local file
|
// Load data from local file
|
||||||
ipcMain.handle(
|
ipcMain.handle(
|
||||||
'msgLoadData',
|
'msgLoadData',
|
||||||
@@ -148,7 +188,15 @@ export default class IPCs {
|
|||||||
const filePath = join(baseDirPath, file.path)
|
const filePath = join(baseDirPath, file.path)
|
||||||
const fileDir = dirname(filePath)
|
const fileDir = dirname(filePath)
|
||||||
ensureDirSync(fileDir)
|
ensureDirSync(fileDir)
|
||||||
writeFileSync(filePath, file.content, 'utf-8')
|
|
||||||
|
if (file.sourceFilePath) {
|
||||||
|
// Copy from source file
|
||||||
|
const sourceContent = readFileSync(file.sourceFilePath)
|
||||||
|
writeFileSync(filePath, sourceContent)
|
||||||
|
} else if (file.content) {
|
||||||
|
// Write content directly
|
||||||
|
writeFileSync(filePath, file.content, 'utf-8')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ const mainAvailChannels: string[] = [
|
|||||||
'msgSelectDirectory',
|
'msgSelectDirectory',
|
||||||
'msgLoadData',
|
'msgLoadData',
|
||||||
'msgSaveData',
|
'msgSaveData',
|
||||||
'msgExecuteTemplate'
|
'msgExecuteTemplate',
|
||||||
|
'msgFindFilesInFolder'
|
||||||
]
|
]
|
||||||
const rendererAvailChannels: string[] = []
|
const rendererAvailChannels: string[] = []
|
||||||
|
|
||||||
|
|||||||
@@ -196,7 +196,7 @@
|
|||||||
<!-- Add File Dialog -->
|
<!-- Add File Dialog -->
|
||||||
<v-dialog
|
<v-dialog
|
||||||
v-model="showFileDialog"
|
v-model="showFileDialog"
|
||||||
max-width="500"
|
max-width="600"
|
||||||
>
|
>
|
||||||
<v-card>
|
<v-card>
|
||||||
<v-card-title>添加文件</v-card-title>
|
<v-card-title>添加文件</v-card-title>
|
||||||
@@ -204,14 +204,52 @@
|
|||||||
<v-form v-model="fileFormValid">
|
<v-form v-model="fileFormValid">
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-model="newFilePath"
|
v-model="newFilePath"
|
||||||
label="文件路径 *"
|
label="目标文件路径 *"
|
||||||
placeholder="例如: src/index.js"
|
placeholder="例如: src/index.js"
|
||||||
:rules="[requiredRule]"
|
:rules="[requiredRule]"
|
||||||
required
|
required
|
||||||
density="comfortable"
|
density="comfortable"
|
||||||
ref="fileInputRef"
|
ref="fileInputRef"
|
||||||
@keydown.enter="confirmAddFile"
|
@keydown.enter="selectAndAddFile"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<div class="d-flex align-center my-4">
|
||||||
|
<v-btn
|
||||||
|
@click="selectSourceFile"
|
||||||
|
variant="outlined"
|
||||||
|
class="mr-2"
|
||||||
|
>
|
||||||
|
选择文件
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
@click="selectSourceFolder"
|
||||||
|
variant="outlined"
|
||||||
|
class="mr-2"
|
||||||
|
>
|
||||||
|
选择文件夹
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Display selected source -->
|
||||||
|
<div v-if="selectedSourcePath" class="d-flex align-center">
|
||||||
|
<v-icon :color="isAutoFindMode ? 'warning' : 'info'" class="mr-2">
|
||||||
|
{{ isAutoFindMode ? 'mdi-folder' : 'mdi-file' }}
|
||||||
|
</v-icon>
|
||||||
|
<span class="text-truncate">{{ selectedSourcePath }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Auto-find indicator -->
|
||||||
|
<v-alert
|
||||||
|
v-if="isAutoFindMode"
|
||||||
|
type="info"
|
||||||
|
variant="tonal"
|
||||||
|
class="mt-2"
|
||||||
|
>
|
||||||
|
<div class="d-flex align-center">
|
||||||
|
<v-icon class="mr-2">mdi-auto-fix</v-icon>
|
||||||
|
<span>自动寻找文件夹内的文件</span>
|
||||||
|
</div>
|
||||||
|
</v-alert>
|
||||||
</v-form>
|
</v-form>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
<v-card-actions>
|
<v-card-actions>
|
||||||
@@ -219,10 +257,10 @@
|
|||||||
<v-btn @click="showFileDialog = false"> 取消 </v-btn>
|
<v-btn @click="showFileDialog = false"> 取消 </v-btn>
|
||||||
<v-btn
|
<v-btn
|
||||||
color="primary"
|
color="primary"
|
||||||
@click="confirmAddFile"
|
@click="selectAndAddFile"
|
||||||
:disabled="!fileFormValid"
|
:disabled="!fileFormValid || !selectedSourcePath"
|
||||||
>
|
>
|
||||||
添加并编辑
|
添加
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</v-card-actions>
|
</v-card-actions>
|
||||||
</v-card>
|
</v-card>
|
||||||
@@ -248,9 +286,9 @@
|
|||||||
<v-spacer />
|
<v-spacer />
|
||||||
<v-btn
|
<v-btn
|
||||||
variant="text"
|
variant="text"
|
||||||
@click="saveFileContent"
|
@click="showEditorDialog = false"
|
||||||
>
|
>
|
||||||
保存
|
关闭
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</v-toolbar>
|
</v-toolbar>
|
||||||
|
|
||||||
@@ -337,6 +375,8 @@ const newFilePath = ref('')
|
|||||||
const editingFile = ref<{ path: string; content: string } | null>(null)
|
const editingFile = ref<{ path: string; content: string } | null>(null)
|
||||||
const editingFileContent = ref('')
|
const editingFileContent = ref('')
|
||||||
const parentDirectoryForNewItems = ref<string | null>(null) // Track parent context
|
const parentDirectoryForNewItems = ref<string | null>(null) // Track parent context
|
||||||
|
const selectedSourcePath = ref<string>('') // Track the selected source file or folder
|
||||||
|
const isAutoFindMode = ref(false) // Track if auto-find mode is enabled
|
||||||
|
|
||||||
// Tree state
|
// Tree state
|
||||||
const expandedNodes = ref<string[]>([])
|
const expandedNodes = ref<string[]>([])
|
||||||
@@ -352,7 +392,7 @@ interface TreeItem {
|
|||||||
|
|
||||||
// Item state
|
// Item state
|
||||||
const directories = ref<{ path: string }[]>([])
|
const directories = ref<{ path: string }[]>([])
|
||||||
const files = ref<{ path: string; content: string }[]>([])
|
const files = ref<{ path: string; content?: string; sourceFilePath?: string }[]>([])
|
||||||
|
|
||||||
// Computed
|
// Computed
|
||||||
const isEditMode = computed(() => !!route.params.id)
|
const isEditMode = computed(() => !!route.params.id)
|
||||||
@@ -570,7 +610,7 @@ const confirmAddDirectory = () => {
|
|||||||
currentContextItem.value = null
|
currentContextItem.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
const confirmAddFile = () => {
|
const selectAndAddFile = async () => {
|
||||||
if (!newFilePath.value.trim()) return
|
if (!newFilePath.value.trim()) return
|
||||||
|
|
||||||
// Construct the full path if there's a parent directory
|
// Construct the full path if there's a parent directory
|
||||||
@@ -590,20 +630,61 @@ const confirmAddFile = () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const newFile = {
|
// Check if we're in auto-find mode and have a selected folder
|
||||||
path: fullPath,
|
if (isAutoFindMode.value && selectedSourcePath.value) {
|
||||||
content: ''
|
// 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
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
files.value.push(newFile)
|
// If we have a selected file, just add it
|
||||||
|
if (selectedSourcePath.value) {
|
||||||
|
const newFile = {
|
||||||
|
path: fullPath,
|
||||||
|
sourceFilePath: selectedSourcePath.value
|
||||||
|
}
|
||||||
|
|
||||||
// Open editor for the new file
|
files.value.push(newFile)
|
||||||
editFileContent(newFile)
|
|
||||||
|
|
||||||
showFileDialog.value = false
|
// Reset selection state
|
||||||
newFilePath.value = ''
|
selectedSourcePath.value = ''
|
||||||
parentDirectoryForNewItems.value = null // Clear parent context after creation
|
isAutoFindMode.value = false
|
||||||
currentContextItem.value = null
|
|
||||||
|
showFileDialog.value = false
|
||||||
|
newFilePath.value = ''
|
||||||
|
parentDirectoryForNewItems.value = null // Clear parent context after creation
|
||||||
|
currentContextItem.value = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, show file selection dialog
|
||||||
|
showSnackbar('请先选择一个文件或文件夹', 'warning')
|
||||||
}
|
}
|
||||||
|
|
||||||
const goBack = () => {
|
const goBack = () => {
|
||||||
@@ -625,28 +706,8 @@ const collapseAll = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const editFile = (item: TreeItem) => {
|
const editFile = (item: TreeItem) => {
|
||||||
const file = files.value.find((f) => `file-${f.path}` === item.id)
|
// For now, just show a message that files are selected, not edited
|
||||||
if (file) {
|
showSnackbar('该文件是从源文件复制的,无法直接编辑', 'info')
|
||||||
editFileContent(file)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const editFileContent = (file: { path: string; content: string }) => {
|
|
||||||
editingFile.value = file
|
|
||||||
editingFileContent.value = file.content
|
|
||||||
showEditorDialog.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
const saveFileContent = () => {
|
|
||||||
if (editingFile.value) {
|
|
||||||
const index = files.value.findIndex(
|
|
||||||
(f) => f.path === editingFile.value?.path
|
|
||||||
)
|
|
||||||
if (index !== -1) {
|
|
||||||
files.value[index].content = editingFileContent.value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
showEditorDialog.value = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteItem = (item: TreeItem) => {
|
const deleteItem = (item: TreeItem) => {
|
||||||
@@ -668,6 +729,23 @@ const showSnackbar = (message: string, color: string = 'error') => {
|
|||||||
snackbarVisible.value = true
|
snackbarVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Methods for file/folder selection
|
||||||
|
const selectSourceFile = async () => {
|
||||||
|
const result = await window.mainApi.invoke('msgOpenFile', 'any')
|
||||||
|
if (!result.canceled && result.filePaths && result.filePaths.length > 0) {
|
||||||
|
selectedSourcePath.value = result.filePaths[0]
|
||||||
|
isAutoFindMode.value = false // Not in auto-find mode when selecting a specific file
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectSourceFolder = async () => {
|
||||||
|
const result = await window.mainApi.invoke('msgSelectDirectory')
|
||||||
|
if (!result.canceled && result.filePath) {
|
||||||
|
selectedSourcePath.value = result.filePath
|
||||||
|
isAutoFindMode.value = true // In auto-find mode when selecting a folder
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Auto focus methods
|
// Auto focus methods
|
||||||
const autoFocusDirectoryInput = async () => {
|
const autoFocusDirectoryInput = async () => {
|
||||||
await nextTick()
|
await nextTick()
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ import { ref } from 'vue'
|
|||||||
|
|
||||||
interface TemplateFile {
|
interface TemplateFile {
|
||||||
path: string
|
path: string
|
||||||
content: string
|
content?: string // Content for text files that are created directly
|
||||||
|
sourceFilePath?: string // Path to copy from for file copying
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TemplateDirectory {
|
interface TemplateDirectory {
|
||||||
|
|||||||
Reference in new issue
Block a user