995 lines
29 KiB
Vue
995 lines
29 KiB
Vue
<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 } 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'
|
||
|
||
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 directories = ref<{ path: string }[]>([])
|
||
const files = ref<{ path: string; content?: string; sourceFilePath?: string }[]>([])
|
||
|
||
// 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 || []
|
||
})
|
||
|
||
// Validation rules
|
||
const requiredRule = (v: string | null | undefined) => !!v || '此字段为必填项'
|
||
|
||
// Watch for changes to detect unsaved changes
|
||
watch(
|
||
[templateName, templateDescription, directories, files],
|
||
() => {
|
||
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
|
||
directories.value = [...template.directories]
|
||
files.value = [...template.files]
|
||
hasChanges.value = false
|
||
} else {
|
||
router.push('/templates')
|
||
}
|
||
}
|
||
|
||
// Expand all nodes by default
|
||
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 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)
|
||
}
|
||
} else {
|
||
// Check for duplicates only when creating a new directory
|
||
const exists = directories.value.some(
|
||
(d) => d.path === fullPath
|
||
)
|
||
if (exists) {
|
||
showSnackbar('该目录路径已存在!')
|
||
return
|
||
}
|
||
|
||
directories.value.push({
|
||
path: fullPath
|
||
})
|
||
}
|
||
|
||
showDirectoryDialog.value = false
|
||
newDirectoryPath.value = ''
|
||
parentDirectoryForNewItems.value = null // Clear parent context after creation
|
||
currentEditingItem.value = null
|
||
|
||
// If we added to a parent directory, make sure it's expanded
|
||
if (parentDirectoryForNewItems.value) {
|
||
if (!expandedNodes.value.includes(`dir-${parentDirectoryForNewItems.value}`)) {
|
||
expandedNodes.value.push(`dir-${parentDirectoryForNewItems.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)
|
||
}
|
||
}
|
||
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
|
||
})
|
||
}
|
||
|
||
// 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
|
||
}
|
||
|
||
// 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
|
||
}
|
||
}
|
||
} else {
|
||
// Check for duplicates only when creating a new file
|
||
const exists = files.value.some((f) => f.path === fullPath)
|
||
if (exists) {
|
||
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
|
||
const duplicate = files.value.some(f => f.path === fullTargetPath)
|
||
if (!duplicate) {
|
||
const newFile = {
|
||
path: fullTargetPath,
|
||
sourceFilePath: foundFile
|
||
}
|
||
files.value.push(newFile)
|
||
}
|
||
}
|
||
|
||
// Reset selection state
|
||
selectedSourcePath.value = ''
|
||
isAutoFindMode.value = false
|
||
|
||
showFileDialog.value = false
|
||
newFilePath.value = ''
|
||
parentDirectoryForNewItems.value = null // Clear parent context after creation
|
||
currentEditingItem.value = null
|
||
return
|
||
}
|
||
|
||
// If we have a selected file, just add it
|
||
if (selectedSourcePath.value) {
|
||
const newFile = {
|
||
path: fullPath,
|
||
sourceFilePath: selectedSourcePath.value
|
||
}
|
||
|
||
files.value.push(newFile)
|
||
|
||
// Reset selection state
|
||
selectedSourcePath.value = ''
|
||
isAutoFindMode.value = false
|
||
|
||
showFileDialog.value = false
|
||
newFilePath.value = ''
|
||
parentDirectoryForNewItems.value = null // Clear parent context after creation
|
||
currentEditingItem.value = null
|
||
return
|
||
}
|
||
|
||
// Otherwise, show file selection dialog
|
||
showSnackbar('请先选择一个文件或文件夹', 'warning')
|
||
return
|
||
}
|
||
|
||
// Reset selection state after editing
|
||
selectedSourcePath.value = ''
|
||
isAutoFindMode.value = false
|
||
|
||
showFileDialog.value = false
|
||
newFilePath.value = ''
|
||
parentDirectoryForNewItems.value = null
|
||
currentEditingItem.value = null
|
||
}
|
||
|
||
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) => {
|
||
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)
|
||
} 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')
|
||
}
|
||
}
|
||
}
|
||
|
||
// Show dialog to edit directory
|
||
const showEditDirectoryDialog = (directory: { path: string }) => {
|
||
// Set up for editing an existing directory
|
||
currentEditingItem.value = { ...directory, type: '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 }) => {
|
||
// Set up for editing an existing file
|
||
currentEditingItem.value = { ...file, type: '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<{ path: string; type: 'directory' | 'file'; content?: string; sourceFilePath?: string } | null>(null)
|
||
|
||
const deleteItem = async (item: TreeItem) => {
|
||
const confirmed = await ConfirmDialogService.confirm({
|
||
title: '删除确认',
|
||
message: '确定要删除此项吗?',
|
||
confirmText: '删除',
|
||
cancelText: '取消',
|
||
confirmColor: 'error'
|
||
})
|
||
|
||
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);
|
||
}
|
||
}
|
||
|
||
// 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') => {
|
||
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,
|
||
files: files.value,
|
||
directories: directories.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>
|