From 1aecd5f4b6e1972aa22e6d5010e41a67be6ca7b6 Mon Sep 17 00:00:00 2001 From: zhangguoyun Date: Mon, 27 Oct 2025 15:17:32 +0800 Subject: [PATCH] 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. --- package.json | 2 + pnpm-lock.yaml | 42 ++- src/main/IPCs.ts | 123 +++++++ src/main/utils/Constants.ts | 3 + src/preload/index.ts | 6 +- src/renderer/locales/en.json | 4 +- src/renderer/locales/zh-hans.json | 10 +- src/renderer/router/index.ts | 15 +- src/renderer/screens/CreateTemplateScreen.vue | 334 ++++++++++++++++++ src/renderer/screens/MainScreen.vue | 258 +++++++------- src/renderer/screens/TemplateListScreen.vue | 275 ++++++++++++++ src/renderer/screens/index.ts | 10 +- src/renderer/store/template.ts | 107 ++++++ tests/specs/templateManagement.spec.ts | 174 +++++++++ vite.config.mts | 2 +- 15 files changed, 1225 insertions(+), 140 deletions(-) create mode 100644 src/renderer/screens/CreateTemplateScreen.vue create mode 100644 src/renderer/screens/TemplateListScreen.vue create mode 100644 src/renderer/store/template.ts create mode 100644 tests/specs/templateManagement.spec.ts diff --git a/package.json b/package.json index 608ab0c..960cef4 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ }, "dependencies": { "electron-log": "^5.4.3", + "fs-extra": "^11.3.2", "pinia": "^3.0.3", "qsu": "^1.10.2", "vue": "^3.5.22", @@ -53,6 +54,7 @@ "@eslint/js": "^9.37.0", "@mdi/js": "^7.4.47", "@playwright/test": "^1.56.1", + "@types/fs-extra": "^11.0.4", "@typescript-eslint/parser": "^8.46.1", "@vitejs/plugin-vue": "^6.0.1", "@vitejs/plugin-vue-jsx": "^5.1.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 95412c7..817c9c1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: electron-log: specifier: ^5.4.3 version: 5.4.3 + fs-extra: + specifier: ^11.3.2 + version: 11.3.2 pinia: specifier: ^3.0.3 version: 3.0.3(typescript@5.9.3)(vue@3.5.22(typescript@5.9.3)) @@ -36,6 +39,9 @@ importers: '@playwright/test': specifier: ^1.56.1 version: 1.56.1 + '@types/fs-extra': + specifier: ^11.0.4 + version: 11.0.4 '@typescript-eslint/parser': specifier: ^8.46.1 version: 8.46.1(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) @@ -644,56 +650,67 @@ packages: resolution: {integrity: sha512-HZZBXJL1udxlCVvoVadstgiU26seKkHbbAMLg7680gAcMnRNP9SAwTMVet02ANA94kXEI2VhBnXs4e5nf7KG2A==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.47.1': resolution: {integrity: sha512-sZ5p2I9UA7T950JmuZ3pgdKA6+RTBr+0FpK427ExW0t7n+QwYOcmDTK/aRlzoBrWyTpJNlS3kacgSlSTUg6P/Q==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.47.1': resolution: {integrity: sha512-3hBFoqPyU89Dyf1mQRXCdpc6qC6At3LV6jbbIOZd72jcx7xNk3aAp+EjzAtN6sDlmHFzsDJN5yeUySvorWeRXA==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.47.1': resolution: {integrity: sha512-49J4FnMHfGodJWPw73Ve+/hsPjZgcXQGkmqBGZFvltzBKRS+cvMiWNLadOMXKGnYRhs1ToTGM0sItKISoSGUNA==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loongarch64-gnu@4.47.1': resolution: {integrity: sha512-4yYU8p7AneEpQkRX03pbpLmE21z5JNys16F1BZBZg5fP9rIlb0TkeQjn5du5w4agConCCEoYIG57sNxjryHEGg==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-ppc64-gnu@4.47.1': resolution: {integrity: sha512-fAiq+J28l2YMWgC39jz/zPi2jqc0y3GSRo1yyxlBHt6UN0yYgnegHSRPa3pnHS5amT/efXQrm0ug5+aNEu9UuQ==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-gnu@4.47.1': resolution: {integrity: sha512-daoT0PMENNdjVYYU9xec30Y2prb1AbEIbb64sqkcQcSaR0zYuKkoPuhIztfxuqN82KYCKKrj+tQe4Gi7OSm1ow==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.47.1': resolution: {integrity: sha512-JNyXaAhWtdzfXu5pUcHAuNwGQKevR+6z/poYQKVW+pLaYOj9G1meYc57/1Xv2u4uTxfu9qEWmNTjv/H/EpAisw==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.47.1': resolution: {integrity: sha512-U/CHbqKSwEQyZXjCpY43/GLYcTVKEXeRHw0rMBJP7fP3x6WpYG4LTJWR3ic6TeYKX6ZK7mrhltP4ppolyVhLVQ==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.47.1': resolution: {integrity: sha512-uTLEakjxOTElfeZIGWkC34u2auLHB1AYS6wBjPGI00bWdxdLcCzK5awjs25YXpqB9lS8S0vbO0t9ZcBeNibA7g==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.47.1': resolution: {integrity: sha512-Ft+d/9DXs30BK7CHCTX11FtQGHUdpNDLJW0HHLign4lgMgBcPFN3NkdIXhC5r9iwsMwYreBBc4Rho5ieOmKNVQ==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-win32-arm64-msvc@4.47.1': resolution: {integrity: sha512-N9X5WqGYzZnjGAFsKSfYFtAShYjwOmFJoWbLg3dYixZOZqU7hdMq+/xyS14zKLhFhZDhP9VfkzQnsdk0ZDS9IA==} @@ -740,6 +757,9 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/fs-extra@11.0.4': + resolution: {integrity: sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==} + '@types/fs-extra@9.0.13': resolution: {integrity: sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==} @@ -752,6 +772,9 @@ packages: '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + '@types/jsonfile@6.1.4': + resolution: {integrity: sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==} + '@types/keyv@3.1.4': resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} @@ -1675,8 +1698,8 @@ packages: resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} engines: {node: '>=12'} - fs-extra@11.3.0: - resolution: {integrity: sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==} + fs-extra@11.3.2: + resolution: {integrity: sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==} engines: {node: '>=14.14'} fs-extra@7.0.1: @@ -3423,7 +3446,7 @@ snapshots: '@malept/cross-spawn-promise': 2.0.0 debug: 4.4.0 dir-compare: 4.2.0 - fs-extra: 11.3.0 + fs-extra: 11.3.2 minimatch: 9.0.5 plist: 3.1.0 transitivePeerDependencies: @@ -3433,7 +3456,7 @@ snapshots: dependencies: cross-dirname: 0.1.0 debug: 4.4.0 - fs-extra: 11.3.0 + fs-extra: 11.3.2 minimist: 1.2.8 postject: 1.0.0-alpha.6 transitivePeerDependencies: @@ -3768,6 +3791,11 @@ snapshots: '@types/estree@1.0.8': {} + '@types/fs-extra@11.0.4': + dependencies: + '@types/jsonfile': 6.1.4 + '@types/node': 22.15.15 + '@types/fs-extra@9.0.13': dependencies: '@types/node': 22.15.15 @@ -3778,6 +3806,10 @@ snapshots: '@types/json5@0.0.29': {} + '@types/jsonfile@6.1.4': + dependencies: + '@types/node': 22.15.15 + '@types/keyv@3.1.4': dependencies: '@types/node': 22.15.15 @@ -5108,7 +5140,7 @@ snapshots: jsonfile: 6.1.0 universalify: 2.0.1 - fs-extra@11.3.0: + fs-extra@11.3.2: dependencies: graceful-fs: 4.2.11 jsonfile: 6.1.0 diff --git a/src/main/IPCs.ts b/src/main/IPCs.ts index a048f8d..8ae92b4 100644 --- a/src/main/IPCs.ts +++ b/src/main/IPCs.ts @@ -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 } + } + } + ) } } diff --git a/src/main/utils/Constants.ts b/src/main/utils/Constants.ts index 176f985..64e5724 100644 --- a/src/main/utils/Constants.ts +++ b/src/main/utils/Constants.ts @@ -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, '..') diff --git a/src/preload/index.ts b/src/preload/index.ts index e6e5d2f..cc5328a 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -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[] = [] diff --git a/src/renderer/locales/en.json b/src/renderer/locales/en.json index c57a3ff..1931111 100644 --- a/src/renderer/locales/en.json +++ b/src/renderer/locales/en.json @@ -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", diff --git a/src/renderer/locales/zh-hans.json b/src/renderer/locales/zh-hans.json index 4f2cdc9..6746dc1 100644 --- a/src/renderer/locales/zh-hans.json +++ b/src/renderer/locales/zh-hans.json @@ -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": "改变主题", diff --git a/src/renderer/router/index.ts b/src/renderer/router/index.ts index 63edb6c..4361278 100644 --- a/src/renderer/router/index.ts +++ b/src/renderer/router/index.ts @@ -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' } }, { diff --git a/src/renderer/screens/CreateTemplateScreen.vue b/src/renderer/screens/CreateTemplateScreen.vue new file mode 100644 index 0000000..2b36bc3 --- /dev/null +++ b/src/renderer/screens/CreateTemplateScreen.vue @@ -0,0 +1,334 @@ + + + diff --git a/src/renderer/screens/MainScreen.vue b/src/renderer/screens/MainScreen.vue index f62062d..697f542 100644 --- a/src/renderer/screens/MainScreen.vue +++ b/src/renderer/screens/MainScreen.vue @@ -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') +}