feat: add @mdi/font and electron-devtools-installer dependencies

- Added @mdi/font for Material Design Icons support.
- Included electron-devtools-installer to facilitate Vue DevTools installation in development.

refactor: enhance CreateTemplateScreen layout and functionality

- Improved layout with a responsive design.
- Added functionality for adding directories and files with validation.
- Implemented a file content editor dialog for editing file contents.
- Updated template save logic to handle changes more effectively.

fix: update TemplateListScreen for better UX

- Enhanced template list display with improved card layout.
- Added action buttons for viewing, executing, editing, and deleting templates.
- Improved dialog handling for template details and execution.

chore: update vuetify plugin configuration

- Removed unused icon set configuration.
- Updated locale settings to use simplified Chinese.

style: clean up HeaderLayout and vuetify styles

- Removed commented-out code and unnecessary styles.
- Improved overall styling for better readability and consistency.
This commit is contained in:
zhanggy committed 2025-10-27 18:19:39 +08:00
1 parent 1aecd5f4b6
commit 4f859a4933
8 files changed
+653 -394

No files matched your search

+469 -246
View File
@@ -1,334 +1,557 @@
<template>
<v-container>
<v-row>
<v-col cols="12">
<h2 class="mb-4">
{{ templateId ? '编辑模板' : '创建模板' }}
</h2>
<v-form v-model="formValid">
<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="template.name"
v-model="templateName"
label="模板名称 *"
:rules="[(v) => !!v || '模板名称不能为空']"
placeholder="输入模板名称"
:rules="[requiredRule]"
required
data-testid="template-name-input"
clearable
density="comfortable"
/>
<v-textarea
v-model="template.description"
v-model="templateDescription"
label="模板描述"
placeholder="描述这个模板的用途..."
rows="2"
data-testid="template-description-input"
auto-grow
clearable
density="comfortable"
/>
<!-- Template content -->
<v-card class="mb-4">
<v-card-title>模板内容</v-card-title>
<v-card-text>
<!-- Files section -->
<v-expansion-panels class="mb-4">
<v-expansion-panel>
<v-expansion-panel-title>文件</v-expansion-panel-title>
<v-expansion-panel-text>
<v-data-table
:headers="fileHeaders"
:items="template.files"
class="elevation-1"
>
<template v-slot:item.actions="{ item }">
<v-btn
icon
color="error"
@click="removeFile(item)"
>
<v-icon>mdi-delete</v-icon>
</v-btn>
</template>
</v-data-table>
<v-row class="mt-4">
<v-col cols="12">
<v-btn
color="primary"
@click="showAddFileDialog = true"
prepend-icon="mdi-file-plus"
>
添加文件
</v-btn>
</v-col>
</v-row>
</v-expansion-panel-text>
</v-expansion-panel>
<!-- Directories section -->
<v-expansion-panel>
<v-expansion-panel-title>目录</v-expansion-panel-title>
<v-expansion-panel-text>
<v-data-table
:headers="dirHeaders"
:items="template.directories"
class="elevation-1"
>
<template v-slot:item.actions="{ item }">
<v-btn
icon
color="error"
@click="removeDirectory(item)"
>
<v-icon>mdi-delete</v-icon>
</v-btn>
</template>
</v-data-table>
<v-row class="mt-4">
<v-col cols="12">
<v-btn
color="primary"
@click="showAddDirDialog = true"
prepend-icon="mdi-folder-plus"
>
添加目录
</v-btn>
</v-col>
</v-row>
</v-expansion-panel-text>
</v-expansion-panel>
</v-expansion-panels>
</v-card-text>
</v-card>
<v-btn
:color="templateId ? 'warning' : 'success'"
@click="saveTemplate"
:disabled="!formValid"
prepend-icon="mdi-content-save"
>
{{ templateId ? '更新模板' : '保存模板' }}
</v-btn>
<v-btn
class="ml-2"
@click="cancel"
prepend-icon="mdi-arrow-left"
>
返回
</v-btn>
</v-form>
<v-divider class="my-4" />
<!-- File Operations -->
<div class="mb-4">
<h3 class="text-subtitle-1 mb-2">添加项</h3>
<div class="d-flex flex-column ga-2">
<v-btn
color="primary"
variant="outlined"
prepend-icon="mdi-folder-plus"
@click="showAddDirectoryDialog"
block
>
添加目录
</v-btn>
<v-btn
color="success"
variant="outlined"
prepend-icon="mdi-file-plus"
@click="showAddFileDialog"
block
>
添加文件
</v-btn>
</div>
</div>
<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="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>
<v-card
variant="outlined"
class="pa-2"
:height="treeHeight"
>
<v-treeview
v-model:expanded="expandedNodes"
:items="treeItems"
item-title="name"
item-key="id"
item-children="children"
activatable
open-on-click
:return-object="false"
>
<template v-slot:prepend="{ item }">
<v-icon
:color="item.type === 'directory' ? 'warning' : 'info'"
size="large"
>
{{
item.type === 'directory'
? 'mdi-folder-outline'
: 'mdi-file-document-outline'
}}
</v-icon>
</template>
<template v-slot:append="{ item }">
<div class="d-flex align-center ga-1">
<v-btn
v-if="item.type === 'file'"
icon
variant="text"
size="small"
@click.stop="editFile(item)"
>
<v-icon size="small">mdi-pencil</v-icon>
<v-tooltip
activator="parent"
location="top"
>
编辑内容
</v-tooltip>
</v-btn>
<v-btn
icon
variant="text"
size="small"
@click.stop="deleteItem(item)"
>
<v-icon
size="small"
color="error"
>
mdi-delete
</v-icon>
<v-tooltip
activator="parent"
location="top"
>
删除
</v-tooltip>
</v-btn>
</div>
</template>
<template v-slot:no-data>
<div class="text-center pa-4">
<v-icon size="48" color="grey-lighten-1">mdi-folder-open-outline</v-icon>
<p class="text-body-2 mt-2">暂无文件或目录</p>
<p class="text-caption">点击左侧"添加目录"或"添加文件"开始创建</p>
</div>
</template>
</v-treeview>
</v-card>
</v-col>
</v-row>
<!-- Add file dialog -->
<v-dialog
v-model="showAddFileDialog"
max-width="600"
>
<!-- Add Directory Dialog -->
<v-dialog v-model="showDirectoryDialog" max-width="500">
<v-card>
<v-card-title>添加文件</v-card-title>
<v-card-title>添加目录</v-card-title>
<v-card-text>
<v-form v-model="fileFormValid">
<v-form v-model="directoryFormValid">
<v-text-field
v-model="newFile.path"
label="文件路径 *"
placeholder="例如: src/index.js"
:rules="[(v) => !!v || '文件路径不能为空']"
data-testid="add-file-path-input"
/>
<v-textarea
v-model="newFile.content"
label="文件内容"
placeholder="输入文件内容..."
rows="6"
data-testid="add-file-content-input"
v-model="newDirectoryPath"
label="目录路径 *"
placeholder="例如: src/components"
:rules="[requiredRule]"
required
density="comfortable"
/>
</v-form>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn @click="showAddFileDialog = false"> 取消 </v-btn>
<v-btn @click="showDirectoryDialog = false">
取消
</v-btn>
<v-btn
color="primary"
@click="addFile"
:disabled="!fileFormValid"
@click="addDirectory"
:disabled="!directoryFormValid"
>
添加
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- Add directory dialog -->
<v-dialog
v-model="showAddDirDialog"
max-width="600"
>
<!-- Add File Dialog -->
<v-dialog v-model="showFileDialog" max-width="500">
<v-card>
<v-card-title>添加目录</v-card-title>
<v-card-title>添加文件</v-card-title>
<v-card-text>
<v-form v-model="dirFormValid">
<v-form v-model="fileFormValid">
<v-text-field
v-model="newDir.path"
label="目录路径 *"
placeholder="例如: src/components"
:rules="[(v) => !!v || '目录路径不能为空']"
data-testid="add-dir-path-input"
v-model="newFilePath"
label="文件路径 *"
placeholder="例如: src/index.js"
:rules="[requiredRule]"
required
density="comfortable"
/>
</v-form>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn @click="showAddDirDialog = false"> 取消 </v-btn>
<v-btn @click="showFileDialog = false">
取消
</v-btn>
<v-btn
color="primary"
@click="addDirectory"
:disabled="!dirFormValid"
@click="addFile"
:disabled="!fileFormValid"
>
添加
添加并编辑
</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="saveFileContent"
>
保存
</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>
</v-container>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useTemplateStore } from '@/renderer/store/template'
import { Template } from '@/renderer/store/template'
const router = useRouter()
const route = useRoute()
const templateStore = useTemplateStore()
// Form validation
const formValid = ref(false)
const fileFormValid = ref(false)
const dirFormValid = ref(false)
// Extract methods from store
const { getTemplateById } = templateStore
// Template data
const templateId = ref(route.params.id as string)
const template = ref({
id: '',
name: '',
description: '',
files: [] as any[],
directories: [] as any[]
})
// Form state
const formValid = ref(false)
const templateName = ref('')
const templateDescription = ref('')
const hasChanges = ref(false)
// Dialogs
const showAddFileDialog = ref(false)
const showAddDirDialog = ref(false)
const showDirectoryDialog = ref(false)
const showFileDialog = ref(false)
const showEditorDialog = ref(false)
const directoryFormValid = ref(false)
const fileFormValid = ref(false)
// New file data
const newFile = ref({
path: '',
content: ''
// New item state
const newDirectoryPath = ref('')
const newFilePath = ref('')
const editingFile = ref<{ path: string; content: string } | null>(null)
const editingFileContent = ref('')
// Tree state
const expandedNodes = ref<string[]>([])
const treeHeight = ref(600)
// Item state
const directories = ref<{ path: string }[]>([])
const files = ref<{ path: string; content: string }[]>([])
// Computed
const isEditMode = computed(() => !!route.params.id)
const treeItems = computed(() => {
const items = []
// Add directories
directories.value.forEach(dir => {
items.push({
id: `dir-${dir.path}`,
name: dir.path,
type: 'directory',
children: []
})
})
// Add files
files.value.forEach(file => {
items.push({
id: `file-${file.path}`,
name: file.path,
type: 'file'
})
})
return items
})
// New directory data
const newDir = ref({
path: ''
})
// Validation rules
const requiredRule = (v: any) => !!v || '此字段为必填项'
// Table headers
const fileHeaders = ref([
{ title: '文件路径', key: 'path' },
{ title: '操作', key: 'actions', sortable: false }
])
const dirHeaders = ref([
{ title: '目录路径', key: 'path' },
{ title: '操作', key: 'actions', sortable: false }
])
// Watch for changes to detect unsaved changes
watch([templateName, templateDescription, directories, files], () => {
hasChanges.value = true
}, { deep: true })
// Lifecycle
onMounted(async () => {
await templateStore.loadTemplates()
if (templateId.value) {
// Edit existing template
const existingTemplate = templateStore.getTemplateById(templateId.value)
if (existingTemplate) {
template.value = { ...existingTemplate }
// 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 {
// If template doesn't exist, redirect to template list
router.push('/templates')
}
}
// Expand all nodes by default
expandAll()
})
onUnmounted(() => {
window.removeEventListener('resize', updateTreeHeight)
})
// Methods
const updateTreeHeight = () => {
treeHeight.value = window.innerHeight - 200
}
// Methods
const goBack = () => {
if (hasChanges.value) {
if (confirm('您有未保存的更改,确定要离开吗?')) {
router.push('/templates')
}
} else {
// New template
template.value = {
id: '',
name: '',
description: '',
files: [],
directories: []
}
router.push('/templates')
}
})
const addFile = () => {
if (!newFile.value.path.trim()) return
// Check for duplicate file paths
const exists = template.value.files.some((f) => f.path === newFile.value.path)
if (exists) {
alert('该文件路径已存在!')
return
}
template.value.files.push({
path: newFile.value.path,
content: newFile.value.content
})
newFile.value = { path: '', content: '' }
showAddFileDialog.value = false
}
const removeFile = (file: any) => {
template.value.files = template.value.files.filter((f) => f !== file)
const expandAll = () => {
expandedNodes.value = treeItems.value.map(item => item.id)
}
const collapseAll = () => {
expandedNodes.value = []
}
const showAddDirectoryDialog = () => {
newDirectoryPath.value = ''
showDirectoryDialog.value = true
}
const showAddFileDialog = () => {
newFilePath.value = ''
showFileDialog.value = true
}
const addDirectory = () => {
if (!newDir.value.path.trim()) return
// Check for duplicate directory paths
const exists = template.value.directories.some(
(d) => d.path === newDir.value.path
)
if (!newDirectoryPath.value.trim()) return
// Check for duplicates
const exists = directories.value.some(d => d.path === newDirectoryPath.value)
if (exists) {
alert('该目录路径已存在!')
return
}
template.value.directories.push({
path: newDir.value.path
directories.value.push({
path: newDirectoryPath.value
})
newDir.value = { path: '' }
showAddDirDialog.value = false
showDirectoryDialog.value = false
newDirectoryPath.value = ''
}
const removeDirectory = (dir: any) => {
template.value.directories = template.value.directories.filter(
(d) => d !== dir
)
const addFile = () => {
if (!newFilePath.value.trim()) return
// Check for duplicates
const exists = files.value.some(f => f.path === newFilePath.value)
if (exists) {
alert('该文件路径已存在!')
return
}
const newFile = {
path: newFilePath.value,
content: ''
}
files.value.push(newFile)
// Open editor for the new file
editFileContent(newFile)
showFileDialog.value = false
newFilePath.value = ''
}
const editFile = (item: any) => {
const file = files.value.find(f => `file-${f.path}` === item.id)
if (file) {
editFileContent(file)
}
}
const editFileContent = (file: { path: string; content: string }) => {
editingFile.value = file
editingFileContent.value = file.content
showEditorDialog.value = true
}
const saveFileContent = () => {
if (editingFile.value) {
const index = files.value.findIndex(f => f.path === editingFile.value?.path)
if (index !== -1) {
files.value[index].content = editingFileContent.value
}
}
showEditorDialog.value = false
}
const deleteItem = (item: any) => {
if (!confirm('确定要删除此项吗?')) return
if (item.type === 'directory') {
directories.value = directories.value.filter(d => `dir-${d.path}` !== item.id)
} else {
files.value = files.value.filter(f => `file-${f.path}` !== item.id)
}
}
const saveTemplate = () => {
if (!formValid.value) return
if (templateId.value) {
// Update existing template
templateStore.updateTemplate(template.value)
} else {
// Create new template
templateStore.addTemplate(template.value)
const templateData = {
name: templateName.value,
description: templateDescription.value,
files: files.value,
directories: directories.value
}
router.push('/templates')
}
const cancel = () => {
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%;
}
</style>