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

995 lines
29 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 } 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>