Files
gdt-tool/src/renderer/screens/CreateTemplateScreen.vue
T

1133 lines
32 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<v-container
fluid
class="pa-0 fill-height"
>
<v-row class="fill-height">
<!-- Left Panel - Template Info -->
<v-col
cols="12"
md="4"
class="pa-4 border-e"
>
<div class="d-flex align-center mb-4">
<h2 class="text-h5 mr-2">
{{ isEditMode ? '编辑模板' : '创建模板' }}
</h2>
<v-spacer />
<v-btn
icon
variant="text"
@click="goBack"
>
<v-icon>mdi-close</v-icon>
<v-tooltip
activator="parent"
location="bottom"
>
关闭
</v-tooltip>
</v-btn>
</div>
<v-form
v-model="formValid"
class="mb-4"
>
<v-text-field
v-model="templateName"
label="模板名称 *"
placeholder="输入模板名称"
:rules="[requiredRule]"
required
clearable
density="comfortable"
/>
<v-textarea
v-model="templateDescription"
label="模板描述"
placeholder="描述这个模板的用途..."
rows="2"
auto-grow
clearable
density="comfortable"
/>
</v-form>
<v-divider class="my-4" />
<!-- Template Actions -->
<div class="d-flex ga-2">
<v-btn
color="primary"
@click="saveTemplate"
:disabled="!formValid || !hasChanges"
prepend-icon="mdi-content-save"
block
>
保存模板
</v-btn>
</div>
</v-col>
<!-- Right Panel - Tree Structure -->
<v-col
cols="12"
md="8"
class="pa-4"
>
<div class="d-flex align-center mb-4">
<h3 class="text-h6 mr-2">模板结构</h3>
<v-spacer />
<v-btn
icon
variant="text"
@click="addRootDirectory"
size="small"
>
<v-icon>mdi-folder-plus</v-icon>
<v-tooltip
activator="parent"
location="bottom"
>
添加根目录
</v-tooltip>
</v-btn>
<v-btn
icon
variant="text"
@click="addRootFile"
size="small"
>
<v-icon>mdi-file-plus</v-icon>
<v-tooltip
activator="parent"
location="bottom"
>
添加根文件
</v-tooltip>
</v-btn>
<v-btn
icon
variant="text"
@click="expandAll"
size="small"
>
<v-icon>mdi-arrow-expand-down</v-icon>
<v-tooltip
activator="parent"
location="bottom"
>
展开全部
</v-tooltip>
</v-btn>
<v-btn
icon
variant="text"
@click="collapseAll"
size="small"
>
<v-icon>mdi-arrow-collapse-up</v-icon>
<v-tooltip
activator="parent"
location="bottom"
>
收起全部
</v-tooltip>
</v-btn>
</div>
<TemplateTree
:items="treeItems"
:height="treeHeight"
v-model:expanded="expandedNodes"
:empty-text="'暂无文件或目录'"
:read-only="false"
@add-new-item-to-current-directory="addNewItemToCurrentDirectory"
@edit-file="editFile"
@edit-item="editItem"
@delete-item="deleteItem"
@add-file-to-directory="addFileToDirectory"
/>
</v-col>
</v-row>
<!-- Add Directory Dialog -->
<v-dialog
v-model="showDirectoryDialog"
max-width="500"
>
<v-card>
<v-card-title>{{ currentEditingItem ? '编辑目录' : '添加目录' }}</v-card-title>
<v-card-text>
<v-form v-model="directoryFormValid">
<v-text-field
v-model="newDirectoryPath"
:label="currentEditingItem ? '新目录路径 *' : '目录路径 *'"
:placeholder="currentEditingItem ? '例如: src/new-components' : '例如: src/components'"
:rules="[requiredRule]"
required
density="comfortable"
ref="directoryInputRef"
@keydown.enter="confirmAddDirectory"
/>
</v-form>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn
@click="closeDirectoryDialog"
>
取消
</v-btn>
<v-btn
color="primary"
@click="confirmAddDirectory"
:disabled="!directoryFormValid"
>
{{ currentEditingItem ? '保存' : '添加' }}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- Add File Dialog -->
<v-dialog
v-model="showFileDialog"
max-width="600"
>
<v-card>
<v-card-title>{{ currentEditingItem ? '编辑文件' : '添加文件' }}</v-card-title>
<v-card-text>
<v-form v-model="fileFormValid">
<v-text-field
v-model="newFilePath"
:label="currentEditingItem ? '新文件路径 *' : '目标文件路径 *'"
:placeholder="currentEditingItem ? '例如: src/new-index.js' : '例如: src/index.js'"
:rules="[requiredRule]"
required
density="comfortable"
ref="fileInputRef"
@keydown.enter="selectAndAddFile"
/>
<div class="d-flex align-center my-4">
<v-btn
@click="selectSourceFile"
variant="outlined"
class="mr-2"
:disabled="!!currentEditingItem"
>
选择文件
</v-btn>
<v-btn
@click="selectSourceFolder"
variant="outlined"
class="mr-2"
:disabled="!!currentEditingItem"
>
选择文件夹
</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>
<!-- 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="closeFileDialog"
>
取消
</v-btn>
<v-btn
color="primary"
@click="selectAndAddFile"
:disabled="!fileFormValid || (!selectedSourcePath && !currentEditingItem)"
>
{{ currentEditingItem ? '保存' : '添加' }}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- File Content Editor Dialog -->
<v-dialog
v-model="showEditorDialog"
fullscreen
>
<v-card>
<v-toolbar
dark
color="primary"
>
<v-btn
icon
@click="showEditorDialog = false"
>
<v-icon>mdi-close</v-icon>
</v-btn>
<v-toolbar-title> 编辑文件: {{ editingFile?.path }} </v-toolbar-title>
<v-spacer />
<v-btn
variant="text"
@click="showEditorDialog = false"
>
关闭
</v-btn>
</v-toolbar>
<v-card-text class="pa-0 fill-height">
<v-textarea
v-model="editingFileContent"
class="fill-height"
style="height: calc(100vh - 120px)"
no-resize
spellcheck="false"
placeholder="在此输入文件内容..."
variant="solo-filled"
density="comfortable"
/>
</v-card-text>
</v-card>
</v-dialog>
<!-- Snackbar for error messages -->
<v-snackbar
v-model="snackbarVisible"
:color="snackbarColor"
:timeout="2000"
location="top"
>
{{ snackbarMessage }}
<template #actions>
<v-btn
icon
@click="snackbarVisible = false"
>
<v-icon>mdi-close</v-icon>
</v-btn>
</template>
</v-snackbar>
</v-container>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useTemplateStore, TreeNode } from '@/renderer/store/template'
import TemplateTree from '@/renderer/components/TemplateTree.vue'
import { ConfirmDialogService } from '@/renderer/utils/confirm-dialog-service'
const router = useRouter()
const route = useRoute()
const templateStore = useTemplateStore()
// Extract methods from store
const { getTemplateById } = templateStore
// Form state
const formValid = ref(false)
const templateName = ref('')
const templateDescription = ref('')
const hasChanges = ref(false)
// Dialogs
const showDirectoryDialog = ref(false)
const showFileDialog = ref(false)
const showEditorDialog = ref(false)
const directoryFormValid = ref(false)
const fileFormValid = ref(false)
// Input refs for auto-focus
const directoryInputRef = ref<HTMLInputElement | null>(null)
const fileInputRef = ref<HTMLInputElement | null>(null)
// Snackbar variables
const snackbarVisible = ref(false)
const snackbarMessage = ref('')
const snackbarColor = ref('error') // Can be 'error', 'success', 'warning', etc.
// New item state
const newDirectoryPath = ref('')
const newFilePath = ref('')
const editingFile = ref<{ path: string; content: string } | null>(null)
const editingFileContent = ref('')
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
const expandedNodes = ref<string[]>([])
const treeHeight = ref(600)
// Define type for tree items
interface TreeItem {
id: string
name: string
type: 'directory' | 'file'
children?: TreeItem[]
}
// Item state - 使用树状结构
const treeStructure = ref<TreeNode[]>([])
// Computed
const isEditMode = computed(() => !!route.params.id)
const treeItems = computed<TreeItem[]>(() => {
// 直接返回树状结构
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, treeStructure],
() => {
hasChanges.value = true
},
{ deep: true }
)
// Lifecycle
onMounted(async () => {
await templateStore.loadTemplates()
// Set tree height based on window height
updateTreeHeight()
// Add resize listener
window.addEventListener('resize', updateTreeHeight)
// Handle edit mode
if (isEditMode.value) {
const templateId = route.params.id as string
const template = getTemplateById(templateId)
if (template) {
templateName.value = template.name
templateDescription.value = template.description
treeStructure.value = [...template.treeStructure]
hasChanges.value = false
} else {
router.push('/templates')
}
}
// Expand all nodes by default
nextTick(() => {
expandAll()
})
})
onUnmounted(() => {
window.removeEventListener('resize', updateTreeHeight)
})
// Methods
const updateTreeHeight = () => {
treeHeight.value = window.innerHeight - 200
}
const addRootDirectory = () => {
newDirectoryPath.value = ''
showDirectoryDialog.value = true
autoFocusDirectoryInput()
}
const addRootFile = () => {
newFilePath.value = ''
showFileDialog.value = true
autoFocusFileInput()
}
const addDirectory = (parentItem: TreeItem | null = null) => {
parentDirectoryForNewItems.value = parentItem ? parentItem.name : null // Store parent context
newDirectoryPath.value = '' // Don't pre-fill the path
showDirectoryDialog.value = true
autoFocusDirectoryInput()
}
const addFile = (parentItem: TreeItem | null = null) => {
parentDirectoryForNewItems.value = parentItem ? parentItem.name : null // Store parent context
newFilePath.value = '' // Don't pre-fill the path
showFileDialog.value = true
autoFocusFileInput()
}
// Add a file directly to a specific directory
const addFileToDirectory = (directoryItem: TreeItem) => {
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) => {
parentDirectoryForNewItems.value = directoryItem.name // Store parent context
newDirectoryPath.value = '' // Don't pre-fill the path
showDirectoryDialog.value = true
autoFocusDirectoryInput()
}
const confirmAddDirectory = () => {
if (!newDirectoryPath.value.trim()) return
// Construct the full path if there's a parent directory
let fullPath = newDirectoryPath.value
if (parentDirectoryForNewItems.value) {
// Make sure parent directory ends with a slash before appending
const parentPath = parentDirectoryForNewItems.value.endsWith('/')
? parentDirectoryForNewItems.value
: parentDirectoryForNewItems.value + '/'
fullPath = parentPath + newDirectoryPath.value
}
// Check for duplicate names in the same parent directory
if (parentDirectoryForNewItems.value) {
// In the parent directory, check if a node with the same name already exists
const parentNode = findNodeInTreeByName(treeStructure.value, parentDirectoryForNewItems.value);
if (parentNode && parentNode.children) {
const duplicateExists = parentNode.children.some(child =>
child.name === newDirectoryPath.value
);
if (duplicateExists) {
showSnackbar('当前目录下已存在同名节点!', 'error');
return;
}
}
} else {
// Check at root level for duplicates
const duplicateExists = treeStructure.value.some(node =>
node.name === newDirectoryPath.value
);
if (duplicateExists) {
showSnackbar('根目录下已存在同名节点!', 'error');
return;
}
}
const newId = `dir-${Date.now()}-${Math.random()}`
// Check if we're editing an existing directory
if (currentEditingItem.value && currentEditingItem.value.type === 'directory') {
// 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 {
// Create new directory node
const newNode: TreeNode = {
id: newId,
name: newDirectoryPath.value,
path: fullPath,
type: 'directory'
}
// 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
parentDirectoryForNewItems.value = null // Clear parent context after creation
currentEditingItem.value = null
// After adding a new directory, expand all nodes to show the new structure
nextTick(() => {
expandAll()
})
newDirectoryPath.value = ''
}
// 在树状结构中更新节点的辅助函数
const updateNodeInTree = (nodes: TreeNode[], oldPath: string, updatedNode: TreeNode): TreeNode[] => {
return nodes.map(node => {
if (node.path === oldPath) {
return updatedNode;
}
if (node.children) {
return {
...node,
children: updateNodeInTree(node.children, oldPath, updatedNode)
};
}
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
newDirectoryPath.value = ''
parentDirectoryForNewItems.value = null
currentEditingItem.value = null
}
const closeFileDialog = () => {
showFileDialog.value = false
newFilePath.value = ''
parentDirectoryForNewItems.value = null
currentEditingItem.value = null
selectedSourcePath.value = ''
isAutoFindMode.value = false
}
const selectAndAddFile = async () => {
if (!newFilePath.value.trim()) return
// Construct the full path if there's a parent directory
let fullPath = newFilePath.value
if (parentDirectoryForNewItems.value) {
// Make sure parent directory ends with a slash before appending
const parentPath = parentDirectoryForNewItems.value.endsWith('/')
? parentDirectoryForNewItems.value
: parentDirectoryForNewItems.value + '/'
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') {
// 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 duplicate names in the same parent directory
if (parentDirectoryForNewItems.value) {
// In the parent directory, check if a node with the same name already exists
const parentNode = findNodeInTreeByName(treeStructure.value, parentDirectoryForNewItems.value);
if (parentNode && parentNode.children) {
const duplicateExists = parentNode.children.some(child =>
child.name === newFilePath.value
);
if (duplicateExists) {
showSnackbar('当前目录下已存在同名节点!', 'error');
return;
}
}
} else {
// Check at root level for duplicates
const duplicateExists = treeStructure.value.some(node =>
node.name === newFilePath.value
);
if (duplicateExists) {
showSnackbar('根目录下已存在同名节点!', 'error');
return;
}
}
// Also check for duplicates using the full path (for backward compatibility)
const hasDuplicate = checkForDuplicateInTree(treeStructure.value, fullPath);
if (hasDuplicate) {
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 by full path
const hasDuplicateInTree = checkForDuplicateInTree(treeStructure.value, fullTargetPath);
if (!hasDuplicateInTree) {
const fileName = fullTargetPath.split('/').pop() || fullTargetPath;
// Additionally, check for duplicate names in the same parent directory
let duplicateNameExists = false;
if (parentDirectoryForNewItems.value) {
// In the parent directory, check if a node with the same name already exists
const parentNode = findNodeInTreeByName(treeStructure.value, parentDirectoryForNewItems.value);
if (parentNode && parentNode.children) {
duplicateNameExists = parentNode.children.some(child =>
child.name === fileName
);
}
} else {
// Check at root level for duplicates
duplicateNameExists = treeStructure.value.some(node =>
node.name === fileName
);
}
if (duplicateNameExists) {
showSnackbar(`当前目录下已存在同名节点:${fileName}`, 'error');
} else {
const newFileNode: TreeNode = {
id: `file-${Date.now()}-${Math.random()}`,
name: fileName,
path: fullTargetPath,
type: 'file',
sourceFilePath: foundFile
}
// 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 = ''
isAutoFindMode.value = false
showFileDialog.value = false
parentDirectoryForNewItems.value = null // Clear parent context after creation
currentEditingItem.value = null
// After adding new files, expand all nodes to show the new structure
nextTick(() => {
expandAll()
})
newFilePath.value = ''
return
}
// If we have a selected file, just add it
if (selectedSourcePath.value) {
// Check for duplicate names in the same parent directory
let duplicateNameExists = false;
if (parentDirectoryForNewItems.value) {
// In the parent directory, check if a node with the same name already exists
const parentNode = findNodeInTreeByName(treeStructure.value, parentDirectoryForNewItems.value);
if (parentNode && parentNode.children) {
duplicateNameExists = parentNode.children.some(child =>
child.name === newFilePath.value
);
}
} else {
// Check at root level for duplicates
duplicateNameExists = treeStructure.value.some(node =>
node.name === newFilePath.value
);
}
if (duplicateNameExists) {
showSnackbar('当前目录下已存在同名节点!', 'error');
} else {
const newFileNode: TreeNode = {
id: newId,
name: newFilePath.value,
path: fullPath,
type: 'file',
sourceFilePath: selectedSourcePath.value
}
// 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 = ''
isAutoFindMode.value = false
showFileDialog.value = false
parentDirectoryForNewItems.value = null // Clear parent context after creation
currentEditingItem.value = null
// After adding a new file, expand all nodes to show the new structure
nextTick(() => {
expandAll()
})
newFilePath.value = ''
return
}
// Otherwise, show file selection dialog
showSnackbar('请先选择一个文件或文件夹', 'warning')
return
}
// Reset selection state after editing
selectedSourcePath.value = ''
isAutoFindMode.value = false
showFileDialog.value = false
parentDirectoryForNewItems.value = null
currentEditingItem.value = null
// After adding a new file, expand all nodes to show the new structure
nextTick(() => {
expandAll()
})
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({
title: '确认离开',
message: '您有未保存的更改,确定要离开吗?',
confirmText: '离开',
cancelText: '取消',
confirmColor: 'warning'
})
if (confirmed) {
router.push('/templates')
}
} else {
router.push('/templates')
}
}
const expandAll = () => {
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 = () => {
expandedNodes.value = []
}
const editFile = (item: TreeItem) => {
// For now, just show a message that files are selected, not edited
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) => {
// 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 {
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;
}
// 通过节点名称查找节点的辅助函数
const findNodeInTreeByName = (nodes: TreeNode[], nodeName: string): TreeNode | null => {
for (const node of nodes) {
if (node.name === nodeName) {
return node;
}
if (node.children) {
const found = findNodeInTreeByName(node.children, nodeName);
if (found) {
return found;
}
}
}
return null;
}
// 在指定节点中通过名称查找子节点的辅助函数
const findChildNodeByName = (parentNode: TreeNode, childName: string): TreeNode | null => {
if (parentNode.children) {
for (const child of parentNode.children) {
if (child.name === childName) {
return child;
}
}
}
return null;
}
// Show dialog to edit directory
const showEditDirectoryDialog = (directory: TreeNode) => {
// Set up for editing an existing 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: TreeNode) => {
// Set up for editing an existing 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
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<TreeNode | null>(null)
const deleteItem = async (item: TreeItem) => {
const confirmed = await ConfirmDialogService.confirm({
title: '删除确认',
message: '确定要删除此项吗?',
confirmText: '删除',
cancelText: '取消',
confirmColor: 'error'
})
if (!confirmed) return
// 从树状结构中删除项目
treeStructure.value = removeItemFromTree(treeStructure.value, item.id);
}
// 从树状结构中删除项目的辅助函数
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
snackbarColor.value = color
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
const autoFocusDirectoryInput = async () => {
await nextTick()
if (directoryInputRef.value) {
;(directoryInputRef.value as HTMLInputElement).focus()
}
}
const autoFocusFileInput = async () => {
await nextTick()
if (fileInputRef.value) {
;(fileInputRef.value as HTMLInputElement).focus()
}
}
const saveTemplate = () => {
if (!formValid.value) return
const templateData = {
name: templateName.value,
description: templateDescription.value,
treeStructure: treeStructure.value
}
if (isEditMode.value) {
const templateId = route.params.id as string
templateStore.updateTemplate({
id: templateId,
...templateData
})
} else {
templateStore.addTemplate(templateData)
}
hasChanges.value = false
router.push('/templates')
}
</script>
<style scoped>
.border-e {
border-right: 1px solid rgba(0, 0, 0, 0.12);
}
.fill-height {
height: 100%;
}
.cursor-pointer {
cursor: pointer;
}
</style>