feat: 重构模板管理,采用树状结构存储文件和目录,优化模板创建和编辑流程

This commit is contained in:
zhanggy committed 2025-10-28 18:34:31 +08:00
1 parent f8890ff1e9
commit 5ca31a5348
4 files changed
+351 -246

No files matched your search

+189 -190
View File
@@ -348,8 +348,7 @@
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useTemplateStore } from '@/renderer/store/template'
import { Template } from '@/renderer/store/template'
import { useTemplateStore, TreeNode } from '@/renderer/store/template'
import TemplateTree from '@/renderer/components/TemplateTree.vue'
import { ConfirmDialogService } from '@/renderer/utils/confirm-dialog-service'
@@ -405,99 +404,37 @@ interface TreeItem {
children?: TreeItem[]
}
// Item state
const directories = ref<{ path: string }[]>([])
const files = ref<{ path: string; content?: string; sourceFilePath?: string }[]>([])
// Item state - 使用树状结构
const treeStructure = ref<TreeNode[]>([])
// Computed
const isEditMode = computed(() => !!route.params.id)
const treeItems = computed<TreeItem[]>(() => {
// Create root structure
const root: TreeItem = {
id: 'root',
name: 'root',
type: 'directory',
children: []
// 直接返回树状结构
return treeStructure.value.map(node => convertToTreeItem(node));
})
// 转换 TreeNode 到 TreeItem 的辅助函数
const convertToTreeItem = (node: TreeNode): TreeItem => {
const treeItem: TreeItem = {
id: node.id,
name: node.name,
type: node.type
};
if (node.children && node.children.length > 0) {
treeItem.children = node.children.map(child => convertToTreeItem(child));
}
// Process each directory
directories.value.forEach((dir) => {
const pathParts = dir.path.split('/').filter(part => part !== '') // Split path and remove empty parts
let currentLevel = root.children || []
// Navigate/create the directory structure
for (let i = 0; i < pathParts.length; i++) {
const part = pathParts[i]
const isLastPart = i === pathParts.length - 1
// Check if this part already exists in the current level
let existingItem = currentLevel.find(item => item.name === part)
if (!existingItem) {
// Create new directory item
const newDir: TreeItem = {
id: `dir-${part}-${Date.now()}-${Math.random()}`, // Unique ID
name: part,
type: 'directory',
children: []
}
currentLevel.push(newDir)
existingItem = newDir
}
// Move to the next level if not the last part
if (!isLastPart && existingItem.children) {
currentLevel = existingItem.children
}
}
})
// Process each file and add to the appropriate directory
files.value.forEach((file) => {
const pathParts = file.path.split('/').filter(part => part !== '')
const fileName = pathParts.pop() // Get the last part as filename
if (!fileName) return // Skip if no filename
let currentLevel = root.children || []
// Navigate to the appropriate directory for the file
for (let i = 0; i < pathParts.length; i++) {
const part = pathParts[i]
const dirItem = currentLevel.find(item => item.name === part && item.type === 'directory')
if (dirItem && dirItem.children) {
currentLevel = dirItem.children
} else {
// If the directory doesn't exist, create it
const newDir: TreeItem = {
id: `dir-${part}-${Date.now()}-${Math.random()}`,
name: part,
type: 'directory',
children: []
}
currentLevel.push(newDir)
currentLevel = newDir.children || []
}
}
// Add the file to the current level
currentLevel.push({
id: `file-${fileName}-${Date.now()}-${Math.random()}`,
name: fileName,
type: 'file'
})
})
return root.children || []
})
return treeItem;
}
// Validation rules
const requiredRule = (v: string | null | undefined) => !!v || '此字段为必填项'
// Watch for changes to detect unsaved changes
watch(
[templateName, templateDescription, directories, files],
[templateName, templateDescription, treeStructure],
() => {
hasChanges.value = true
},
@@ -522,8 +459,7 @@ onMounted(async () => {
if (template) {
templateName.value = template.name
templateDescription.value = template.description
directories.value = [...template.directories]
files.value = [...template.files]
treeStructure.value = [...template.treeStructure]
hasChanges.value = false
} else {
router.push('/templates')
@@ -601,29 +537,35 @@ const confirmAddDirectory = () => {
fullPath = parentPath + newDirectoryPath.value
}
const newId = `dir-${Date.now()}-${Math.random()}`
// 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)
}
// Find and update the existing directory in the tree
const updatedNode: TreeNode = {
id: currentEditingItem.value.id,
name: newDirectoryPath.value,
path: fullPath,
type: 'directory'
};
treeStructure.value = updateNodeInTree(treeStructure.value, currentEditingItem.value.path, updatedNode)
} else {
// Check for duplicates only when creating a new directory
const exists = directories.value.some(
(d) => d.path === fullPath
)
if (exists) {
showSnackbar('该目录路径已存在!')
return
// Create new directory node
const newNode: TreeNode = {
id: newId,
name: newDirectoryPath.value,
path: fullPath,
type: 'directory'
}
directories.value.push({
path: fullPath
})
// Add to tree structure
if (parentDirectoryForNewItems.value) {
// Add as child to the parent directory
treeStructure.value = addChildToNode(treeStructure.value, parentDirectoryForNewItems.value, newNode)
} else {
// Add as root node
treeStructure.value.push(newNode)
}
}
showDirectoryDialog.value = false
@@ -634,35 +576,48 @@ const confirmAddDirectory = () => {
nextTick(() => {
expandAll()
})
newDirectoryPath.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)
}
// 在树状结构中更新节点的辅助函数
const updateNodeInTree = (nodes: TreeNode[], oldPath: string, updatedNode: TreeNode): TreeNode[] => {
return nodes.map(node => {
if (node.path === oldPath) {
return updatedNode;
}
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) {
if (node.children) {
return {
...dir,
path: dir.path.replace(oldPath, newPath)
}
...node,
children: updateNodeInTree(node.children, oldPath, updatedNode)
};
}
return dir
})
return node;
});
}
// 向节点添加子节点的辅助函数
const addChildToNode = (nodes: TreeNode[], parentName: string, childNode: TreeNode): TreeNode[] => {
return nodes.map(node => {
if (node.name === parentName || (node.path && node.path.endsWith('/' + parentName) || node.path === parentName)) {
const children = node.children || [];
return {
...node,
children: [...children, childNode]
};
}
if (node.children) {
return {
...node,
children: addChildToNode(node.children, parentName, childNode)
};
}
return node;
});
}
// Functions to close dialogs and reset state
const closeDirectoryDialog = () => {
showDirectoryDialog.value = false
@@ -693,20 +648,24 @@ const selectAndAddFile = async () => {
fullPath = parentPath + newFilePath.value
}
const newId = `file-${Date.now()}-${Math.random()}`
// 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
}
}
// Find and update the existing file in the tree
const updatedNode: TreeNode = {
id: currentEditingItem.value.id,
name: newFilePath.value,
path: fullPath,
type: 'file',
content: currentEditingItem.value.content,
sourceFilePath: currentEditingItem.value.sourceFilePath
};
treeStructure.value = updateNodeInTree(treeStructure.value, currentEditingItem.value.path, updatedNode)
} else {
// Check for duplicates only when creating a new file
const exists = files.value.some((f) => f.path === fullPath)
if (exists) {
const hasDuplicate = checkForDuplicateInTree(treeStructure.value, fullPath);
if (hasDuplicate) {
showSnackbar('该文件路径已存在!')
return
}
@@ -723,13 +682,24 @@ const selectAndAddFile = async () => {
const fullTargetPath = fullPath ? `${fullPath}/${relativePath}` : relativePath
// Check for duplicates
const duplicate = files.value.some(f => f.path === fullTargetPath)
if (!duplicate) {
const newFile = {
const hasDuplicateInTree = checkForDuplicateInTree(treeStructure.value, fullTargetPath);
if (!hasDuplicateInTree) {
const newFileNode: TreeNode = {
id: `file-${Date.now()}-${Math.random()}`,
name: fullTargetPath.split('/').pop() || fullTargetPath,
path: fullTargetPath,
type: 'file',
sourceFilePath: foundFile
}
files.value.push(newFile)
// Add to tree structure
if (parentDirectoryForNewItems.value) {
// Add as child to the parent directory
treeStructure.value = addChildToNode(treeStructure.value, parentDirectoryForNewItems.value, newFileNode)
} else {
// Add as root node
treeStructure.value.push(newFileNode)
}
}
}
@@ -752,12 +722,22 @@ const selectAndAddFile = async () => {
// If we have a selected file, just add it
if (selectedSourcePath.value) {
const newFile = {
const newFileNode: TreeNode = {
id: newId,
name: newFilePath.value,
path: fullPath,
type: 'file',
sourceFilePath: selectedSourcePath.value
}
files.value.push(newFile)
// Add to tree structure
if (parentDirectoryForNewItems.value) {
// Add as child to the parent directory
treeStructure.value = addChildToNode(treeStructure.value, parentDirectoryForNewItems.value, newFileNode)
} else {
// Add as root node
treeStructure.value.push(newFileNode)
}
// Reset selection state
selectedSourcePath.value = ''
@@ -797,6 +777,21 @@ const selectAndAddFile = async () => {
newFilePath.value = ''
}
// 检查树中是否有重复路径的辅助函数
const checkForDuplicateInTree = (nodes: TreeNode[], pathToCheck: string): boolean => {
for (const node of nodes) {
if (node.path === pathToCheck) {
return true;
}
if (node.children) {
if (checkForDuplicateInTree(node.children, pathToCheck)) {
return true;
}
}
}
return false;
}
const goBack = async () => {
if (hasChanges.value) {
const confirmed = await ConfirmDialogService.confirm({
@@ -843,47 +838,56 @@ const editFile = (item: TreeItem) => {
// 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)
// Find the node in the tree structure
const node = findNodeInTree(treeStructure.value, item.id);
if (node) {
const nodeData = {
id: node.id,
path: node.path,
type: node.type,
content: node.content, // For files
sourceFilePath: node.sourceFilePath, // For files
name: node.name,
};
if (node.type === 'directory') {
showEditDirectoryDialog(nodeData)
} 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')
showEditFileDialog(nodeData)
}
} else {
showSnackbar('无法找到对应的节点', 'error')
}
}
// 在树状结构中查找节点的辅助函数
const findNodeInTree = (nodes: TreeNode[], nodeId: string): TreeNode | null => {
for (const node of nodes) {
if (node.id === nodeId) {
return node;
}
if (node.children) {
const found = findNodeInTree(node.children, nodeId);
if (found) {
return found;
}
}
}
return null;
}
// Show dialog to edit directory
const showEditDirectoryDialog = (directory: { path: string }) => {
const showEditDirectoryDialog = (directory: TreeNode) => {
// Set up for editing an existing directory
currentEditingItem.value = { ...directory, type: 'directory' }
currentEditingItem.value = 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 }) => {
const showEditFileDialog = (file: TreeNode) => {
// Set up for editing an existing file
currentEditingItem.value = { ...file, type: 'file' }
currentEditingItem.value = 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
@@ -897,7 +901,7 @@ const showEditFileDialog = (file: { path: string; content?: string; sourceFilePa
}
// Variable to track the current item being edited
const currentEditingItem = ref<{ path: string; type: 'directory' | 'file'; content?: string; sourceFilePath?: string } | null>(null)
const currentEditingItem = ref<TreeNode | null>(null)
const deleteItem = async (item: TreeItem) => {
const confirmed = await ConfirmDialogService.confirm({
@@ -910,30 +914,26 @@ const deleteItem = async (item: TreeItem) => {
if (!confirmed) return
if (item.type === 'directory') {
// 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 {
// For files, just remove the file
files.value = files.value.filter(f => f.path !== item.name);
}
// 从树状结构中删除项目
treeStructure.value = removeItemFromTree(treeStructure.value, item.id);
}
// 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 + '\\');
// 从树状结构中删除项目的辅助函数
const removeItemFromTree = (nodes: TreeNode[], itemId: string): TreeNode[] => {
return nodes.filter(node => node.id !== itemId) // 过滤掉要删除的节点
.map(node => {
if (node.children) {
return {
...node,
children: removeItemFromTree(node.children, itemId)
};
}
return node;
});
}
// Show snackbar message
const showSnackbar = (message: string, color: string = 'error') => {
snackbarMessage.value = message
@@ -979,8 +979,7 @@ const saveTemplate = () => {
const templateData = {
name: templateName.value,
description: templateDescription.value,
files: files.value,
directories: directories.value
treeStructure: treeStructure.value
}
if (isEditMode.value) {