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:
1 parent
742eb7040f
commit
1aecd5f4b6
15 files changed
+1225
-140
No files matched your search
@@ -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 }
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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, '..')
|
||||
|
||||
@@ -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[] = []
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "改变主题",
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
Reference in new issue
Block a user