1104 lines
31 KiB
Vue
1104 lines
31 KiB
Vue
<template>
|
|
<v-container
|
|
fluid
|
|
class="pa-0 fill-height"
|
|
>
|
|
<v-row class="fill-height">
|
|
<!-- Left Panel - Template Info -->
|
|
<v-col
|
|
cols="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="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="'目录名称'"
|
|
:placeholder="'请输入目录名称'"
|
|
: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="'文件名'"
|
|
:placeholder="'文件名需要带上后缀,如:style skech.doc'"
|
|
:rules="[requiredRule('文件名')]"
|
|
required
|
|
density="comfortable"
|
|
ref="fileInputRef"
|
|
@keydown.enter="selectAndAddFile"
|
|
/>
|
|
<v-file-input webkitdirectory directory label="选择文件或文件夹" density="comfortable"></v-file-input>
|
|
|
|
<div class="d-flex align-center my-4">
|
|
<v-btn
|
|
@click="selectSourceFileOrFolder"
|
|
variant="outlined"
|
|
class="mr-2"
|
|
:disabled="!!currentEditingItem"
|
|
>
|
|
选择文件或文件夹
|
|
</v-btn>
|
|
</div>
|
|
|
|
<!-- 选择提示 -->
|
|
<v-alert
|
|
type="info"
|
|
variant="tonal"
|
|
class="mt-2"
|
|
>
|
|
<div class="d-flex align-start flex-column">
|
|
<span>选择文件会直接复制到目标位置</span>
|
|
<span>选择文件夹则会自动匹配该文件夹下的文件进行复制并且重命名</span>
|
|
</div>
|
|
</v-alert>
|
|
|
|
<!-- 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>
|
|
</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 = (fieldName: string) => (v: string | null | undefined) => !!v || `${fieldName}为必填项`
|
|
|
|
// 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 selectSourceFileOrFolder = async () => {
|
|
// 使用现有的主进程API来选择文件或文件夹
|
|
const result = await window.mainApi.invoke('msgSelectFileOrFolder')
|
|
|
|
if (result && result.filePath) {
|
|
selectedSourcePath.value = result.filePath
|
|
isAutoFindMode.value = result.isDirectory // 如果是目录,则启用自动查找模式
|
|
}
|
|
}
|
|
|
|
// 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>
|