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

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

No files matched your search

+29 -14
View File
@@ -177,25 +177,40 @@ export default class IPCs {
const baseDirPath = join(targetPath, baseDirName)
ensureDirSync(baseDirPath)
// Create directories from template
for (const dir of template.directories) {
const dirPath = join(baseDirPath, dir.path)
ensureDirSync(dirPath)
// Helper function to traverse the tree and collect all nodes
const collectAllNodes = (nodes, allNodes = []) => {
for (const node of nodes) {
allNodes.push(node);
if (node.children) {
collectAllNodes(node.children, allNodes);
}
}
return allNodes;
};
// Create files from template
for (const file of template.files) {
const filePath = join(baseDirPath, file.path)
const fileDir = dirname(filePath)
ensureDirSync(fileDir)
// Collect all nodes from the tree structure
if (template.treeStructure && template.treeStructure.length > 0) {
const allNodes = collectAllNodes(template.treeStructure);
if (file.sourceFilePath) {
// Process all nodes, making sure directories are created before files
for (const node of allNodes) {
const nodePath = join(baseDirPath, node.path);
if (node.type === 'directory') {
ensureDirSync(nodePath);
} else if (node.type === 'file') {
const fileDir = dirname(nodePath);
ensureDirSync(fileDir);
if (node.sourceFilePath) {
// Copy from source file
const sourceContent = readFileSync(file.sourceFilePath)
writeFileSync(filePath, sourceContent)
} else if (file.content) {
const sourceContent = readFileSync(node.sourceFilePath);
writeFileSync(nodePath, sourceContent);
} else if (node.content) {
// Write content directly
writeFileSync(filePath, file.content, 'utf-8')
writeFileSync(nodePath, node.content, 'utf-8');
}
}
}
}
}
+188 -189
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: []
}
// 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 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));
}
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
@@ -638,31 +580,44 @@ const confirmAddDirectory = () => {
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) {
// 在树状结构中更新节点的辅助函数
const updateNodeInTree = (nodes: TreeNode[], oldPath: string, updatedNode: TreeNode): TreeNode[] => {
return nodes.map(node => {
if (node.path === oldPath) {
return updatedNode;
}
if (node.children) {
return {
...file,
path: file.path.replace(oldPath, newPath)
...node,
children: updateNodeInTree(node.children, oldPath, 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) {
return {
...dir,
path: dir.path.replace(oldPath, newPath)
}
}
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')
showEditFileDialog(nodeData)
}
} 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')
}
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,29 +914,25 @@ 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);
});
// 从树状结构中删除项目
treeStructure.value = removeItemFromTree(treeStructure.value, item.id);
}
// 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);
// 从树状结构中删除项目的辅助函数
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;
});
}
// 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') => {
@@ -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) {
+26 -34
View File
@@ -37,10 +37,10 @@
<v-card-text class="flex-grow-1 py-2">
<v-chip-group column>
<v-chip size="small" color="primary" variant="tonal">
文件: {{ template.files.length }}
节点: {{ countTreeNodes(template.treeStructure).files }}
</v-chip>
<v-chip size="small" color="success" variant="tonal">
目录: {{ template.directories.length }}
目录: {{ countTreeNodes(template.treeStructure).directories }}
</v-chip>
</v-chip-group>
</v-card-text>
@@ -138,7 +138,7 @@
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useTemplateStore } from '@/renderer/store/template'
import { useTemplateStore, TreeNode } from '@/renderer/store/template'
import { Template } from '@/renderer/store/template'
import TemplateTree from '@/renderer/components/TemplateTree.vue'
import { ConfirmDialogService } from '@/renderer/utils/confirm-dialog-service'
@@ -166,42 +166,34 @@ const executePath = ref('')
const baseDirNames = ref('')
const executeFormValid = ref(false)
// Define item type for the tree view
interface TreeNode {
id: string
name: string
type: 'directory' | 'file'
children?: TreeNode[]
// Count files and directories in tree structure
const countTreeNodes = (nodes: TreeNode[]) => {
let files = 0;
let directories = 0;
const count = (nodeList: TreeNode[]) => {
nodeList.forEach(node => {
if (node.type === 'file') {
files++;
} else if (node.type === 'directory') {
directories++;
if (node.children) {
count(node.children);
}
}
});
};
count(nodes || []);
return { files, directories };
}
const templateTree = computed<TreeNode[]>(() => {
if (!selectedTemplate.value) return []
const items: TreeNode[] = []
// Add directories
selectedTemplate.value.directories.forEach((dir) => {
items.push({
id: `dir-${dir.path}`,
name: dir.path.split('/').pop() || dir.path, // Show just the directory name, not full path
type: 'directory',
children: []
})
})
// Add files
selectedTemplate.value.files.forEach((file) => {
items.push({
id: `file-${file.path}`,
name: file.path.split('/').pop() || file.path, // Show just the file name, not full path
type: 'file'
})
})
// Auto-expand all nodes when showing template details
expandedTreeNodes.value = items.map(item => item.id)
return items
// 直接返回模板的树状结构
return selectedTemplate.value.treeStructure || []
})
const navigateToCreateTemplate = () => {
+103 -4
View File
@@ -1,22 +1,35 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
interface TemplateFile {
// 保持旧的接口用于数据迁移
interface OldTemplateFile {
path: string
content?: string // Content for text files that are created directly
sourceFilePath?: string // Path to copy from for file copying
}
interface TemplateDirectory {
interface OldTemplateDirectory {
path: string
}
// 定义树状结构的节点
export interface TreeNode {
id: string
name: string
path: string // 完整路径
type: 'directory' | 'file'
children?: TreeNode[]
// 文件特定属性
content?: string
sourceFilePath?: string
}
export interface Template {
id: string
name: string
description: string
files: TemplateFile[]
directories: TemplateDirectory[]
// 使用树状结构存储
treeStructure: TreeNode[]
}
export const useTemplateStore = defineStore('template', () => {
@@ -95,6 +108,92 @@ export const useTemplateStore = defineStore('template', () => {
}))
}
// 将扁平结构转换为树状结构
function convertFlatToTreeStructure(files: OldTemplateFile[], directories: OldTemplateDirectory[]): TreeNode[] {
// 创建根节点
const root: TreeNode = {
id: 'root',
name: 'root',
path: '',
type: 'directory',
children: []
}
// 先处理所有目录
directories.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: TreeNode = {
id: `dir-${part}-${Date.now()}-${Math.random()}`, // Unique ID
name: part,
path: dir.path.substring(0, dir.path.indexOf(part) + part.length), // Full path up to this 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.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: TreeNode = {
id: `dir-${part}-${Date.now()}-${Math.random()}`,
name: part,
path: pathParts.slice(0, i+1).join('/'),
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,
path: file.path,
type: 'file'
})
})
return root.children || []
}
return {
templates,
loadTemplates,