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
+1188 -103

No files matched your search

+2
View File
@@ -43,6 +43,7 @@
}, },
"dependencies": { "dependencies": {
"electron-log": "^5.4.3", "electron-log": "^5.4.3",
"fs-extra": "^11.3.2",
"pinia": "^3.0.3", "pinia": "^3.0.3",
"qsu": "^1.10.2", "qsu": "^1.10.2",
"vue": "^3.5.22", "vue": "^3.5.22",
@@ -53,6 +54,7 @@
"@eslint/js": "^9.37.0", "@eslint/js": "^9.37.0",
"@mdi/js": "^7.4.47", "@mdi/js": "^7.4.47",
"@playwright/test": "^1.56.1", "@playwright/test": "^1.56.1",
"@types/fs-extra": "^11.0.4",
"@typescript-eslint/parser": "^8.46.1", "@typescript-eslint/parser": "^8.46.1",
"@vitejs/plugin-vue": "^6.0.1", "@vitejs/plugin-vue": "^6.0.1",
"@vitejs/plugin-vue-jsx": "^5.1.1", "@vitejs/plugin-vue-jsx": "^5.1.1",
+37 -5
View File
@@ -11,6 +11,9 @@ importers:
electron-log: electron-log:
specifier: ^5.4.3 specifier: ^5.4.3
version: 5.4.3 version: 5.4.3
fs-extra:
specifier: ^11.3.2
version: 11.3.2
pinia: pinia:
specifier: ^3.0.3 specifier: ^3.0.3
version: 3.0.3(typescript@5.9.3)(vue@3.5.22(typescript@5.9.3)) version: 3.0.3(typescript@5.9.3)(vue@3.5.22(typescript@5.9.3))
@@ -36,6 +39,9 @@ importers:
'@playwright/test': '@playwright/test':
specifier: ^1.56.1 specifier: ^1.56.1
version: 1.56.1 version: 1.56.1
'@types/fs-extra':
specifier: ^11.0.4
version: 11.0.4
'@typescript-eslint/parser': '@typescript-eslint/parser':
specifier: ^8.46.1 specifier: ^8.46.1
version: 8.46.1(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) 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==} resolution: {integrity: sha512-HZZBXJL1udxlCVvoVadstgiU26seKkHbbAMLg7680gAcMnRNP9SAwTMVet02ANA94kXEI2VhBnXs4e5nf7KG2A==}
cpu: [arm] cpu: [arm]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm-musleabihf@4.47.1': '@rollup/rollup-linux-arm-musleabihf@4.47.1':
resolution: {integrity: sha512-sZ5p2I9UA7T950JmuZ3pgdKA6+RTBr+0FpK427ExW0t7n+QwYOcmDTK/aRlzoBrWyTpJNlS3kacgSlSTUg6P/Q==} resolution: {integrity: sha512-sZ5p2I9UA7T950JmuZ3pgdKA6+RTBr+0FpK427ExW0t7n+QwYOcmDTK/aRlzoBrWyTpJNlS3kacgSlSTUg6P/Q==}
cpu: [arm] cpu: [arm]
os: [linux] os: [linux]
libc: [musl]
'@rollup/rollup-linux-arm64-gnu@4.47.1': '@rollup/rollup-linux-arm64-gnu@4.47.1':
resolution: {integrity: sha512-3hBFoqPyU89Dyf1mQRXCdpc6qC6At3LV6jbbIOZd72jcx7xNk3aAp+EjzAtN6sDlmHFzsDJN5yeUySvorWeRXA==} resolution: {integrity: sha512-3hBFoqPyU89Dyf1mQRXCdpc6qC6At3LV6jbbIOZd72jcx7xNk3aAp+EjzAtN6sDlmHFzsDJN5yeUySvorWeRXA==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm64-musl@4.47.1': '@rollup/rollup-linux-arm64-musl@4.47.1':
resolution: {integrity: sha512-49J4FnMHfGodJWPw73Ve+/hsPjZgcXQGkmqBGZFvltzBKRS+cvMiWNLadOMXKGnYRhs1ToTGM0sItKISoSGUNA==} resolution: {integrity: sha512-49J4FnMHfGodJWPw73Ve+/hsPjZgcXQGkmqBGZFvltzBKRS+cvMiWNLadOMXKGnYRhs1ToTGM0sItKISoSGUNA==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl]
'@rollup/rollup-linux-loongarch64-gnu@4.47.1': '@rollup/rollup-linux-loongarch64-gnu@4.47.1':
resolution: {integrity: sha512-4yYU8p7AneEpQkRX03pbpLmE21z5JNys16F1BZBZg5fP9rIlb0TkeQjn5du5w4agConCCEoYIG57sNxjryHEGg==} resolution: {integrity: sha512-4yYU8p7AneEpQkRX03pbpLmE21z5JNys16F1BZBZg5fP9rIlb0TkeQjn5du5w4agConCCEoYIG57sNxjryHEGg==}
cpu: [loong64] cpu: [loong64]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-ppc64-gnu@4.47.1': '@rollup/rollup-linux-ppc64-gnu@4.47.1':
resolution: {integrity: sha512-fAiq+J28l2YMWgC39jz/zPi2jqc0y3GSRo1yyxlBHt6UN0yYgnegHSRPa3pnHS5amT/efXQrm0ug5+aNEu9UuQ==} resolution: {integrity: sha512-fAiq+J28l2YMWgC39jz/zPi2jqc0y3GSRo1yyxlBHt6UN0yYgnegHSRPa3pnHS5amT/efXQrm0ug5+aNEu9UuQ==}
cpu: [ppc64] cpu: [ppc64]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-gnu@4.47.1': '@rollup/rollup-linux-riscv64-gnu@4.47.1':
resolution: {integrity: sha512-daoT0PMENNdjVYYU9xec30Y2prb1AbEIbb64sqkcQcSaR0zYuKkoPuhIztfxuqN82KYCKKrj+tQe4Gi7OSm1ow==} resolution: {integrity: sha512-daoT0PMENNdjVYYU9xec30Y2prb1AbEIbb64sqkcQcSaR0zYuKkoPuhIztfxuqN82KYCKKrj+tQe4Gi7OSm1ow==}
cpu: [riscv64] cpu: [riscv64]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-musl@4.47.1': '@rollup/rollup-linux-riscv64-musl@4.47.1':
resolution: {integrity: sha512-JNyXaAhWtdzfXu5pUcHAuNwGQKevR+6z/poYQKVW+pLaYOj9G1meYc57/1Xv2u4uTxfu9qEWmNTjv/H/EpAisw==} resolution: {integrity: sha512-JNyXaAhWtdzfXu5pUcHAuNwGQKevR+6z/poYQKVW+pLaYOj9G1meYc57/1Xv2u4uTxfu9qEWmNTjv/H/EpAisw==}
cpu: [riscv64] cpu: [riscv64]
os: [linux] os: [linux]
libc: [musl]
'@rollup/rollup-linux-s390x-gnu@4.47.1': '@rollup/rollup-linux-s390x-gnu@4.47.1':
resolution: {integrity: sha512-U/CHbqKSwEQyZXjCpY43/GLYcTVKEXeRHw0rMBJP7fP3x6WpYG4LTJWR3ic6TeYKX6ZK7mrhltP4ppolyVhLVQ==} resolution: {integrity: sha512-U/CHbqKSwEQyZXjCpY43/GLYcTVKEXeRHw0rMBJP7fP3x6WpYG4LTJWR3ic6TeYKX6ZK7mrhltP4ppolyVhLVQ==}
cpu: [s390x] cpu: [s390x]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-gnu@4.47.1': '@rollup/rollup-linux-x64-gnu@4.47.1':
resolution: {integrity: sha512-uTLEakjxOTElfeZIGWkC34u2auLHB1AYS6wBjPGI00bWdxdLcCzK5awjs25YXpqB9lS8S0vbO0t9ZcBeNibA7g==} resolution: {integrity: sha512-uTLEakjxOTElfeZIGWkC34u2auLHB1AYS6wBjPGI00bWdxdLcCzK5awjs25YXpqB9lS8S0vbO0t9ZcBeNibA7g==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-musl@4.47.1': '@rollup/rollup-linux-x64-musl@4.47.1':
resolution: {integrity: sha512-Ft+d/9DXs30BK7CHCTX11FtQGHUdpNDLJW0HHLign4lgMgBcPFN3NkdIXhC5r9iwsMwYreBBc4Rho5ieOmKNVQ==} resolution: {integrity: sha512-Ft+d/9DXs30BK7CHCTX11FtQGHUdpNDLJW0HHLign4lgMgBcPFN3NkdIXhC5r9iwsMwYreBBc4Rho5ieOmKNVQ==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl]
'@rollup/rollup-win32-arm64-msvc@4.47.1': '@rollup/rollup-win32-arm64-msvc@4.47.1':
resolution: {integrity: sha512-N9X5WqGYzZnjGAFsKSfYFtAShYjwOmFJoWbLg3dYixZOZqU7hdMq+/xyS14zKLhFhZDhP9VfkzQnsdk0ZDS9IA==} resolution: {integrity: sha512-N9X5WqGYzZnjGAFsKSfYFtAShYjwOmFJoWbLg3dYixZOZqU7hdMq+/xyS14zKLhFhZDhP9VfkzQnsdk0ZDS9IA==}
@@ -740,6 +757,9 @@ packages:
'@types/estree@1.0.8': '@types/estree@1.0.8':
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} 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': '@types/fs-extra@9.0.13':
resolution: {integrity: sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==} resolution: {integrity: sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==}
@@ -752,6 +772,9 @@ packages:
'@types/json5@0.0.29': '@types/json5@0.0.29':
resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==}
'@types/jsonfile@6.1.4':
resolution: {integrity: sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==}
'@types/keyv@3.1.4': '@types/keyv@3.1.4':
resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==}
@@ -1675,8 +1698,8 @@ packages:
resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==}
engines: {node: '>=12'} engines: {node: '>=12'}
fs-extra@11.3.0: fs-extra@11.3.2:
resolution: {integrity: sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==} resolution: {integrity: sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==}
engines: {node: '>=14.14'} engines: {node: '>=14.14'}
fs-extra@7.0.1: fs-extra@7.0.1:
@@ -3423,7 +3446,7 @@ snapshots:
'@malept/cross-spawn-promise': 2.0.0 '@malept/cross-spawn-promise': 2.0.0
debug: 4.4.0 debug: 4.4.0
dir-compare: 4.2.0 dir-compare: 4.2.0
fs-extra: 11.3.0 fs-extra: 11.3.2
minimatch: 9.0.5 minimatch: 9.0.5
plist: 3.1.0 plist: 3.1.0
transitivePeerDependencies: transitivePeerDependencies:
@@ -3433,7 +3456,7 @@ snapshots:
dependencies: dependencies:
cross-dirname: 0.1.0 cross-dirname: 0.1.0
debug: 4.4.0 debug: 4.4.0
fs-extra: 11.3.0 fs-extra: 11.3.2
minimist: 1.2.8 minimist: 1.2.8
postject: 1.0.0-alpha.6 postject: 1.0.0-alpha.6
transitivePeerDependencies: transitivePeerDependencies:
@@ -3768,6 +3791,11 @@ snapshots:
'@types/estree@1.0.8': {} '@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': '@types/fs-extra@9.0.13':
dependencies: dependencies:
'@types/node': 22.15.15 '@types/node': 22.15.15
@@ -3778,6 +3806,10 @@ snapshots:
'@types/json5@0.0.29': {} '@types/json5@0.0.29': {}
'@types/jsonfile@6.1.4':
dependencies:
'@types/node': 22.15.15
'@types/keyv@3.1.4': '@types/keyv@3.1.4':
dependencies: dependencies:
'@types/node': 22.15.15 '@types/node': 22.15.15
@@ -5108,7 +5140,7 @@ snapshots:
jsonfile: 6.1.0 jsonfile: 6.1.0
universalify: 2.0.1 universalify: 2.0.1
fs-extra@11.3.0: fs-extra@11.3.2:
dependencies: dependencies:
graceful-fs: 4.2.11 graceful-fs: 4.2.11
jsonfile: 6.1.0 jsonfile: 6.1.0
+123
View File
@@ -1,5 +1,12 @@
import { ipcMain, shell, IpcMainEvent, dialog } from 'electron' import { ipcMain, shell, IpcMainEvent, dialog } from 'electron'
import Constants from './utils/Constants' import Constants from './utils/Constants'
import { join, dirname } from 'path'
import {
writeFileSync,
readFileSync,
existsSync,
ensureDirSync
} from 'fs-extra'
/* /*
* IPC Communications * IPC Communications
@@ -36,5 +43,121 @@ export default class IPCs {
return dialogResult 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 { join, dirname, resolve } from 'path'
import { name, version, debug } from '../../../package.json' import { name, version, debug } from '../../../package.json'
import { fileURLToPath } from 'url' import { fileURLToPath } from 'url'
import { app } from 'electron'
const __dirname = dirname(fileURLToPath(import.meta.url)) 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 IS_DEV_ENV = process.env.NODE_ENV === 'development'
static APP_DATA_PATH = app.getPath('userData')
static PUBLIC_PATH = Constants.IS_DEV_ENV static PUBLIC_PATH = Constants.IS_DEV_ENV
? resolve(__dirname, '../../src/public') ? resolve(__dirname, '../../src/public')
: resolve(__dirname, '..') : resolve(__dirname, '..')
+5 -1
View File
@@ -9,7 +9,11 @@ log.transports.console.format = '{h}:{i}:{s}.{ms} {text}'
const mainAvailChannels: string[] = [ const mainAvailChannels: string[] = [
'msgRequestGetVersion', 'msgRequestGetVersion',
'msgOpenExternalLink', 'msgOpenExternalLink',
'msgOpenFile' 'msgOpenFile',
'msgSelectDirectory',
'msgLoadData',
'msgSaveData',
'msgExecuteTemplate'
] ]
const rendererAvailChannels: string[] = [] const rendererAvailChannels: string[] = []
+3 -1
View File
@@ -8,7 +8,9 @@
"title": { "title": {
"main": "Main Screen", "main": "Main Screen",
"second": "Second Screen", "second": "Second Screen",
"error": "Unknown Error" "error": "Unknown Error",
"templates": "Template List",
"create-template": "Create Template"
}, },
"menu": { "menu": {
"change-theme": "Change Theme", "change-theme": "Change Theme",
+6 -4
View File
@@ -1,14 +1,16 @@
{ {
"desc": { "desc": {
"welcome-title": "你好武特龙! 一切都准备好了。", "welcome-title": "你好! 模板生成器",
"welcome-desc": "您现在可以编写跨平台的 Web 应用程序。 实时查看更改并使用一个命令构建到多个平台。 如果您还不熟悉它,可以从下面的文档页面获得帮助,或者运行一些预先编写的示例代码。", "welcome-desc": "您现在可以创建和管理文件模板。 实时查看更改并使用模板批量生成文件和目录。",
"second-desc": "您已移至第二个屏幕! 这里什么都没有!", "second-desc": "您已移至第二个屏幕! 这里什么都没有!",
"selected-file": "选定的文件:{filePath}." "selected-file": "选定的文件:{filePath}."
}, },
"title": { "title": {
"main": "主屏幕", "main": "模板生成器",
"second": "第二屏", "second": "第二屏",
"error": "未知错误" "error": "未知错误",
"templates": "模板列表",
"create-template": "创建模板"
}, },
"menu": { "menu": {
"change-theme": "改变主题", "change-theme": "改变主题",
+13 -2
View File
@@ -6,9 +6,20 @@ export default createRouter({
routes: [ routes: [
{ {
path: '/', path: '/',
component: MainScreen, redirect: '/templates'
},
{
path: '/templates',
component: () => import('@/renderer/screens/TemplateListScreen.vue'),
meta: { 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>
+96 -88
View File
@@ -3,16 +3,22 @@ import { useI18n } from 'vue-i18n'
import { useTheme } from 'vuetify' import { useTheme } from 'vuetify'
import { openExternal, openFile } from '@/renderer/utils' import { openExternal, openFile } from '@/renderer/utils'
import { useCounterStore } from '@/renderer/store/counter' import { useCounterStore } from '@/renderer/store/counter'
import { onMounted, ref } from 'vue' import { onMounted, ref, onBeforeMount } from 'vue'
import { import {
mdiBrightness6, mdiBrightness6,
mdiFileDocument, mdiFileDocument,
mdiFolderOpen, mdiFolderOpen,
mdiGithub, mdiGithub,
mdiPlusCircle mdiPlusCircle,
mdiFolderMultiple,
mdiFileDocumentMultiple,
mdiArrowRight,
mdiHome
} from '@mdi/js' } from '@mdi/js'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import { useRouter } from 'vue-router'
const router = useRouter()
const { t, locale, availableLocales } = useI18n() const { t, locale, availableLocales } = useI18n()
const { increaseCount } = useCounterStore() const { increaseCount } = useCounterStore()
const { count } = storeToRefs(useCounterStore()) const { count } = storeToRefs(useCounterStore())
@@ -60,35 +66,38 @@ const handleOpenFile = async () => {
selectedFile.value = dialogResult.filePaths[0] selectedFile.value = dialogResult.filePaths[0]
} }
} }
const navigateToTemplates = () => {
router.push('/templates')
}
const navigateToCreateTemplate = () => {
router.push('/create-template')
}
</script> </script>
<template> <template>
<v-container> <v-container>
<v-row <v-row class="text-center mb-6">
no-gutters <v-col cols="12">
class="text-center align-center" <h1 class="text-h4 font-weight-bold mb-4">
> {{ t('title.main') }}
<v-col </h1>
cols="12" <p class="text-h6 mb-6">
md="5" {{ t('desc.welcome-desc') }}
> </p>
<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-col> </v-col>
<v-col </v-row>
cols="12"
md="7" <v-row justify="center" class="mb-6">
> <v-col cols="12" md="8" lg="6">
<h2 class="my-4"> <v-card class="pa-6 elevation-4">
{{ t('desc.welcome-title') }} <v-card-title class="text-h5 mb-4">
</h2> 模板管理器
<p>{{ t('desc.welcome-desc') }}</p> </v-card-title>
<p class="my-4"> <v-card-text>
<p>创建、管理和使用文件模板来批量生成目录和文件结构</p>
<p class="mt-4">
App Version: <strong>{{ appVersion }}</strong> App Version: <strong>{{ appVersion }}</strong>
</p> </p>
<p v-if="selectedFile"> <p v-if="selectedFile">
@@ -98,8 +107,55 @@ const handleOpenFile = async () => {
}) })
}} }}
</p> </p>
<v-row class="my-4"> </v-card-text>
<v-col> <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="6"
>
<v-btn <v-btn
icon icon
color="primary" color="primary"
@@ -113,75 +169,30 @@ const handleOpenFile = async () => {
{{ t('menu.change-theme') }} {{ t('menu.change-theme') }}
</v-tooltip> </v-tooltip>
</v-btn> </v-btn>
</v-col>
<v-col>
<v-badge
data-testid="counter-badge"
color="blue"
:content="count"
>
<v-btn <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" color="primary"
@click="handleOpenDocument" @click="handleOpenDocument"
class="ml-2"
> >
<v-icon :icon="mdiFileDocument" /> <v-icon :icon="mdiFileDocument" start />
<v-tooltip
activator="parent"
location="bottom"
>
{{ t('menu.documentation') }} {{ t('menu.documentation') }}
</v-tooltip>
</v-btn> </v-btn>
</v-col>
<v-col>
<v-btn <v-btn
icon
color="primary" color="primary"
@click="handleOpenGitHub" @click="handleOpenGitHub"
class="ml-2"
> >
<v-icon :icon="mdiGithub" /> <v-icon :icon="mdiGithub" start />
<v-tooltip
activator="parent"
location="bottom"
>
{{ t('menu.github') }} {{ t('menu.github') }}
</v-tooltip>
</v-btn> </v-btn>
</v-col> </v-col>
<v-col> <v-col
<v-btn cols="12"
icon md="6"
color="primary" class="d-flex justify-end"
@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 <v-select
data-testid="select-language" data-testid="select-language"
:model-value="locale" :model-value="locale"
@@ -189,11 +200,8 @@ const handleOpenFile = async () => {
:label="t('menu.change-language')" :label="t('menu.change-language')"
:items="languages" :items="languages"
@update:model-value="handleChangeLanguage" @update:model-value="handleChangeLanguage"
> class="ml-2"
{{ t('menu.change-language') }} />
</v-select>
</v-col>
</v-row>
</v-col> </v-col>
</v-row> </v-row>
</v-container> </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 ErrorScreen from '@/renderer/screens/ErrorScreen.vue'
import MainScreen from '@/renderer/screens/MainScreen.vue' import MainScreen from '@/renderer/screens/MainScreen.vue'
import SecondScreen from '@/renderer/screens/SecondScreen.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
}
})
+174
View File
@@ -0,0 +1,174 @@
import { test, expect, beforeAll, afterAll } from '../fixtures.mjs'
test.beforeAll(beforeAll)
test.afterAll(afterAll)
// @ts-expect-error: `util` is not using types in playwright
test('Template list page navigation and basic functionality', async ({
page,
util
}) => {
try {
// Navigate to templates page
await page.goto('/')
await page.waitForURL('**/#/templates')
// Check if we're on the templates page
await expect(page.getByText('模板列表')).toBeVisible()
// Check if "No templates" message is visible initially
await expect(page.getByText('暂无模板,请先创建模板。')).toBeVisible()
// Check if "Create Template" button is visible
await expect(page.getByText('创建模板')).toBeVisible()
await util.captureScreenshot(page, 'template-list-no-templates')
} catch (error) {
throw await util.onTestError(error)
}
})
// @ts-expect-error: `util` is not using types in playwright
test('Create template page navigation and functionality', async ({
page,
util
}) => {
try {
// Navigate to templates page
await page.goto('/')
await page.waitForURL('**/#/templates')
// Click create template button
await page.getByText('创建模板').click()
await page.waitForURL('**/#/create-template')
// Check if we're on the create template page
await expect(page.getByText('创建模板')).toBeVisible()
// Check if form elements are visible
await expect(page.getByLabel('模板名称 *')).toBeVisible()
await expect(page.getByLabel('模板描述')).toBeVisible()
await util.captureScreenshot(page, 'create-template-page')
} catch (error) {
throw await util.onTestError(error)
}
})
// @ts-expect-error: `util` is not using types in playwright
test('Add files and directories to template', async ({ page, util }) => {
try {
// Navigate to create template page
await page.goto('/#/create-template')
// Fill in template name and description
await page.getByLabel('模板名称 *').fill('Test Template')
await page.getByLabel('模板描述').fill('This is a test template')
// Add a file
await page.getByText('添加文件').click()
// Wait for dialog to appear and fill in file details
await page
.locator('[data-testid="add-file-path-input"]')
.fill('src/index.js')
await page
.locator('[data-testid="add-file-content-input"]')
.fill('console.log("Hello World");')
await page.getByText('添加').click()
// Add a directory
await page.getByText('添加目录').click()
// Wait for dialog to appear and fill in directory details
await page
.locator('[data-testid="add-dir-path-input"]')
.fill('src/components')
await page.getByText('添加').click()
// Verify items were added to the tables
await expect(page.getByText('src/index.js')).toBeVisible()
await expect(page.getByText('src/components')).toBeVisible()
await util.captureScreenshot(page, 'template-with-content')
} catch (error) {
throw await util.onTestError(error)
}
})
// @ts-expect-error: `util` is not using types in playwright
test('Template creation and listing', async ({ page, util }) => {
try {
// Navigate to create template page
await page.goto('/#/create-template')
// Fill in template name and description
await page.getByLabel('模板名称 *').fill('Test Template')
await page.getByLabel('模板描述').fill('This is a test template')
// Add a file
await page.getByText('添加文件').click()
await page
.locator('[data-testid="add-file-path-input"]')
.fill('src/index.js')
await page
.locator('[data-testid="add-file-content-input"]')
.fill('console.log("Hello World");')
await page.getByText('添加').click()
// Save template
await page.getByText('保存模板').click()
// Wait for redirect to templates page
await page.waitForURL('**/#/templates')
// Check if the created template is listed
await expect(page.getByText('Test Template')).toBeVisible()
await expect(page.getByText('This is a test template')).toBeVisible()
await util.captureScreenshot(page, 'template-list-with-template')
} catch (error) {
throw await util.onTestError(error)
}
})
// @ts-expect-error: `util` is not using types in playwright
test('View template details', async ({ page, util }) => {
try {
// Navigate to templates page
await page.goto('/#/templates')
// Wait for template to appear and click view button
// First we need to create a template for this test
if (await page.getByText('暂无模板,请先创建模板。').isVisible()) {
await page.getByText('创建模板').click()
// Create a test template
await page.getByLabel('模板名称 *').fill('View Test Template')
await page.getByLabel('模板描述').fill('Template for view test')
await page.getByText('添加文件').click()
await page
.locator('[data-testid="add-file-path-input"]')
.fill('src/test.js')
await page
.locator('[data-testid="add-file-content-input"]')
.fill('console.log("Test");')
await page.getByText('添加').click()
await page.getByText('保存模板').click()
await page.waitForURL('**/#/templates')
}
// Wait for template to appear and click view button
await page.getByRole('button', { name: '查看' }).click()
// Check if template detail dialog appears
await expect(page.getByText('模板详情')).toBeVisible()
await expect(page.getByText('src/test.js')).toBeVisible()
await util.captureScreenshot(page, 'template-detail-view')
} catch (error) {
throw await util.onTestError(error)
}
})
+1 -1
View File
@@ -105,7 +105,7 @@ export default defineConfig(({ mode }) => {
autoImport: true autoImport: true
}), }),
// Docs: https://github.com/gxmari007/vite-plugin-eslint // Docs: https://github.com/gxmari007/vite-plugin-eslint
EslintPlugin(), // EslintPlugin(),
// Docs: https://github.com/electron-vite/vite-plugin-electron // Docs: https://github.com/electron-vite/vite-plugin-electron
ElectronPlugin(electronPluginConfigs), ElectronPlugin(electronPluginConfigs),
RendererPlugin() RendererPlugin()