feat: Implement template management feature

- Added IPC handlers for selecting directories, loading, saving, and executing templates.
- Created a new TemplateListScreen for displaying and managing templates.
- Developed CreateTemplateScreen for creating and editing templates with file and directory management.
- Updated routing to include new template screens.
- Enhanced MainScreen with navigation to template management.
- Added localization for new template-related strings in English and Chinese.
- Introduced a Pinia store for managing template data.
- Implemented tests for template creation, listing, and detail viewing.
This commit is contained in:
zhangguoyun committed 2025-10-27 15:17:32 +08:00
1 parent 742eb7040f
commit 1aecd5f4b6
15 files changed
+1225 -140

No files matched your search

+123
View File
@@ -1,5 +1,12 @@
import { ipcMain, shell, IpcMainEvent, dialog } from 'electron'
import Constants from './utils/Constants'
import { join, dirname } from 'path'
import {
writeFileSync,
readFileSync,
existsSync,
ensureDirSync
} from 'fs-extra'
/*
* IPC Communications
@@ -36,5 +43,121 @@ export default class IPCs {
return dialogResult
}
)
// Select directory
ipcMain.handle('msgSelectDirectory', async () => {
const dialogResult = await dialog.showOpenDialog({
properties: ['openDirectory']
})
if (!dialogResult.canceled && dialogResult.filePaths.length > 0) {
return {
canceled: false,
filePath: dialogResult.filePaths[0]
}
}
return { canceled: true }
})
// Load data from local file
ipcMain.handle(
'msgLoadData',
async (event: IpcMainEvent, fileName: string) => {
try {
const userDataPath = join(Constants.APP_DATA_PATH, 'data')
const filePath = join(userDataPath, fileName)
// Ensure directory exists
ensureDirSync(userDataPath)
if (existsSync(filePath)) {
const data = readFileSync(filePath, 'utf-8')
return data
}
return null
} catch (error) {
console.error('Error loading data:', error)
return null
}
}
)
// Save data to local file
ipcMain.handle(
'msgSaveData',
async (event: IpcMainEvent, fileName: string, data: string) => {
try {
const userDataPath = join(Constants.APP_DATA_PATH, 'data')
const filePath = join(userDataPath, fileName)
// Ensure directory exists
ensureDirSync(userDataPath)
writeFileSync(filePath, data, 'utf-8')
return true
} catch (error) {
console.error('Error saving data:', error)
return false
}
}
)
// Execute template
ipcMain.handle(
'msgExecuteTemplate',
async (
event: IpcMainEvent,
templateId: string,
targetPath: string,
baseDirNames: string[]
) => {
try {
// Load templates data
const userDataPath = join(Constants.APP_DATA_PATH, 'data')
const filePath = join(userDataPath, 'templates.json')
let templates = []
if (existsSync(filePath)) {
const templatesData = readFileSync(filePath, 'utf-8')
templates = JSON.parse(templatesData)
}
const template = templates.find((t) => t.id === templateId)
if (!template) {
return { success: false, error: 'Template not found' }
}
// Create directories and files for each base directory name
for (const baseDirName of baseDirNames) {
if (!baseDirName.trim()) continue
// Create base directory
const baseDirPath = join(targetPath, baseDirName)
ensureDirSync(baseDirPath)
// Create directories from template
for (const dir of template.directories) {
const dirPath = join(baseDirPath, dir.path)
ensureDirSync(dirPath)
}
// Create files from template
for (const file of template.files) {
const filePath = join(baseDirPath, file.path)
const fileDir = dirname(filePath)
ensureDirSync(fileDir)
writeFileSync(filePath, file.content, 'utf-8')
}
}
return { success: true }
} catch (error) {
console.error('Error executing template:', error)
return { success: false, error: error.message }
}
}
)
}
}
+3
View File
@@ -1,6 +1,7 @@
import { join, dirname, resolve } from 'path'
import { name, version, debug } from '../../../package.json'
import { fileURLToPath } from 'url'
import { app } from 'electron'
const __dirname = dirname(fileURLToPath(import.meta.url))
@@ -34,6 +35,8 @@ export default class Constants {
static IS_DEV_ENV = process.env.NODE_ENV === 'development'
static APP_DATA_PATH = app.getPath('userData')
static PUBLIC_PATH = Constants.IS_DEV_ENV
? resolve(__dirname, '../../src/public')
: resolve(__dirname, '..')
+5 -1
View File
@@ -9,7 +9,11 @@ log.transports.console.format = '{h}:{i}:{s}.{ms} {text}'
const mainAvailChannels: string[] = [
'msgRequestGetVersion',
'msgOpenExternalLink',
'msgOpenFile'
'msgOpenFile',
'msgSelectDirectory',
'msgLoadData',
'msgSaveData',
'msgExecuteTemplate'
]
const rendererAvailChannels: string[] = []
+3 -1
View File
@@ -8,7 +8,9 @@
"title": {
"main": "Main Screen",
"second": "Second Screen",
"error": "Unknown Error"
"error": "Unknown Error",
"templates": "Template List",
"create-template": "Create Template"
},
"menu": {
"change-theme": "Change Theme",
+6 -4
View File
@@ -1,14 +1,16 @@
{
"desc": {
"welcome-title": "你好武特龙! 一切都准备好了。",
"welcome-desc": "您现在可以编写跨平台的 Web 应用程序。 实时查看更改并使用一个命令构建到多个平台。 如果您还不熟悉它,可以从下面的文档页面获得帮助,或者运行一些预先编写的示例代码。",
"welcome-title": "你好! 模板生成器",
"welcome-desc": "您现在可以创建和管理文件模板。 实时查看更改并使用模板批量生成文件和目录。",
"second-desc": "您已移至第二个屏幕! 这里什么都没有!",
"selected-file": "选定的文件:{filePath}."
},
"title": {
"main": "主屏幕",
"main": "模板生成器",
"second": "第二屏",
"error": "未知错误"
"error": "未知错误",
"templates": "模板列表",
"create-template": "创建模板"
},
"menu": {
"change-theme": "改变主题",
+13 -2
View File
@@ -6,9 +6,20 @@ export default createRouter({
routes: [
{
path: '/',
component: MainScreen,
redirect: '/templates'
},
{
path: '/templates',
component: () => import('@/renderer/screens/TemplateListScreen.vue'),
meta: {
titleKey: 'title.main'
titleKey: 'title.templates'
}
},
{
path: '/create-template',
component: () => import('@/renderer/screens/CreateTemplateScreen.vue'),
meta: {
titleKey: 'title.create-template'
}
},
{
@@ -0,0 +1,334 @@
<template>
<v-container>
<v-row>
<v-col cols="12">
<h2 class="mb-4">
{{ templateId ? '编辑模板' : '创建模板' }}
</h2>
<v-form v-model="formValid">
<v-text-field
v-model="template.name"
label="模板名称 *"
:rules="[(v) => !!v || '模板名称不能为空']"
required
data-testid="template-name-input"
/>
<v-textarea
v-model="template.description"
label="模板描述"
rows="2"
data-testid="template-description-input"
/>
<!-- 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-col>
</v-row>
<!-- Add file dialog -->
<v-dialog
v-model="showAddFileDialog"
max-width="600"
>
<v-card>
<v-card-title>添加文件</v-card-title>
<v-card-text>
<v-form v-model="fileFormValid">
<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-form>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn @click="showAddFileDialog = false"> 取消 </v-btn>
<v-btn
color="primary"
@click="addFile"
:disabled="!fileFormValid"
>
添加
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- Add directory dialog -->
<v-dialog
v-model="showAddDirDialog"
max-width="600"
>
<v-card>
<v-card-title>添加目录</v-card-title>
<v-card-text>
<v-form v-model="dirFormValid">
<v-text-field
v-model="newDir.path"
label="目录路径 *"
placeholder="例如: src/components"
:rules="[(v) => !!v || '目录路径不能为空']"
data-testid="add-dir-path-input"
/>
</v-form>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn @click="showAddDirDialog = false"> 取消 </v-btn>
<v-btn
color="primary"
@click="addDirectory"
:disabled="!dirFormValid"
>
添加
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</v-container>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useTemplateStore } 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)
// Template data
const templateId = ref(route.params.id as string)
const template = ref({
id: '',
name: '',
description: '',
files: [] as any[],
directories: [] as any[]
})
// Dialogs
const showAddFileDialog = ref(false)
const showAddDirDialog = ref(false)
// New file data
const newFile = ref({
path: '',
content: ''
})
// New directory data
const newDir = ref({
path: ''
})
// Table headers
const fileHeaders = ref([
{ title: '文件路径', key: 'path' },
{ title: '操作', key: 'actions', sortable: false }
])
const dirHeaders = ref([
{ title: '目录路径', key: 'path' },
{ title: '操作', key: 'actions', sortable: false }
])
onMounted(async () => {
await templateStore.loadTemplates()
if (templateId.value) {
// Edit existing template
const existingTemplate = templateStore.getTemplateById(templateId.value)
if (existingTemplate) {
template.value = { ...existingTemplate }
} else {
// If template doesn't exist, redirect to template list
router.push('/templates')
}
} else {
// New template
template.value = {
id: '',
name: '',
description: '',
files: [],
directories: []
}
}
})
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 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 (exists) {
alert('该目录路径已存在!')
return
}
template.value.directories.push({
path: newDir.value.path
})
newDir.value = { path: '' }
showAddDirDialog.value = false
}
const removeDirectory = (dir: any) => {
template.value.directories = template.value.directories.filter(
(d) => d !== dir
)
}
const saveTemplate = () => {
if (!formValid.value) return
if (templateId.value) {
// Update existing template
templateStore.updateTemplate(template.value)
} else {
// Create new template
templateStore.addTemplate(template.value)
}
router.push('/templates')
}
const cancel = () => {
router.push('/templates')
}
</script>
+133 -125
View File
@@ -3,16 +3,22 @@ import { useI18n } from 'vue-i18n'
import { useTheme } from 'vuetify'
import { openExternal, openFile } from '@/renderer/utils'
import { useCounterStore } from '@/renderer/store/counter'
import { onMounted, ref } from 'vue'
import { onMounted, ref, onBeforeMount } from 'vue'
import {
mdiBrightness6,
mdiFileDocument,
mdiFolderOpen,
mdiGithub,
mdiPlusCircle
mdiPlusCircle,
mdiFolderMultiple,
mdiFileDocumentMultiple,
mdiArrowRight,
mdiHome
} from '@mdi/js'
import { storeToRefs } from 'pinia'
import { useRouter } from 'vue-router'
const router = useRouter()
const { t, locale, availableLocales } = useI18n()
const { increaseCount } = useCounterStore()
const { count } = storeToRefs(useCounterStore())
@@ -60,140 +66,142 @@ const handleOpenFile = async () => {
selectedFile.value = dialogResult.filePaths[0]
}
}
const navigateToTemplates = () => {
router.push('/templates')
}
const navigateToCreateTemplate = () => {
router.push('/create-template')
}
</script>
<template>
<v-container>
<v-row
no-gutters
class="text-center align-center"
>
<v-row class="text-center mb-6">
<v-col cols="12">
<h1 class="text-h4 font-weight-bold mb-4">
{{ t('title.main') }}
</h1>
<p class="text-h6 mb-6">
{{ t('desc.welcome-desc') }}
</p>
</v-col>
</v-row>
<v-row justify="center" class="mb-6">
<v-col cols="12" md="8" lg="6">
<v-card class="pa-6 elevation-4">
<v-card-title class="text-h5 mb-4">
模板管理器
</v-card-title>
<v-card-text>
<p>创建、管理和使用文件模板来批量生成目录和文件结构</p>
<p class="mt-4">
App Version: <strong>{{ appVersion }}</strong>
</p>
<p v-if="selectedFile">
{{
t('desc.selected-file', {
filePath: selectedFile
})
}}
</p>
</v-card-text>
<v-card-actions class="justify-center">
<v-btn
color="primary"
size="x-large"
@click="navigateToCreateTemplate"
class="mr-4"
prepend-icon="mdi-plus"
>
创建模板
</v-btn>
<v-btn
color="success"
size="x-large"
@click="navigateToTemplates"
prepend-icon="mdi-folder-multiple"
>
管理模板
</v-btn>
</v-card-actions>
</v-card>
</v-col>
</v-row>
<v-row justify="center" class="mt-6">
<v-col cols="12" md="10">
<v-expansion-panels variant="accordion">
<v-expansion-panel>
<v-expansion-panel-title>
<v-icon start>{{ mdiFolderMultiple }}</v-icon>
如何使用模板生成器
</v-expansion-panel-title>
<v-expansion-panel-text>
<ol class="text-left">
<li><strong>创建模板:</strong>点击"创建模板"按钮,定义您需要的文件和目录结构</li>
<li><strong>管理模板:</strong>在模板列表中查看、编辑和删除已创建的模板</li>
<li><strong>执行模板:</strong>选择一个模板并指定基础目录名,批量生成目录结构</li>
</ol>
</v-expansion-panel-text>
</v-expansion-panel>
</v-expansion-panels>
</v-col>
</v-row>
<v-row class="mt-6">
<v-col
cols="12"
md="5"
md="6"
>
<v-img
data-testid="main-logo"
alt="logo"
draggable="false"
class="ma-auto h-auto w-sm-50 w-md-100"
src="images/vutron-logo.webp"
/>
<v-btn
icon
color="primary"
@click="handleChangeTheme"
>
<v-icon :icon="mdiBrightness6" />
<v-tooltip
activator="parent"
location="bottom"
>
{{ t('menu.change-theme') }}
</v-tooltip>
</v-btn>
<v-btn
color="primary"
@click="handleOpenDocument"
class="ml-2"
>
<v-icon :icon="mdiFileDocument" start />
{{ t('menu.documentation') }}
</v-btn>
<v-btn
color="primary"
@click="handleOpenGitHub"
class="ml-2"
>
<v-icon :icon="mdiGithub" start />
{{ t('menu.github') }}
</v-btn>
</v-col>
<v-col
cols="12"
md="7"
md="6"
class="d-flex justify-end"
>
<h2 class="my-4">
{{ t('desc.welcome-title') }}
</h2>
<p>{{ t('desc.welcome-desc') }}</p>
<p class="my-4">
App Version: <strong>{{ appVersion }}</strong>
</p>
<p v-if="selectedFile">
{{
t('desc.selected-file', {
filePath: selectedFile
})
}}
</p>
<v-row class="my-4">
<v-col>
<v-btn
icon
color="primary"
@click="handleChangeTheme"
>
<v-icon :icon="mdiBrightness6" />
<v-tooltip
activator="parent"
location="bottom"
>
{{ t('menu.change-theme') }}
</v-tooltip>
</v-btn>
</v-col>
<v-col>
<v-badge
data-testid="counter-badge"
color="blue"
:content="count"
>
<v-btn
data-testid="btn-counter"
icon
color="primary"
@click="handleCountIncrease"
>
<v-icon :icon="mdiPlusCircle" />
<v-tooltip
activator="parent"
location="bottom"
>
{{ t('menu.increase-count') }}
</v-tooltip>
</v-btn>
</v-badge>
</v-col>
<v-col>
<v-btn
icon
color="primary"
@click="handleOpenDocument"
>
<v-icon :icon="mdiFileDocument" />
<v-tooltip
activator="parent"
location="bottom"
>
{{ t('menu.documentation') }}
</v-tooltip>
</v-btn>
</v-col>
<v-col>
<v-btn
icon
color="primary"
@click="handleOpenGitHub"
>
<v-icon :icon="mdiGithub" />
<v-tooltip
activator="parent"
location="bottom"
>
{{ t('menu.github') }}
</v-tooltip>
</v-btn>
</v-col>
<v-col>
<v-btn
icon
color="primary"
@click="handleOpenFile"
>
<v-icon :icon="mdiFolderOpen" />
<v-tooltip
activator="parent"
location="bottom"
>
{{ t('menu.open-file') }}
</v-tooltip>
</v-btn>
</v-col>
<v-col cols="12">
<v-select
data-testid="select-language"
:model-value="locale"
density="compact"
:label="t('menu.change-language')"
:items="languages"
@update:model-value="handleChangeLanguage"
>
{{ t('menu.change-language') }}
</v-select>
</v-col>
</v-row>
<v-select
data-testid="select-language"
:model-value="locale"
density="compact"
:label="t('menu.change-language')"
:items="languages"
@update:model-value="handleChangeLanguage"
class="ml-2"
/>
</v-col>
</v-row>
</v-container>
+275
View File
@@ -0,0 +1,275 @@
<template>
<v-container>
<v-row>
<v-col cols="12">
<h2 class="mb-4"> 模板列表 </h2>
<!-- No templates message -->
<v-card
v-if="templates.length === 0"
class="pa-6 mb-4 text-center"
>
<v-card-title class="text-h5 mb-4">
暂无模板
</v-card-title>
<v-card-text>
<p>还没有创建任何模板。创建您的第一个模板以开始批量生成文件和目录。</p>
</v-card-text>
<v-card-actions class="justify-center">
<v-btn
color="primary"
size="x-large"
@click="navigateToCreateTemplate"
prepend-icon="mdi-plus"
>
创建模板
</v-btn>
</v-card-actions>
</v-card>
<!-- Template list -->
<div v-else>
<v-row>
<v-col
v-for="template in templates"
:key="template.id"
cols="12"
md="6"
lg="4"
>
<v-card
class="d-flex flex-column"
height="100%"
>
<v-card-title>{{ template.name }}</v-card-title>
<v-card-subtitle>{{ template.description }}</v-card-subtitle>
<v-card-text class="flex-grow-1">
<p>文件数量: {{ template.files.length }}</p>
<p>目录数量: {{ template.directories.length }}</p>
</v-card-text>
<v-card-actions>
<v-btn
color="primary"
variant="outlined"
@click="viewTemplate(template)"
>
查看
</v-btn>
<v-btn
color="success"
variant="outlined"
@click="executeTemplate(template)"
>
执行
</v-btn>
</v-card-actions>
</v-card>
</v-col>
</v-row>
</div>
</v-col>
</v-row>
<!-- Template detail dialog -->
<v-dialog
v-model="showTemplateDetail"
max-width="800"
>
<v-card>
<v-card-title>
<span>模板详情: {{ selectedTemplate?.name }}</span>
<v-spacer />
<v-btn
icon
@click="showTemplateDetail = false"
>
<v-icon>mdi-close</v-icon>
</v-btn>
</v-card-title>
<v-card-text>
<v-treeview
:items="templateTree"
item-title="name"
item-key="id"
item-children="children"
activatable
open-all
>
<template v-slot:prepend="{ item, open }">
<v-icon>
{{
item.type === 'directory'
? open
? 'mdi-folder-open'
: 'mdi-folder'
: 'mdi-file'
}}
</v-icon>
</template>
</v-treeview>
</v-card-text>
</v-card>
</v-dialog>
<!-- Execute template dialog -->
<v-dialog
v-model="showExecuteDialog"
max-width="600"
>
<v-card>
<v-card-title>执行模板</v-card-title>
<v-card-text>
<v-form v-model="executeFormValid">
<v-text-field
v-model="executePath"
label="选择目录"
readonly
@click="selectDirectory"
prepend-inner-icon="mdi-folder"
:rules="[(v) => !!v || '请选择目录']"
/>
<v-textarea
v-model="baseDirNames"
label="基础目录名 (每行一个,支持批量)"
placeholder="输入基础目录名,每行一个"
:rules="[(v) => !!v || '请输入基础目录名']"
/>
</v-form>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn @click="showExecuteDialog = false"> 取消 </v-btn>
<v-btn
color="success"
@click="confirmExecuteTemplate"
:disabled="!executeFormValid"
>
确认执行
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- Floating Action Button for creating templates -->
<v-btn
v-if="templates.length > 0"
color="primary"
size="x-large"
icon="mdi-plus"
fixed
location="bottom end"
class="mb-8 mr-8"
style="z-index: 1000;"
@click="navigateToCreateTemplate"
>
<v-tooltip
activator="parent"
location="top"
>
创建模板
</v-tooltip>
</v-btn>
</v-container>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useTemplateStore } from '@/renderer/store/template'
const router = useRouter()
const templateStore = useTemplateStore()
// Initialize templates
onMounted(async () => {
await templateStore.loadTemplates()
})
const templates = computed(() => templateStore.templates)
// Template detail view
const showTemplateDetail = ref(false)
const selectedTemplate = ref(null as any)
// Execute template
const showExecuteDialog = ref(false)
const executePath = ref('')
const baseDirNames = ref('')
const executeFormValid = ref(false)
const templateTree = computed(() => {
if (!selectedTemplate.value) return []
const items = []
// Add directories
selectedTemplate.value.directories.forEach((dir: any) => {
items.push({
id: `dir-${dir.path}`,
name: dir.path,
type: 'directory',
children: []
})
})
// Add files
selectedTemplate.value.files.forEach((file: any) => {
items.push({
id: `file-${file.path}`,
name: file.path,
type: 'file'
})
})
return items
})
const navigateToCreateTemplate = () => {
router.push('/create-template')
}
const viewTemplate = (template: any) => {
selectedTemplate.value = template
showTemplateDetail.value = true
}
const executeTemplate = (template: any) => {
selectedTemplate.value = template
executePath.value = ''
baseDirNames.value = ''
showExecuteDialog.value = true
}
const selectDirectory = async () => {
const result = await window.mainApi.invoke('msgSelectDirectory')
if (!result.canceled) {
executePath.value = result.filePath
}
}
const confirmExecuteTemplate = () => {
if (!executePath.value || !baseDirNames.value.trim()) return
const dirNames = baseDirNames.value.split('\n').filter((name) => name.trim())
// Call main process to execute template
window.mainApi
.invoke(
'msgExecuteTemplate',
selectedTemplate.value.id,
executePath.value,
dirNames
)
.then((result: any) => {
if (result.success) {
alert('模板执行成功!')
showExecuteDialog.value = false
} else {
alert(`模板执行失败: ${result.error}`)
}
})
.catch((error: any) => {
alert(`模板执行出错: ${error.message}`)
})
}
</script>
+9 -1
View File
@@ -1,5 +1,13 @@
import ErrorScreen from '@/renderer/screens/ErrorScreen.vue'
import MainScreen from '@/renderer/screens/MainScreen.vue'
import SecondScreen from '@/renderer/screens/SecondScreen.vue'
import TemplateListScreen from '@/renderer/screens/TemplateListScreen.vue'
import CreateTemplateScreen from '@/renderer/screens/CreateTemplateScreen.vue'
export { ErrorScreen, MainScreen, SecondScreen }
export {
ErrorScreen,
MainScreen,
SecondScreen,
TemplateListScreen,
CreateTemplateScreen
}
+107
View File
@@ -0,0 +1,107 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
interface TemplateFile {
path: string
content: string
}
interface TemplateDirectory {
path: string
}
export interface Template {
id: string
name: string
description: string
files: TemplateFile[]
directories: TemplateDirectory[]
}
export const useTemplateStore = defineStore('template', () => {
const templates = ref<Template[]>([])
const dataFile = 'templates.json'
// Load templates from local storage or initialize
async function loadTemplates() {
try {
const stored = await window.mainApi.invoke('msgLoadData', dataFile)
if (stored) {
templates.value = JSON.parse(stored as string) || []
} else {
// Initialize with sample data if no stored data
templates.value = []
}
} catch (error) {
console.error('Failed to load templates:', error)
templates.value = []
}
}
// Save templates to local storage
function saveTemplates() {
try {
window.mainApi.invoke(
'msgSaveData',
dataFile,
JSON.stringify(templates.value)
)
} catch (error) {
console.error('Failed to save templates:', error)
}
}
// Get all templates
function getTemplates() {
return templates.value
}
// Get template by ID
function getTemplateById(id: string) {
return templates.value.find((template) => template.id === id)
}
// Add new template
function addTemplate(template: Omit<Template, 'id'>) {
const newTemplate: Template = {
id: `template_${Date.now()}`,
...template
}
templates.value.push(newTemplate)
saveTemplates()
}
// Update existing template
function updateTemplate(updatedTemplate: Template) {
const index = templates.value.findIndex((t) => t.id === updatedTemplate.id)
if (index !== -1) {
templates.value[index] = updatedTemplate
saveTemplates()
}
}
// Delete template
function deleteTemplate(id: string) {
templates.value = templates.value.filter((template) => template.id !== id)
saveTemplates()
}
// Get template names for dropdowns
function getTemplateNames() {
return templates.value.map((template) => ({
text: template.name,
value: template.id
}))
}
return {
templates,
loadTemplates,
getTemplates,
getTemplateById,
addTemplate,
updateTemplate,
deleteTemplate,
getTemplateNames
}
})