This commit is contained in:
zhanggy committed 2025-11-05 14:46:49 +08:00
1 parent fc250c54ed
commit 5eec516459
61 files changed
+3897 -3361

No files matched your search

+49 -18
View File
@@ -2,31 +2,62 @@
"workbench.settings.useSplitJSON": true, "workbench.settings.useSplitJSON": true,
"eslint.format.enable": true, "eslint.format.enable": true,
"eslint.lintTask.enable": false, "eslint.lintTask.enable": false,
"eslint.codeActionsOnSave.rules": null,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
"eslint.validate": ["javascript", "vue"],
"files.autoSave": "afterDelay",
"editor.formatOnSave": true,
"editor.wordWrap": "on",
"editor.defaultFormatter": "esbenp.prettier-vscode", "editor.defaultFormatter": "esbenp.prettier-vscode",
"[javascript]": { "[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode" "editor.defaultFormatter": "esbenp.prettier-vscode"
}, },
"editor.tabSize": 2, "editor.tabSize": 2,
"prettier.embeddedLanguageFormatting": "off",
"prettier.enable": true,
"files.associations": {
"*.mjs": "javascript",
"*.cjs": "javascript",
"*.mts": "typescript"
},
"i18n-ally.localesPaths": [ "i18n-ally.localesPaths": [
"src/renderer/locales" "src/renderer/locales"
], ],
"i18n-ally.sourceLanguage": "zh-hans", "i18n-ally.sourceLanguage": "zh-hans",
"[vue]": {
"editor.defaultFormatter": "dbaeumer.vscode-eslint" // Disable the default formatter, use eslint instead
} "prettier.enable": false,
"editor.formatOnSave": false,
// Auto fix
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit",
"source.organizeImports": "never"
},
// Silent the stylistic rules in your IDE, but still auto fix them
"eslint.rules.customizations": [
{ "rule": "style/*", "severity": "off", "fixable": true },
{ "rule": "format/*", "severity": "off", "fixable": true },
{ "rule": "*-indent", "severity": "off", "fixable": true },
{ "rule": "*-spacing", "severity": "off", "fixable": true },
{ "rule": "*-spaces", "severity": "off", "fixable": true },
{ "rule": "*-order", "severity": "off", "fixable": true },
{ "rule": "*-dangle", "severity": "off", "fixable": true },
{ "rule": "*-newline", "severity": "off", "fixable": true },
{ "rule": "*quotes", "severity": "off", "fixable": true },
{ "rule": "*semi", "severity": "off", "fixable": true }
],
// Enable eslint for all supported languages
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact",
"vue",
"html",
"markdown",
"json",
"jsonc",
"yaml",
"toml",
"xml",
"gql",
"graphql",
"astro",
"svelte",
"css",
"less",
"scss",
"pcss",
"postcss"
]
} }
+1
View File
@@ -31,6 +31,7 @@
- `chore: ...` - 其他杂项,不触发版本更新 - `chore: ...` - 其他杂项,不触发版本更新
示例: 示例:
- `feat: 添加用户认证功能` - `feat: 添加用户认证功能`
- `fix: 修复登录页面的错误` - `fix: 修复登录页面的错误`
- `docs: 更新 API 文档` - `docs: 更新 API 文档`
+31 -31
View File
@@ -1,8 +1,8 @@
/* eslint-disable no-template-curly-in-string */ /* eslint-disable no-template-curly-in-string */
const dotenv = require('dotenv') const dotenv = require('dotenv');
const packageJson = require('../../package.json') const packageJson = require('../../package.json');
dotenv.config() dotenv.config();
const baseConfig = { const baseConfig = {
productName: packageJson.name, productName: packageJson.name,
@@ -16,12 +16,12 @@ const baseConfig = {
owner: 'Jasery', owner: 'Jasery',
repo: 'gdt-tool', repo: 'gdt-tool',
releaseType: 'release', releaseType: 'release',
token: process.env.GITHUB_TOKEN || process.env.GH_TOKEN || '' // 优先使用 GITHUB_TOKEN(GitHub Actions 自动提供) token: process.env.GITHUB_TOKEN || process.env.GH_TOKEN || '', // 优先使用 GITHUB_TOKEN(GitHub Actions 自动提供)
} },
], ],
artifactName: '${productName} ${version}_${arch}.${ext}', artifactName: '${productName} ${version}_${arch}.${ext}',
directories: { directories: {
output: './release/${version}' output: './release/${version}',
}, },
mac: { mac: {
bundleVersion: '1.0', bundleVersion: '1.0',
@@ -33,9 +33,9 @@ const baseConfig = {
target: [ target: [
{ {
target: 'dmg', target: 'dmg',
arch: ['x64', 'arm64', 'universal'] arch: ['x64', 'arm64', 'universal'],
} },
] ],
}, },
dmg: { dmg: {
contents: [ contents: [
@@ -43,15 +43,15 @@ const baseConfig = {
x: 410, x: 410,
y: 150, y: 150,
type: 'link', type: 'link',
path: '/Applications' path: '/Applications',
}, },
{ {
x: 130, x: 130,
y: 150, y: 150,
type: 'file' type: 'file',
} },
], ],
sign: false sign: false,
}, },
win: { win: {
icon: 'buildAssets/icons/icon.ico', icon: 'buildAssets/icons/icon.ico',
@@ -70,15 +70,15 @@ const baseConfig = {
// }, // },
{ {
target: 'nsis', target: 'nsis',
arch: 'x64' arch: 'x64',
} },
] ],
}, },
portable: { portable: {
artifactName: '${productName} ${version}_${arch} Portable.${ext}' artifactName: '${productName} ${version}_${arch} Portable.${ext}',
}, },
nsis: { nsis: {
oneClick: true oneClick: true,
}, },
linux: { linux: {
executableName: packageJson.name.toLowerCase(), executableName: packageJson.name.toLowerCase(),
@@ -87,21 +87,21 @@ const baseConfig = {
target: [ target: [
{ {
target: 'snap', target: 'snap',
arch: 'x64' arch: 'x64',
}, },
{ {
target: 'deb', target: 'deb',
arch: 'x64' arch: 'x64',
}, },
{ {
target: 'rpm', target: 'rpm',
arch: 'x64' arch: 'x64',
} },
] ],
} },
} };
baseConfig.copyright = `ⓒ ${new Date().getFullYear()} $\{author}` baseConfig.copyright = `ⓒ ${new Date().getFullYear()} $\{author}`;
baseConfig.files = [ baseConfig.files = [
/* A list of files not to be included in the build. */ /* A list of files not to be included in the build. */
/* /*
@@ -111,11 +111,11 @@ baseConfig.files = [
'!dist/main/index.dev.js', '!dist/main/index.dev.js',
'!docs/**/*', '!docs/**/*',
'!tests/**/*', '!tests/**/*',
'!release/**/*' '!release/**/*',
] ];
// TODO: Notarize for macOS // TODO: Notarize for macOS
baseConfig.mac.identity = null baseConfig.mac.identity = null;
/* if (process.env.MAC_NOTARIZE === 'true') { /* if (process.env.MAC_NOTARIZE === 'true') {
baseConfig.afterSign = './buildAssets/builder/notarize.ts' baseConfig.afterSign = './buildAssets/builder/notarize.ts'
} else { } else {
@@ -123,5 +123,5 @@ baseConfig.mac.identity = null
} */ } */
module.exports = { module.exports = {
...baseConfig ...baseConfig,
} };
+1 -1
View File
@@ -40,7 +40,7 @@ publish: [
releaseType: 'release', releaseType: 'release',
token: process.env.GH_TOKEN || '' // 从环境变量获取 token: process.env.GH_TOKEN || '' // 从环境变量获取
} }
] ];
``` ```
## 发布更新 ## 发布更新
+38 -37
View File
@@ -1,14 +1,15 @@
import { withSidebar } from 'vitepress-sidebar' import type { UserConfig } from 'vitepress';
import { name, repository, homepage } from '../../../package.json' import type { VitePressI18nOptions } from 'vitepress-i18n/types';
import { defineConfig, UserConfig } from 'vitepress' import type { VitePressSidebarOptions } from 'vitepress-sidebar/types';
import { withI18n } from 'vitepress-i18n' import { defineConfig } from 'vitepress';
import type { VitePressSidebarOptions } from 'vitepress-sidebar/types' import { withI18n } from 'vitepress-i18n';
import type { VitePressI18nOptions } from 'vitepress-i18n/types' import { withSidebar } from 'vitepress-sidebar';
import { homepage, name, repository } from '../../../package.json';
const capitalizeFirst = (str: string): string => const capitalizeFirst = (str: string): string =>
str.charAt(0).toUpperCase() + str.slice(1) str.charAt(0).toUpperCase() + str.slice(1);
const supportLocales = ['en', 'ko', 'zhHans'] const supportLocales = ['en', 'ko', 'zhHans'];
const defaultLocale: string = supportLocales[0] const defaultLocale: string = supportLocales[0];
const vitePressI18nConfigs: VitePressI18nOptions = { const vitePressI18nConfigs: VitePressI18nOptions = {
locales: supportLocales, locales: supportLocales,
@@ -18,35 +19,35 @@ const vitePressI18nConfigs: VitePressI18nOptions = {
en: 'Vutron is a preconfigured template for developing Electron cross-platform desktop apps. It uses Vue 3 and allows you to build a fast development environment with little effort.', en: 'Vutron is a preconfigured template for developing Electron cross-platform desktop apps. It uses Vue 3 and allows you to build a fast development environment with little effort.',
ko: 'Vutron은 Electron 크로스 플랫폼 데스크톱 앱 개발을 위해 미리 구성된 템플릿입니다. Vue 3을 사용하며 적은 노력으로 빠른 개발 환경을 구축할 수 있습니다.', ko: 'Vutron은 Electron 크로스 플랫폼 데스크톱 앱 개발을 위해 미리 구성된 템플릿입니다. Vue 3을 사용하며 적은 노력으로 빠른 개발 환경을 구축할 수 있습니다.',
zhHans: zhHans:
'Vutron 是用于开发 Electron 跨平台桌面应用程序的预配置模板。它使用 Vue 3,可让您轻松构建快速开发环境。' 'Vutron 是用于开发 Electron 跨平台桌面应用程序的预配置模板。它使用 Vue 3,可让您轻松构建快速开发环境。',
}, },
themeConfig: { themeConfig: {
en: { en: {
nav: [ nav: [
{ {
text: 'Getting Started', text: 'Getting Started',
link: '/installation-and-build/getting-started' link: '/installation-and-build/getting-started',
} },
] ],
}, },
ko: { ko: {
nav: [ nav: [
{ {
text: '시작하기', text: '시작하기',
link: '/ko/installation-and-build/getting-started' link: '/ko/installation-and-build/getting-started',
} },
] ],
}, },
zhHans: { zhHans: {
nav: [ nav: [
{ {
text: '入门', text: '入门',
link: '/zhHans/installation-and-build/getting-started' link: '/zhHans/installation-and-build/getting-started',
} },
] ],
} },
} },
} };
const vitePressSidebarConfigs: VitePressSidebarOptions = [ const vitePressSidebarConfigs: VitePressSidebarOptions = [
...supportLocales.map((lang) => { ...supportLocales.map((lang) => {
@@ -62,14 +63,14 @@ const vitePressSidebarConfigs: VitePressSidebarOptions = [
'introduction.md', 'introduction.md',
'installation-and-build', 'installation-and-build',
'project-structures', 'project-structures',
'electron-how-to' 'electron-how-to',
], ],
documentRootPath: `/src/${lang}`, documentRootPath: `/src/${lang}`,
resolvePath: defaultLocale === lang ? '/' : `/${lang}/`, resolvePath: defaultLocale === lang ? '/' : `/${lang}/`,
...(defaultLocale === lang ? {} : { basePath: `/${lang}/` }) ...(defaultLocale === lang ? {} : { basePath: `/${lang}/` }),
} };
}) }),
] ];
const vitePressConfigs: UserConfig = { const vitePressConfigs: UserConfig = {
title: capitalizeFirst(name), title: capitalizeFirst(name),
@@ -77,28 +78,28 @@ const vitePressConfigs: UserConfig = {
outDir: '../dist', outDir: '../dist',
head: [ head: [
['link', { rel: 'icon', href: '/logo.png' }], ['link', { rel: 'icon', href: '/logo.png' }],
['link', { rel: 'shortcut icon', href: '/favicon.ico' }] ['link', { rel: 'shortcut icon', href: '/favicon.ico' }],
], ],
cleanUrls: true, cleanUrls: true,
metaChunk: true, metaChunk: true,
rewrites: { rewrites: {
'en/:rest*': ':rest*' 'en/:rest*': ':rest*',
}, },
sitemap: { sitemap: {
hostname: homepage hostname: homepage,
}, },
themeConfig: { themeConfig: {
logo: { src: '/icon.png', width: 24, height: 24 }, logo: { src: '/icon.png', width: 24, height: 24 },
editLink: { editLink: {
pattern: 'https://github.com/jooy2/vutron/edit/main/docs/src/:path' pattern: 'https://github.com/jooy2/vutron/edit/main/docs/src/:path',
}, },
socialLinks: [{ icon: 'github', link: repository.url.replace('.git', '') }] socialLinks: [{ icon: 'github', link: repository.url.replace('.git', '') }],
} },
} };
export default defineConfig( export default defineConfig(
withSidebar( withSidebar(
withI18n(vitePressConfigs, vitePressI18nConfigs), withI18n(vitePressConfigs, vitePressI18nConfigs),
vitePressSidebarConfigs vitePressSidebarConfigs,
) ),
) );
@@ -19,7 +19,7 @@ If you want to skip the security issues and use Node.js scripts in your renderer
```javascript ```javascript
rendererPlugin({ rendererPlugin({
nodeIntegration: true nodeIntegration: true
}) });
``` ```
For more information on this, see the following articles: https://github.com/electron-vite/vite-plugin-electron-renderer For more information on this, see the following articles: https://github.com/electron-vite/vite-plugin-electron-renderer
@@ -9,31 +9,31 @@ order: 3
## General ## General
| Script Name | Description | | Script Name | Description |
| --- | --- | | ----------------- | ------------------------------------------------------------------------------------------------------- |
| `dev` | Start Electron as a development environment | | `dev` | Start Electron as a development environment |
| `dev:debug` | Start Electron as a development environment (with vite debug) | | `dev:debug` | Start Electron as a development environment (with vite debug) |
| `dev:debug:force` | Start Electron as a development environment (with vite debug + clean vite cache) | | `dev:debug:force` | Start Electron as a development environment (with vite debug + clean vite cache) |
| `build:pre` | Commands commonly run at build time. This script does not need to be run separately. | | `build:pre` | Commands commonly run at build time. This script does not need to be run separately. |
| `build` | Build the package for the current operating system. | | `build` | Build the package for the current operating system. |
| `build:all` | Build a specified package for the entire operating system (Requires cross-platform build configuration) | | `build:all` | Build a specified package for the entire operating system (Requires cross-platform build configuration) |
| `build:dir` | `electron-builder` directory build | | `build:dir` | `electron-builder` directory build |
| `build:mac` | Build preconfigured packages for macOS | | `build:mac` | Build preconfigured packages for macOS |
| `build:linux` | Build preconfigured packages for Linux | | `build:linux` | Build preconfigured packages for Linux |
| `build:win` | Build preconfigured packages for Windows | | `build:win` | Build preconfigured packages for Windows |
| `lint` | ESLint code inspection. It does not modify the code. | | `lint` | ESLint code inspection. It does not modify the code. |
| `lint:fix` | ESLint code inspection. Use auto-fix to fix your code. | | `lint:fix` | ESLint code inspection. Use auto-fix to fix your code. |
| `format` | Prettier code inspection. It does not modify the code. | | `format` | Prettier code inspection. It does not modify the code. |
| `format:fix` | Prettier code inspection. Use auto-fix to fix your code. | | `format:fix` | Prettier code inspection. Use auto-fix to fix your code. |
| `test` | Build a package for testing and run tests against the test specification file. | | `test` | Build a package for testing and run tests against the test specification file. |
| `test:linux` | Build a package for testing and run tests against the test specification file. (for linux ci only) | | `test:linux` | Build a package for testing and run tests against the test specification file. (for linux ci only) |
## For Documentation ## For Documentation
Used only for contributing to project documentation. Must be run from the `docs` directory location. Used only for contributing to project documentation. Must be run from the `docs` directory location.
| Script Name | Description | | Script Name | Description |
| --- | --- | | ----------- | ------------------------------------------------------------------ |
| `dev` | Start the local document server. (For development) | | `dev` | Start the local document server. (For development) |
| `build` | Build a local document server. Used only for GitHub page builders. | | `build` | Build a local document server. Used only for GitHub page builders. |
| `serve` | Start the local document server. | | `serve` | Start the local document server. |
@@ -19,7 +19,7 @@
```javascript ```javascript
rendererPlugin({ rendererPlugin({
nodeIntegration: true nodeIntegration: true
}) });
``` ```
이에 대한 자세한 내용은 다음 문서를 참조하세요: https://github.com/electron-vite/vite-plugin-electron-renderer 이에 대한 자세한 내용은 다음 문서를 참조하세요: https://github.com/electron-vite/vite-plugin-electron-renderer
@@ -9,31 +9,31 @@ order: 3
## 일반 ## 일반
| Script Name | Description | | Script Name | Description |
| --- | --- | | ----------------- | ------------------------------------------------------------------------------------------------------- |
| `dev` | Start Electron as a development environment | | `dev` | Start Electron as a development environment |
| `dev:debug` | Start Electron as a development environment (with vite debug) | | `dev:debug` | Start Electron as a development environment (with vite debug) |
| `dev:debug:force` | Start Electron as a development environment (with vite debug + clean vite cache) | | `dev:debug:force` | Start Electron as a development environment (with vite debug + clean vite cache) |
| `build:pre` | Commands commonly run at build time. This script does not need to be run separately. | | `build:pre` | Commands commonly run at build time. This script does not need to be run separately. |
| `build` | Build the package for the current operating system. | | `build` | Build the package for the current operating system. |
| `build:all` | Build a specified package for the entire operating system (Requires cross-platform build configuration) | | `build:all` | Build a specified package for the entire operating system (Requires cross-platform build configuration) |
| `build:dir` | `electron-builder` directory build | | `build:dir` | `electron-builder` directory build |
| `build:mac` | Build preconfigured packages for macOS | | `build:mac` | Build preconfigured packages for macOS |
| `build:linux` | Build preconfigured packages for Linux | | `build:linux` | Build preconfigured packages for Linux |
| `build:win` | Build preconfigured packages for Windows | | `build:win` | Build preconfigured packages for Windows |
| `lint` | ESLint code inspection. It does not modify the code. | | `lint` | ESLint code inspection. It does not modify the code. |
| `lint:fix` | ESLint code inspection. Use auto-fix to fix your code. | | `lint:fix` | ESLint code inspection. Use auto-fix to fix your code. |
| `format` | Prettier code inspection. It does not modify the code. | | `format` | Prettier code inspection. It does not modify the code. |
| `format:fix` | Prettier code inspection. Use auto-fix to fix your code. | | `format:fix` | Prettier code inspection. Use auto-fix to fix your code. |
| `test` | Build a package for testing and run tests against the test specification file. | | `test` | Build a package for testing and run tests against the test specification file. |
| `test:linux` | Build a package for testing and run tests against the test specification file. (for linux ci only) | | `test:linux` | Build a package for testing and run tests against the test specification file. (for linux ci only) |
## 문서용 ## 문서용
프로젝트 문서에 기여하는 경우에만 사용됩니다. `docs` 디렉토리 위치에서 실행해야 합니다. 프로젝트 문서에 기여하는 경우에만 사용됩니다. `docs` 디렉토리 위치에서 실행해야 합니다.
| Script Name | Description | | Script Name | Description |
| --- | --- | | ----------- | ------------------------------------------------------------------ |
| `dev` | Start the local document server. (For development) | | `dev` | Start the local document server. (For development) |
| `build` | Build a local document server. Used only for GitHub page builders. | | `build` | Build a local document server. Used only for GitHub page builders. |
| `serve` | Start the local document server. | | `serve` | Start the local document server. |
@@ -19,7 +19,7 @@
```javascript ```javascript
rendererPlugin({ rendererPlugin({
nodeIntegration: true nodeIntegration: true
}) });
``` ```
欲了解更多信息,请参阅以下文章: https://github.com/electron-vite/vite-plugin-electron-renderer 欲了解更多信息,请参阅以下文章: https://github.com/electron-vite/vite-plugin-electron-renderer
@@ -9,24 +9,24 @@ order: 3
## 一般情况 ## 一般情况
| 脚本名称 | 说明 | | 脚本名称 | 说明 |
| --- | --- | | ----------------- | ---------------------------------------------------------- |
| `dev` | 启动电子作为开发环境 | | `dev` | 启动电子作为开发环境 |
| `dev:debug` | 将 Electron 作为开发环境启动(使用 vite debug) | | `dev:debug` | 将 Electron 作为开发环境启动(使用 vite debug) |
| `dev:debug:force` | 以Electron作为开发环境启动(使用vite调试+清理vite缓存) | | `dev:debug:force` | 以Electron作为开发环境启动(使用vite调试+清理vite缓存) |
| `build:pre` | 通常在编译时运行的命令。此脚本无需单独运行。 | | `build:pre` | 通常在编译时运行的命令。此脚本无需单独运行。 |
| `build` | 为当前操作系统打包。 | | `build` | 为当前操作系统打包。 |
| `build:all` | 为整个操作系统构建指定软件包(需要跨平台构建配置) | | `build:all` | 为整个操作系统构建指定软件包(需要跨平台构建配置) |
| `build:dir` | `electron-builder`目录构建 | | `build:dir` | `electron-builder`目录构建 |
| `build:mac` | 为macOS构建预配置软件包 | | `build:mac` | 为macOS构建预配置软件包 |
| `build:linux` | 为Linux构建预配置软件包 | | `build:linux` | 为Linux构建预配置软件包 |
| `build:win` | 为Windows构建预配置软件包 | | `build:win` | 为Windows构建预配置软件包 |
| `lint` | ESLint代码检查。它不会修改代码。 | | `lint` | ESLint代码检查。它不会修改代码。 |
| `lint:fix` | ESLint代码检查。使用自动修复功能修复代码。 | | `lint:fix` | ESLint代码检查。使用自动修复功能修复代码。 |
| `format` | 更漂亮的代码检查。它不会修改代码。 | | `format` | 更漂亮的代码检查。它不会修改代码。 |
| `format:fix` | 更漂亮的代码检查。使用自动修复功能修复代码。 | | `format:fix` | 更漂亮的代码检查。使用自动修复功能修复代码。 |
| `test` | 根据测试规范文件构建测试包并运行测试。 | | `test` | 根据测试规范文件构建测试包并运行测试。 |
| `test:linux` | 根据测试规范文件构建测试包并运行测试。(仅适用于linux ci) | | `test:linux` | 根据测试规范文件构建测试包并运行测试。(仅适用于linux ci) |
## 文档 ## 文档
+24
View File
@@ -0,0 +1,24 @@
import antfu from '@antfu/eslint-config';
export default antfu({
formatters: true,
vue: true,
stylistic: {
indent: 2,
quotes: 'single',
semi: true,
},
typescript: true,
jsonc: false,
yaml: false,
lessOpinionated: true,
}, {
rules: {
'style/brace-style': ['error', '1tbs'],
'node/prefer-global/process': 'off',
'no-alert': 'off',
'no-console': 'warn',
'ts/no-use-before-define': 'off',
'ts/no-require-imports': 'off',
},
});
-86
View File
@@ -1,86 +0,0 @@
import { globalIgnores } from 'eslint/config'
import pluginJs from '@eslint/js'
import pluginTypeScriptESLint from 'typescript-eslint'
import parserVue from 'vue-eslint-parser'
import parserTypeScript from '@typescript-eslint/parser'
import pluginVue from 'eslint-plugin-vue'
import pluginNode from 'eslint-plugin-n'
import pluginImport from 'eslint-plugin-import'
import pluginPrettier from 'eslint-plugin-prettier/recommended'
import globals from 'globals'
export default pluginTypeScriptESLint.config(
pluginPrettier,
pluginJs.configs.recommended,
pluginTypeScriptESLint.configs.recommended,
pluginImport.flatConfigs.electron,
pluginNode.configs['flat/recommended-script'],
...pluginVue.configs['flat/strongly-recommended'],
globalIgnores([
'**/node_modules',
'**/dist',
'**/release',
'**/docs',
'**/.idea',
'**/.vscode',
'**/buildAssets/builder',
'**/tests/results',
'**/package-lock.json'
]),
{
files: ['**/*.{js,mjs,cjs,ts,vue}'],
languageOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
globals: {
...globals.browser,
...globals.node
},
parser: parserVue,
parserOptions: {
parser: parserTypeScript,
ecmaVersion: 2022,
ecmaFeatures: {
jsx: true
},
requireConfigFile: false
}
},
rules: {
// override/add rules settings here, such as:
eqeqeq: 'error',
'no-unused-vars': 'off',
'no-case-declarations': 'off',
'no-trailing-spaces': 'error',
'no-unsafe-optional-chaining': 'off',
'no-control-regex': 'off',
'n/no-missing-import': 'off',
'n/no-unsupported-features/node-builtins': 'off',
'vue/v-on-event-hyphenation': 'off',
'vue/no-v-text-v-html-on-component': 'off',
'vue/attribute-hyphenation': 'off',
'vue/singleline-html-element-content-newline': 'off',
'vue/max-attributes-per-line': [
'error',
{
singleline: {
max: 1
},
multiline: {
max: 1
}
}
],
'@typescript-eslint/no-unused-vars': [
'error',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
caughtErrorsIgnorePattern: '^_'
}
],
'@typescript-eslint/no-explicit-any': 'off'
}
}
)
+4 -9
View File
@@ -32,8 +32,8 @@
"build:mac": "npm run build:pre && CSC_IDENTITY_AUTO_DISCOVERY=false electron-builder --config=buildAssets/builder/config.js --mac", "build:mac": "npm run build:pre && CSC_IDENTITY_AUTO_DISCOVERY=false electron-builder --config=buildAssets/builder/config.js --mac",
"build:linux": "npm run build:pre && electron-builder --config=buildAssets/builder/config.js --linux", "build:linux": "npm run build:pre && electron-builder --config=buildAssets/builder/config.js --linux",
"build:win": "npm run build:pre && electron-builder --config=buildAssets/builder/config.js --windows", "build:win": "npm run build:pre && electron-builder --config=buildAssets/builder/config.js --windows",
"lint": "eslint . --ext .js,.ts,.vue .", "lint": "eslint",
"lint:fix": "eslint . --ext .js,.ts,.vue --fix .", "lint:fix": "eslint --fix",
"test": "npm run build:pre && playwright test", "test": "npm run build:pre && playwright test",
"test:linux": "npm run build:pre && xvfb-run --auto-servernum --server-args='-screen 0, 1280x960x24' -- playwright test", "test:linux": "npm run build:pre && xvfb-run --auto-servernum --server-args='-screen 0, 1280x960x24' -- playwright test",
"pub": "npm run build:pre && electron-builder --config=buildAssets/builder/config.js --windows --publish=always", "pub": "npm run build:pre && electron-builder --config=buildAssets/builder/config.js --windows --publish=always",
@@ -75,6 +75,7 @@
} }
}, },
"dependencies": { "dependencies": {
"@antfu/eslint-config": "^6.2.0",
"@mdi/font": "^7.4.47", "@mdi/font": "^7.4.47",
"@vueuse/core": "^14.0.0", "@vueuse/core": "^14.0.0",
"electron-log": "^5.4.3", "electron-log": "^5.4.3",
@@ -87,11 +88,9 @@
"vue-router": "^4.6.3" "vue-router": "^4.6.3"
}, },
"devDependencies": { "devDependencies": {
"@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", "@types/fs-extra": "^11.0.4",
"@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",
"dotenv": "^17.2.3", "dotenv": "^17.2.3",
@@ -100,11 +99,7 @@
"electron-devtools-installer": "^4.0.0", "electron-devtools-installer": "^4.0.0",
"electron-extension-installer": "^2.0.0", "electron-extension-installer": "^2.0.0",
"eslint": "^9.37.0", "eslint": "^9.37.0",
"eslint-config-prettier": "^10.1.8", "eslint-plugin-format": "^1.0.2",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-n": "^17.23.1",
"eslint-plugin-prettier": "^5.5.4",
"eslint-plugin-vue": "^10.5.1",
"globals": "^16.4.0", "globals": "^16.4.0",
"jiti": "^2.6.1", "jiti": "^2.6.1",
"playwright": "^1.56.1", "playwright": "^1.56.1",
+4 -4
View File
@@ -1,4 +1,4 @@
import { defineConfig } from '@playwright/test' import { defineConfig } from '@playwright/test';
export default defineConfig({ export default defineConfig({
outputDir: 'tests/results', outputDir: 'tests/results',
@@ -6,6 +6,6 @@ export default defineConfig({
workers: process.env.CI ? 1 : undefined, workers: process.env.CI ? 1 : undefined,
timeout: 60000, timeout: 60000,
expect: { expect: {
timeout: 10000 timeout: 10000,
} },
}) });
+1643 -1039
View File
File diff suppressed because it is too large. Load diff
+39 -39
View File
@@ -1,63 +1,63 @@
import { dialog, ipcMain, app } from 'electron' import { dialog } from 'electron';
import { autoUpdater } from 'electron-updater' import log from 'electron-log/main';
import log from 'electron-log/main' import { autoUpdater } from 'electron-updater';
// 配置日志 // 配置日志
autoUpdater.logger = log autoUpdater.logger = log;
log.transports.file.level = 'info' log.transports.file.level = 'info';
export class AutoUpdater { export class AutoUpdater {
private mainWindow: Electron.BrowserWindow | null = null private mainWindow: Electron.BrowserWindow | null = null;
constructor(mainWindow: Electron.BrowserWindow) { constructor(mainWindow: Electron.BrowserWindow) {
this.mainWindow = mainWindow this.mainWindow = mainWindow;
// 配置更新检查间隔 // 配置更新检查间隔
autoUpdater.autoDownload = false autoUpdater.autoDownload = false;
autoUpdater.allowPrerelease = false autoUpdater.allowPrerelease = false;
autoUpdater.channel = 'latest' autoUpdater.channel = 'latest';
// 如果是私有仓库,设置认证信息 // 如果是私有仓库,设置认证信息
if (process.env.GH_TOKEN || process.env.GITHUB_TOKEN) { if (process.env.GH_TOKEN || process.env.GITHUB_TOKEN) {
// 使用环境变量中的令牌 // 使用环境变量中的令牌
autoUpdater.requestHeaders = { autoUpdater.requestHeaders = {
'Authorization': `token ${process.env.GH_TOKEN || process.env.GITHUB_TOKEN}` Authorization: `token ${process.env.GH_TOKEN || process.env.GITHUB_TOKEN}`,
} };
} }
// 设置事件监听器 // 设置事件监听器
this.setupEventListeners() this.setupEventListeners();
} }
private setupEventListeners() { private setupEventListeners() {
// 检查更新 // 检查更新
autoUpdater.on('checking-for-update', () => { autoUpdater.on('checking-for-update', () => {
log.info('正在检查更新...') log.info('正在检查更新...');
this.mainWindow?.webContents.send('update-checking') this.mainWindow?.webContents.send('update-checking');
}) });
// 发现新版本 // 发现新版本
autoUpdater.on('update-available', (info) => { autoUpdater.on('update-available', (info) => {
log.info('发现新版本:', info.version) log.info('发现新版本:', info.version);
this.mainWindow?.webContents.send('update-available', info) this.mainWindow?.webContents.send('update-available', info);
}) });
// 没有新版本 // 没有新版本
autoUpdater.on('update-not-available', (info) => { autoUpdater.on('update-not-available', (info) => {
log.info('当前已是最新版本') log.info('当前已是最新版本');
this.mainWindow?.webContents.send('update-not-available', info) this.mainWindow?.webContents.send('update-not-available', info);
}) });
// 更新下载进度 // 更新下载进度
autoUpdater.on('download-progress', (progress) => { autoUpdater.on('download-progress', (progress) => {
log.info('下载进度:', progress.percent, '%') log.info('下载进度:', progress.percent, '%');
this.mainWindow?.webContents.send('update-download-progress', progress) this.mainWindow?.webContents.send('update-download-progress', progress);
}) });
// 更新下载完成 // 更新下载完成
autoUpdater.on('update-downloaded', (info) => { autoUpdater.on('update-downloaded', (info) => {
log.info('更新下载完成') log.info('更新下载完成');
this.mainWindow?.webContents.send('update-downloaded', info) this.mainWindow?.webContents.send('update-downloaded', info);
// 提示用户是否重启应用安装更新 // 提示用户是否重启应用安装更新
dialog.showMessageBox({ dialog.showMessageBox({
@@ -67,31 +67,31 @@ export class AutoUpdater {
detail: '是否现在重启应用以安装更新?', detail: '是否现在重启应用以安装更新?',
buttons: ['稍后重启', '立即重启'], buttons: ['稍后重启', '立即重启'],
defaultId: 1, defaultId: 1,
cancelId: 0 cancelId: 0,
}).then((result) => { }).then((result) => {
if (result.response === 1) { if (result.response === 1) {
// 安装更新并重启应用 // 安装更新并重启应用
setImmediate(() => { setImmediate(() => {
autoUpdater.quitAndInstall() autoUpdater.quitAndInstall();
}) });
} }
}) });
}) });
// 更新错误 // 更新错误
autoUpdater.on('error', (error) => { autoUpdater.on('error', (error) => {
log.error('自动更新出错:', error) log.error('自动更新出错:', error);
this.mainWindow?.webContents.send('update-error', error) this.mainWindow?.webContents.send('update-error', error);
}) });
} }
public checkForUpdates() { public checkForUpdates() {
log.info('开始检查更新...') log.info('开始检查更新...');
autoUpdater.checkForUpdates() autoUpdater.checkForUpdates();
} }
public downloadUpdate() { public downloadUpdate() {
log.info('开始下载更新...') log.info('开始下载更新...');
autoUpdater.downloadUpdate() autoUpdater.downloadUpdate();
} }
} }
+128 -128
View File
@@ -1,17 +1,18 @@
import { ipcMain, shell, IpcMainEvent, dialog } from 'electron' import type { IpcMainEvent } from 'electron';
import Constants from './utils/Constants' import type { Template, TreeNode } from '../types';
import { join, dirname } from 'path' import { dirname, join } from 'node:path';
import { dialog, ipcMain, shell } from 'electron';
import { autoUpdater } from 'electron-updater';
import { import {
writeFile,
readFile,
pathExists,
ensureDir,
copy, copy,
ensureDir,
pathExists,
readdir, readdir,
readFile,
stat, stat,
} from 'fs-extra' writeFile,
import { autoUpdater } from 'electron-updater' } from 'fs-extra';
import { Template, TreeNode } from '../types' import Constants from './utils/Constants';
/* /*
* IPC Communications * IPC Communications
@@ -20,154 +21,151 @@ export default class IPCs {
static initialize(): void { static initialize(): void {
// Get application version // Get application version
ipcMain.handle('msgRequestGetVersion', () => { ipcMain.handle('msgRequestGetVersion', () => {
return Constants.APP_VERSION return Constants.APP_VERSION;
}) });
// Open url via web browser // Open url via web browser
ipcMain.on( ipcMain.on(
'msgOpenExternalLink', 'msgOpenExternalLink',
async (event: IpcMainEvent, url: string) => { async (event: IpcMainEvent, url: string) => {
await shell.openExternal(url) await shell.openExternal(url);
} },
) );
// Open file // Open file
ipcMain.handle( ipcMain.handle(
'msgOpenFile', 'msgOpenFile',
async (event: IpcMainEvent, filter: string) => { async (event: IpcMainEvent, filter: string) => {
const filters = [] const filters = [];
if (filter === 'text') { if (filter === 'text') {
filters.push({ name: 'Text', extensions: ['txt', 'json'] }) filters.push({ name: 'Text', extensions: ['txt', 'json'] });
} else if (filter === 'zip') { } else if (filter === 'zip') {
filters.push({ name: 'Zip', extensions: ['zip'] }) filters.push({ name: 'Zip', extensions: ['zip'] });
} else if (filter === 'any') { } else if (filter === 'any') {
filters.push({ name: 'All Files', extensions: ['*'] }) filters.push({ name: 'All Files', extensions: ['*'] });
} }
const dialogResult = await dialog.showOpenDialog({ const dialogResult = await dialog.showOpenDialog({
properties: ['openFile'], properties: ['openFile'],
filters filters,
}) });
return dialogResult return dialogResult;
} },
) );
// Select directory // Select directory
ipcMain.handle('msgSelectDirectory', async () => { ipcMain.handle('msgSelectDirectory', async () => {
const dialogResult = await dialog.showOpenDialog({ const dialogResult = await dialog.showOpenDialog({
properties: ['openDirectory'] properties: ['openDirectory'],
}) });
if (!dialogResult.canceled && dialogResult.filePaths.length > 0) { if (!dialogResult.canceled && dialogResult.filePaths.length > 0) {
return { return {
canceled: false, canceled: false,
filePath: dialogResult.filePaths[0], filePath: dialogResult.filePaths[0],
isDirectory: true isDirectory: true,
} };
} }
return { canceled: true } return { canceled: true };
}) });
// Select file or directory // Select file or directory
ipcMain.handle('msgSelectFileOrFolder', async () => { ipcMain.handle('msgSelectFileOrFolder', async () => {
const dialogResult = await dialog.showOpenDialog({ const dialogResult = await dialog.showOpenDialog({
properties: ['openFile', 'openDirectory'] properties: ['openFile', 'openDirectory'],
}) });
if (!dialogResult.canceled && dialogResult.filePaths.length > 0) { if (!dialogResult.canceled && dialogResult.filePaths.length > 0) {
const filePath = dialogResult.filePaths[0]; const filePath = dialogResult.filePaths[0];
const fs = require('fs'); const fs = require('node:fs');
const isDirectory = fs.statSync(filePath).isDirectory(); const isDirectory = fs.statSync(filePath).isDirectory();
return { return {
canceled: false, canceled: false,
filePath: filePath, filePath,
isDirectory: isDirectory isDirectory,
} };
} }
return { canceled: true } return { canceled: true };
}) });
// Find all files in a folder recursively // Find all files in a folder recursively
ipcMain.handle( ipcMain.handle(
'msgFindFilesInFolder', 'msgFindFilesInFolder',
async (event: IpcMainEvent, folderPath: string) => { async (event: IpcMainEvent, folderPath: string) => {
try { try {
const foundFiles: string[] = [];
const foundFiles: string[] = []
const searchFiles = async (dir: string) => { const searchFiles = async (dir: string) => {
const items = await readdir(dir) const items = await readdir(dir);
for (const item of items) { for (const item of items) {
const fullPath = join(dir, item) const fullPath = join(dir, item);
const stats = await stat(fullPath) const stats = await stat(fullPath);
if (stats.isDirectory()) { if (stats.isDirectory()) {
// Recursively search in subdirectories // Recursively search in subdirectories
await searchFiles(fullPath) await searchFiles(fullPath);
} else if (stats.isFile()) { } else if (stats.isFile()) {
// Add file to the list // Add file to the list
foundFiles.push(fullPath) foundFiles.push(fullPath);
} }
} }
} };
await searchFiles(folderPath) await searchFiles(folderPath);
return foundFiles return foundFiles;
} catch (error) { } catch (error) {
console.error('Error finding files in folder:', error) console.error('Error finding files in folder:', error);
return [] return [];
} }
} },
) );
// Load data from local file // Load data from local file
ipcMain.handle( ipcMain.handle(
'msgLoadData', 'msgLoadData',
async (event: IpcMainEvent, fileName: string) => { async (event: IpcMainEvent, fileName: string) => {
try { try {
const userDataPath = join(Constants.APP_DATA_PATH, 'data') const userDataPath = join(Constants.APP_DATA_PATH, 'data');
const filePath = join(userDataPath, fileName) const filePath = join(userDataPath, fileName);
// Ensure directory exists // Ensure directory exists
await ensureDir(userDataPath) await ensureDir(userDataPath);
if (await pathExists(filePath)) { if (await pathExists(filePath)) {
const data = await readFile(filePath, 'utf-8') const data = await readFile(filePath, 'utf-8');
return data return data;
} }
return null return null;
} catch (error) { } catch (error) {
console.error('Error loading data:', error) console.error('Error loading data:', error);
return null return null;
} }
} },
) );
// Save data to local file // Save data to local file
ipcMain.handle( ipcMain.handle(
'msgSaveData', 'msgSaveData',
async (event: IpcMainEvent, fileName: string, data: string) => { async (event: IpcMainEvent, fileName: string, data: string) => {
try { try {
const userDataPath = join(Constants.APP_DATA_PATH, 'data') const userDataPath = join(Constants.APP_DATA_PATH, 'data');
const filePath = join(userDataPath, fileName) const filePath = join(userDataPath, fileName);
// Ensure directory exists // Ensure directory exists
await ensureDir(userDataPath) await ensureDir(userDataPath);
await writeFile(filePath, data, 'utf-8') await writeFile(filePath, data, 'utf-8');
return true return true;
} catch (error) { } catch (error) {
console.error('Error saving data:', error) console.error('Error saving data:', error);
return false return false;
} }
} },
) );
// Execute template // Execute template
ipcMain.handle( ipcMain.handle(
@@ -176,39 +174,41 @@ export default class IPCs {
event: IpcMainEvent, event: IpcMainEvent,
templateId: string, templateId: string,
targetPath: string, targetPath: string,
baseDirNames: string[] baseDirNames: string[],
) => { ) => {
try { try {
// Load templates data // Load templates data
const userDataPath = join(Constants.APP_DATA_PATH, 'data') const userDataPath = join(Constants.APP_DATA_PATH, 'data');
const filePath = join(userDataPath, 'templates.json') const filePath = join(userDataPath, 'templates.json');
let templates: Template[] = [] let templates: Template[] = [];
if (await pathExists(filePath)) { if (await pathExists(filePath)) {
const templatesData = await readFile(filePath, 'utf-8') const templatesData = await readFile(filePath, 'utf-8');
templates = JSON.parse(templatesData) templates = JSON.parse(templatesData);
} }
const template = templates.find((t) => t.id === templateId) const template = templates.find(t => t.id === templateId);
if (!template) { if (!template) {
return { success: false, error: 'Template not found' } return { success: false, error: 'Template not found' };
} }
// Create directories and files for each base directory name // Create directories and files for each base directory name
for (const baseDirName of baseDirNames) { for (const baseDirName of baseDirNames) {
if (!baseDirName.trim()) continue if (!baseDirName.trim()) {
continue;
}
// Create base directory // Create base directory
const baseDirPath = join(targetPath, baseDirName) const baseDirPath = join(targetPath, baseDirName);
await ensureDir(baseDirPath) await ensureDir(baseDirPath);
// Helper function to traverse the tree and collect all nodes // Helper function to traverse the tree and collect all nodes
const collectAllNodes = (nodes: TreeNode[], allNodes: TreeNode[] = [], parent?: TreeNode) => { const collectAllNodes = (nodes: TreeNode[], allNodes: TreeNode[] = [], parent?: TreeNode) => {
for (const node of nodes) { for (const node of nodes) {
allNodes.push({ allNodes.push({
...node, ...node,
path: join(parent?.path || '', node.path) path: join(parent?.path || '', node.path),
}); });
if (node.children) { if (node.children) {
collectAllNodes(node.children, allNodes, node); collectAllNodes(node.children, allNodes, node);
@@ -235,7 +235,7 @@ export default class IPCs {
await copy(node.sourcePath, nodePath); await copy(node.sourcePath, nodePath);
} else { } else {
// Write content directly // Write content directly
const files = await readdir(node.sourcePath, { withFileTypes: true }) const files = await readdir(node.sourcePath, { withFileTypes: true });
for (const file of files) { for (const file of files) {
if (file.isFile() && file.name.startsWith(baseDirName)) { if (file.isFile() && file.name.startsWith(baseDirName)) {
const filePath = join(node.sourcePath, file.name); const filePath = join(node.sourcePath, file.name);
@@ -248,91 +248,91 @@ export default class IPCs {
} }
} }
return { success: true } return { success: true };
} catch (error) { } catch (error) {
console.error('Error executing template:', error) console.error('Error executing template:', error);
return { success: false, error: error.message } return { success: false, error: error.message };
} }
} },
) );
// 检查更新 // 检查更新
ipcMain.handle('msgCheckForUpdates', async () => { ipcMain.handle('msgCheckForUpdates', async () => {
if (Constants.IS_DEV_ENV) { if (Constants.IS_DEV_ENV) {
// 在开发环境中模拟更新检查 // 在开发环境中模拟更新检查
console.log('开发环境中模拟检查更新...') console.log('开发环境中模拟检查更新...');
return { updateAvailable: false, currentVersion: Constants.APP_VERSION } return { updateAvailable: false, currentVersion: Constants.APP_VERSION };
} }
return new Promise((resolve) => { return new Promise((resolve) => {
// 监听更新事件 // 监听更新事件
const removeListeners = () => { const removeListeners = () => {
autoUpdater.removeListener('update-available', onUpdateAvailable) autoUpdater.removeListener('update-available', onUpdateAvailable);
autoUpdater.removeListener('update-not-available', onUpdateNotAvailable) autoUpdater.removeListener('update-not-available', onUpdateNotAvailable);
autoUpdater.removeListener('error', onError) autoUpdater.removeListener('error', onError);
} };
const onUpdateAvailable = () => { const onUpdateAvailable = () => {
removeListeners() removeListeners();
resolve({ updateAvailable: true, currentVersion: Constants.APP_VERSION }) resolve({ updateAvailable: true, currentVersion: Constants.APP_VERSION });
} };
const onUpdateNotAvailable = () => { const onUpdateNotAvailable = () => {
removeListeners() removeListeners();
resolve({ updateAvailable: false, currentVersion: Constants.APP_VERSION }) resolve({ updateAvailable: false, currentVersion: Constants.APP_VERSION });
} };
const onError = (error: Error) => { const onError = (error: Error) => {
removeListeners() removeListeners();
console.error('检查更新时出错:', error) console.error('检查更新时出错:', error);
resolve({ updateAvailable: false, currentVersion: Constants.APP_VERSION, error: error.message }) resolve({ updateAvailable: false, currentVersion: Constants.APP_VERSION, error: error.message });
} };
// 添加事件监听器 // 添加事件监听器
autoUpdater.once('update-available', onUpdateAvailable) autoUpdater.once('update-available', onUpdateAvailable);
autoUpdater.once('update-not-available', onUpdateNotAvailable) autoUpdater.once('update-not-available', onUpdateNotAvailable);
autoUpdater.once('error', onError) autoUpdater.once('error', onError);
// 开始检查更新 // 开始检查更新
autoUpdater.checkForUpdates() autoUpdater.checkForUpdates();
}) });
}) });
// 下载更新 // 下载更新
ipcMain.handle('msgDownloadUpdate', async () => { ipcMain.handle('msgDownloadUpdate', async () => {
if (Constants.IS_DEV_ENV) { if (Constants.IS_DEV_ENV) {
// 在开发环境中模拟下载更新 // 在开发环境中模拟下载更新
console.log('开发环境中模拟下载更新...') console.log('开发环境中模拟下载更新...');
return { success: true } return { success: true };
} }
try { try {
await autoUpdater.downloadUpdate() await autoUpdater.downloadUpdate();
return { success: true } return { success: true };
} catch (error) { } catch (error) {
console.error('下载更新时出错:', error) console.error('下载更新时出错:', error);
return { success: false, error: error instanceof Error ? error.message : String(error) } return { success: false, error: error instanceof Error ? error.message : String(error) };
} }
}) });
// 安装更新 // 安装更新
ipcMain.handle('msgInstallUpdate', async () => { ipcMain.handle('msgInstallUpdate', async () => {
if (Constants.IS_DEV_ENV) { if (Constants.IS_DEV_ENV) {
// 在开发环境中模拟安装更新 // 在开发环境中模拟安装更新
console.log('开发环境中模拟安装更新...') console.log('开发环境中模拟安装更新...');
return { success: true } return { success: true };
} }
try { try {
// 立即退出并安装更新 // 立即退出并安装更新
setImmediate(() => { setImmediate(() => {
autoUpdater.quitAndInstall() autoUpdater.quitAndInstall();
}) });
return { success: true } return { success: true };
} catch (error) { } catch (error) {
console.error('安装更新时出错:', error) console.error('安装更新时出错:', error);
return { success: false, error: error instanceof Error ? error.message : String(error) } return { success: false, error: error instanceof Error ? error.message : String(error) };
} }
}) });
} }
} }
+61 -59
View File
@@ -1,13 +1,16 @@
import type {
BrowserWindowConstructorOptions,
RenderProcessGoneDetails,
} from 'electron';
import type { TrayOptions } from './utils/Constants';
import { import {
app, app,
BrowserWindow, BrowserWindow,
RenderProcessGoneDetails, } from 'electron';
BrowserWindowConstructorOptions import log from 'electron-log/main';
} from 'electron' import IPCs from './IPCs';
import Constants, { TrayOptions } from './utils/Constants' import { createTray, hideWindow, showWindow } from './tray';
import IPCs from './IPCs' import Constants from './utils/Constants';
import { createTray, hideWindow, showWindow } from './tray'
import log from 'electron-log/main'
const options = { const options = {
width: Constants.IS_DEV_ENV ? 1500 : 1200, width: Constants.IS_DEV_ENV ? 1500 : 1200,
@@ -16,20 +19,20 @@ const options = {
// all optional values from DEFAULT_TRAY_OPTIONS can de defined here // all optional values from DEFAULT_TRAY_OPTIONS can de defined here
enabled: true, enabled: true,
menu: false, // true, to use a tray menu ; false to toggle visibility on click on tray icon menu: false, // true, to use a tray menu ; false to toggle visibility on click on tray icon
trayWindow: false // true, to use a tray floating window attached to top try icon trayWindow: false, // true, to use a tray floating window attached to top try icon
} },
} };
const exitApp = (mainWindow: BrowserWindow): void => { const exitApp = (mainWindow: BrowserWindow): void => {
if (mainWindow && !mainWindow.isDestroyed()) { if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.hide() mainWindow.hide();
} }
mainWindow.destroy() mainWindow.destroy();
app.exit() app.exit();
} };
export const createMainWindow = async (): Promise<BrowserWindow> => { export const createMainWindow = async (): Promise<BrowserWindow> => {
log.silly('Creating new window...') log.silly('Creating new window...');
let opt: BrowserWindowConstructorOptions = { let opt: BrowserWindowConstructorOptions = {
title: Constants.APP_NAME, title: Constants.APP_NAME,
@@ -38,17 +41,17 @@ export const createMainWindow = async (): Promise<BrowserWindow> => {
height: options.height, height: options.height,
useContentSize: true, useContentSize: true,
webPreferences: Constants.DEFAULT_WEB_PREFERENCES, webPreferences: Constants.DEFAULT_WEB_PREFERENCES,
frame: true frame: true,
} };
const trayOptions: TrayOptions = options.tray?.enabled const trayOptions: TrayOptions = options.tray?.enabled
? { ? {
...Constants.DEFAULT_TRAY_OPTIONS, ...Constants.DEFAULT_TRAY_OPTIONS,
...options.tray ...options.tray,
} }
: { : {
...Constants.DEFAULT_TRAY_OPTIONS, ...Constants.DEFAULT_TRAY_OPTIONS,
enabled: false enabled: false,
} };
// trayWindow requires tray.enabled=true // trayWindow requires tray.enabled=true
if (trayOptions.enabled && trayOptions.trayWindow) { if (trayOptions.enabled && trayOptions.trayWindow) {
@@ -67,93 +70,92 @@ export const createMainWindow = async (): Promise<BrowserWindow> => {
alwaysOnTop: true, alwaysOnTop: true,
webPreferences: { webPreferences: {
...Constants.DEFAULT_WEB_PREFERENCES, ...Constants.DEFAULT_WEB_PREFERENCES,
backgroundThrottling: false backgroundThrottling: false,
} },
} };
} }
const mainWindow = new BrowserWindow(opt) const mainWindow = new BrowserWindow(opt);
mainWindow.setMenu(null) mainWindow.setMenu(null);
mainWindow.on('close', (event: Event): void => { mainWindow.on('close', (event: Event): void => {
event.preventDefault() event.preventDefault();
exitApp(mainWindow) exitApp(mainWindow);
}) });
mainWindow.webContents.on('did-frame-finish-load', (): void => { mainWindow.webContents.on('did-frame-finish-load', (): void => {
if (Constants.IS_DEV_ENV && Constants.FEAT_OPEN_DEV_TOOLS_AT_START) { if (Constants.IS_DEV_ENV && Constants.FEAT_OPEN_DEV_TOOLS_AT_START) {
mainWindow.webContents.openDevTools() mainWindow.webContents.openDevTools();
} }
}) });
if (trayOptions.enabled) { if (trayOptions.enabled) {
createTray(mainWindow, trayOptions) createTray(mainWindow, trayOptions);
} }
if (trayOptions.enabled && trayOptions.trayWindow) { if (trayOptions.enabled && trayOptions.trayWindow) {
hideWindow(mainWindow) hideWindow(mainWindow);
if (trayOptions.showAtStartup) { if (trayOptions.showAtStartup) {
showWindow(mainWindow) showWindow(mainWindow);
} }
} else { } else {
mainWindow.once('ready-to-show', (): void => { mainWindow.once('ready-to-show', (): void => {
mainWindow.setAlwaysOnTop(true) mainWindow.setAlwaysOnTop(true);
mainWindow.show() mainWindow.show();
mainWindow.focus() mainWindow.focus();
mainWindow.setAlwaysOnTop(false) mainWindow.setAlwaysOnTop(false);
}) });
} }
// Initialize IPC Communication // Initialize IPC Communication
IPCs.initialize() IPCs.initialize();
if (Constants.IS_DEV_ENV) { if (Constants.IS_DEV_ENV) {
await mainWindow.loadURL(Constants.APP_INDEX_URL_DEV) await mainWindow.loadURL(Constants.APP_INDEX_URL_DEV);
} else { } else {
await mainWindow.loadFile(Constants.APP_INDEX_URL_PROD) await mainWindow.loadFile(Constants.APP_INDEX_URL_PROD);
} }
return mainWindow return mainWindow;
} };
export const createErrorWindow = async ( export const createErrorWindow = async (
errorWindow: BrowserWindow, errorWindow: BrowserWindow,
mainWindow: BrowserWindow, mainWindow: BrowserWindow,
// eslint-disable-next-line @typescript-eslint/no-unused-vars _details?: RenderProcessGoneDetails,
details?: RenderProcessGoneDetails
): Promise<BrowserWindow> => { ): Promise<BrowserWindow> => {
if (!Constants.IS_DEV_ENV) { if (!Constants.IS_DEV_ENV) {
mainWindow?.hide() mainWindow?.hide();
} }
errorWindow = new BrowserWindow({ errorWindow = new BrowserWindow({
title: Constants.APP_NAME, title: Constants.APP_NAME,
show: false, show: false,
resizable: Constants.IS_DEV_ENV, resizable: Constants.IS_DEV_ENV,
webPreferences: Constants.DEFAULT_WEB_PREFERENCES webPreferences: Constants.DEFAULT_WEB_PREFERENCES,
}) });
errorWindow.setMenu(null) errorWindow.setMenu(null);
if (Constants.IS_DEV_ENV) { if (Constants.IS_DEV_ENV) {
await errorWindow.loadURL(`${Constants.APP_INDEX_URL_DEV}#/error`) await errorWindow.loadURL(`${Constants.APP_INDEX_URL_DEV}#/error`);
} else { } else {
await errorWindow.loadFile(Constants.APP_INDEX_URL_PROD, { hash: 'error' }) await errorWindow.loadFile(Constants.APP_INDEX_URL_PROD, { hash: 'error' });
} }
errorWindow.on('ready-to-show', (): void => { errorWindow.on('ready-to-show', (): void => {
if (!Constants.IS_DEV_ENV && mainWindow && !mainWindow.isDestroyed()) { if (!Constants.IS_DEV_ENV && mainWindow && !mainWindow.isDestroyed()) {
mainWindow.destroy() mainWindow.destroy();
} }
errorWindow.show() errorWindow.show();
errorWindow.focus() errorWindow.focus();
}) });
errorWindow.webContents.on('did-frame-finish-load', (): void => { errorWindow.webContents.on('did-frame-finish-load', (): void => {
if (Constants.IS_DEV_ENV) { if (Constants.IS_DEV_ENV) {
errorWindow.webContents.openDevTools() errorWindow.webContents.openDevTools();
} }
}) });
return errorWindow return errorWindow;
} };
+8 -8
View File
@@ -1,22 +1,22 @@
import { app } from 'electron';
// Warning: This file is only used in the development environment // Warning: This file is only used in the development environment
// and is removed at build time. // and is removed at build time.
// Do not edit the file unless necessary. // Do not edit the file unless necessary.
import { installExtension } from 'electron-extension-installer' import { installExtension } from 'electron-extension-installer';
import { app } from 'electron'
const vue3DevTools = { const vue3DevTools = {
id: 'nhdogjmejiglipccpnnnanhbledajbpd', id: 'nhdogjmejiglipccpnnnanhbledajbpd',
version: '7.7.0' version: '7.7.0',
} };
try { try {
app.on('ready', async () => { app.on('ready', async () => {
await installExtension(vue3DevTools, { await installExtension(vue3DevTools, {
loadExtensionOptions: { loadExtensionOptions: {
allowFileAccess: true allowFileAccess: true,
} },
}) });
}) });
} catch { } catch {
// Do nothing // Do nothing
} }
+39 -39
View File
@@ -1,46 +1,46 @@
import { app, WebContents, RenderProcessGoneDetails } from 'electron' import type { RenderProcessGoneDetails, WebContents } from 'electron';
import Constants from './utils/Constants' import { join } from 'node:path';
import { createErrorWindow, createMainWindow } from './MainRunner' import { app } from 'electron';
import log from 'electron-log/main' import log from 'electron-log/main';
import { join } from 'path' import { AutoUpdater } from './AutoUpdater';
import { AutoUpdater } from './AutoUpdater' import { createErrorWindow, createMainWindow } from './MainRunner';
import Constants from './utils/Constants';
let mainWindow let mainWindow;
let errorWindow let errorWindow;
let autoUpdater: AutoUpdater | null = null
const initializeMainLogger = () => { const initializeMainLogger = () => {
log.initialize({ log.initialize({
includeFutureSessions: false, includeFutureSessions: false,
preload: true preload: true,
}) });
const appLogFilePath = join(app.getPath('userData'), 'logs', 'applog.log') const appLogFilePath = join(app.getPath('userData'), 'logs', 'applog.log');
log.transports.file.resolvePathFn = () => log.transports.file.resolvePathFn = () =>
join(app.getPath('userData'), 'logs', 'applog.log') join(app.getPath('userData'), 'logs', 'applog.log');
log.transports.file.level = 'silly' log.transports.file.level = 'silly';
log.transports.file.format = '[{y}{m}{d} {h}:{i}:{s}.{ms}|{level}]{text}' log.transports.file.format = '[{y}{m}{d} {h}:{i}:{s}.{ms}|{level}]{text}';
log.transports.console.format = '{h}:{i}:{s}.{ms} {text}' log.transports.console.format = '{h}:{i}:{s}.{ms} {text}';
log.transports.console.level = 'silly' log.transports.console.level = 'silly';
log.silly(`Start logging... (Path: ${appLogFilePath}) App is ready.`) log.silly(`Start logging... (Path: ${appLogFilePath}) App is ready.`);
} };
app.on('ready', async () => { app.on('ready', async () => {
if (Constants.IS_DEV_ENV) { if (Constants.IS_DEV_ENV) {
import('./index.dev') import('./index.dev');
// Install Vue DevTools // Install Vue DevTools
try { try {
const { default: installExtension, VUEJS_DEVTOOLS } = await import('electron-devtools-installer') const { default: installExtension, VUEJS_DEVTOOLS } = await import('electron-devtools-installer');
await installExtension(VUEJS_DEVTOOLS, { await installExtension(VUEJS_DEVTOOLS, {
loadExtensionOptions: { loadExtensionOptions: {
allowFileAccess: true, allowFileAccess: true,
}, },
}) });
} catch (err) { } catch (err) {
console.log('Failed to install Vue DevTools: ', err) console.log('Failed to install Vue DevTools: ', err);
} }
} }
@@ -51,42 +51,42 @@ app.on('ready', async () => {
systemPreferences.setUserDefault('NSDisabledCharacterPaletteMenuItem', 'boolean', true) systemPreferences.setUserDefault('NSDisabledCharacterPaletteMenuItem', 'boolean', true)
} }
*/ */
initializeMainLogger() initializeMainLogger();
mainWindow = await createMainWindow() mainWindow = await createMainWindow();
// Initialize auto updater after main window is created // Initialize auto updater after main window is created
if (!Constants.IS_DEV_ENV) { if (!Constants.IS_DEV_ENV) {
autoUpdater = new AutoUpdater(mainWindow) autoUpdater = new AutoUpdater(mainWindow);
} }
}) });
app.on('activate', async () => { app.on('activate', async () => {
if (!mainWindow) { if (!mainWindow) {
mainWindow = await createMainWindow() mainWindow = await createMainWindow();
} }
}) });
app.on('window-all-closed', () => { app.on('window-all-closed', () => {
mainWindow = null mainWindow = null;
errorWindow = null errorWindow = null;
if (!Constants.IS_MAC) { if (!Constants.IS_MAC) {
app.quit() app.quit();
} }
}) });
app.on( app.on(
'render-process-gone', 'render-process-gone',
( (
event: Event, event: Event,
webContents: WebContents, webContents: WebContents,
details: RenderProcessGoneDetails details: RenderProcessGoneDetails,
) => { ) => {
errorWindow = createErrorWindow(errorWindow, mainWindow, details) errorWindow = createErrorWindow(errorWindow, mainWindow, details);
} },
) );
process.on('uncaughtException', () => { process.on('uncaughtException', () => {
errorWindow = createErrorWindow(errorWindow, mainWindow) errorWindow = createErrorWindow(errorWindow, mainWindow);
}) });
+54 -50
View File
@@ -1,111 +1,115 @@
import { app, screen, Menu, Tray, BrowserWindow } from 'electron' import type { BrowserWindow } from 'electron';
import Constants from './utils/Constants' import { join } from 'node:path';
import { join } from 'path' import { app, Menu, screen, Tray } from 'electron';
import { debounce } from 'qsu' import { debounce } from 'qsu';
let tray import Constants from './utils/Constants';
let trayOptions
let tray;
let trayOptions;
export function createTray(window: BrowserWindow, options) { export function createTray(window: BrowserWindow, options) {
trayOptions = options || Constants.DEFAULT_TRAY_OPTIONS trayOptions = options || Constants.DEFAULT_TRAY_OPTIONS;
// menu or trayWindow, you need to choose // menu or trayWindow, you need to choose
if (trayOptions.trayWindow) { if (trayOptions.trayWindow) {
trayOptions.menu = false trayOptions.menu = false;
} }
tray = new Tray(join(Constants.PUBLIC_PATH, 'images/vutron-tray-icon.png')) tray = new Tray(join(Constants.PUBLIC_PATH, 'images/vutron-tray-icon.png'));
tray.setToolTip(trayOptions.tooltip) tray.setToolTip(trayOptions.tooltip);
if (trayOptions.menu) { if (trayOptions.menu) {
const contextMenu = Menu.buildFromTemplate([ const contextMenu = Menu.buildFromTemplate([
{ {
label: 'Show App', label: 'Show App',
click: () => { click: () => {
showWindow(window) showWindow(window);
} },
}, },
{ {
label: 'Hide App', label: 'Hide App',
click: () => { click: () => {
hideWindow(window) hideWindow(window);
} },
}, },
{ {
label: 'Exit', label: 'Exit',
click: () => { click: () => {
app.quit() app.quit();
} },
} },
]) ]);
// tray icon only with classic window // tray icon only with classic window
tray.setContextMenu(contextMenu) tray.setContextMenu(contextMenu);
} else { } else {
// handle click on tray icon // handle click on tray icon
tray.on('right-click', function () { tray.on('right-click', () => {
debounce(() => toggleWindow(window), 200) debounce(() => toggleWindow(window), 200);
}) });
tray.on('click', function () { tray.on('click', () => {
debounce(() => toggleWindow(window), 200) debounce(() => toggleWindow(window), 200);
}) });
// no menu for tray window // no menu for tray window
window.setMenu(null) window.setMenu(null);
tray.setContextMenu(null) tray.setContextMenu(null);
} }
// align at startup // align at startup
alignWindow(window) alignWindow(window);
return tray return tray;
} }
export function hideWindow(window: BrowserWindow) { export function hideWindow(window: BrowserWindow) {
window.hide() window.hide();
// if (!trayOptions.trayWindow) return; // if (!trayOptions.trayWindow) return;
// hide window when click elsewhere on screen // hide window when click elsewhere on screen
window.on('blur', () => { window.on('blur', () => {
// dont close if devtools // dont close if devtools
if (!window.webContents.isDevToolsOpened()) { if (!window.webContents.isDevToolsOpened()) {
window.hide() window.hide();
} }
}) });
} }
export function toggleWindow(window: BrowserWindow) { export function toggleWindow(window: BrowserWindow) {
if (window.isVisible()) { if (window.isVisible()) {
hideWindow(window) hideWindow(window);
} else { } else {
showWindow(window) showWindow(window);
} }
} }
export function showWindow(window: BrowserWindow) { export function showWindow(window: BrowserWindow) {
window.show() window.show();
alignWindow(window) alignWindow(window);
} }
export function alignWindow(window: BrowserWindow) { export function alignWindow(window: BrowserWindow) {
if (!trayOptions.trayWindow) return if (!trayOptions.trayWindow) {
return;
}
const b = window.getBounds() const b = window.getBounds();
const position = calculateWindowPosition(b) const position = calculateWindowPosition(b);
window.setBounds({ window.setBounds({
width: b.width, width: b.width,
height: b.height, height: b.height,
x: position.x, x: position.x,
y: position.y y: position.y,
}) });
} }
function calculateWindowPosition(b) { function calculateWindowPosition(b) {
const margin = trayOptions.margin const margin = trayOptions.margin;
const screenBounds = screen.getPrimaryDisplay().size const screenBounds = screen.getPrimaryDisplay().size;
const trayBounds = tray.getBounds() const trayBounds = tray.getBounds();
const bottom = trayBounds.y > screenBounds.height / 2 const bottom = trayBounds.y > screenBounds.height / 2;
const x = Math.floor( const x = Math.floor(
trayBounds.x - b.width / 2 - margin.x + trayBounds.width / 2 trayBounds.x - b.width / 2 - margin.x + trayBounds.width / 2,
) );
const y = bottom const y = bottom
? Math.floor(trayBounds.y - b.height - margin.y + trayBounds.height / 2) ? Math.floor(trayBounds.y - b.height - margin.y + trayBounds.height / 2)
: Math.floor(trayBounds.y + margin.y + trayBounds.height / 2) : Math.floor(trayBounds.y + margin.y + trayBounds.height / 2);
// constraint into screen // constraint into screen
return { return {
x: Math.max(0, Math.min(screenBounds.width - b.width, x)), x: Math.max(0, Math.min(screenBounds.width - b.width, x)),
y: Math.max(0, Math.min(screenBounds.height - b.height, y)) y: Math.max(0, Math.min(screenBounds.height - b.height, y)),
} };
} }
+24 -24
View File
@@ -1,17 +1,17 @@
import { join, dirname, resolve } from 'path' import { dirname, join, resolve } from 'node:path';
import { name, version, debug } from '../../../package.json' import { fileURLToPath } from 'node:url';
import { fileURLToPath } from 'url' import { app } from 'electron';
import { app } from 'electron' import { debug, name, version } from '../../../package.json';
const __dirname = dirname(fileURLToPath(import.meta.url)) const __dirname = dirname(fileURLToPath(import.meta.url));
export interface TrayOptions { export interface TrayOptions {
enabled: boolean enabled: boolean;
trayWindow: boolean trayWindow: boolean;
menu: boolean menu: boolean;
tooltip: string tooltip: string;
margin: { x: number; y: number } margin: { x: number; y: number };
showAtStartup: boolean showAtStartup: boolean;
} }
export default class Constants { export default class Constants {
@@ -23,32 +23,32 @@ export default class Constants {
* ------------------------------------------------------ */ * ------------------------------------------------------ */
// To show devtools at startup. It requires IS_DEV_ENV=true. // To show devtools at startup. It requires IS_DEV_ENV=true.
// Note: For debugging purpose, window won't be closed if click elsewhere, if devtools is open. // Note: For debugging purpose, window won't be closed if click elsewhere, if devtools is open.
static FEAT_OPEN_DEV_TOOLS_AT_START = true static FEAT_OPEN_DEV_TOOLS_AT_START = true;
/* ------------------------------------------------------ /* ------------------------------------------------------
* END OF FEATURES * END OF FEATURES
* ------------------------------------------------------ */ * ------------------------------------------------------ */
// Display app name (uppercase first letter) // Display app name (uppercase first letter)
static APP_NAME = name.charAt(0).toUpperCase() + name.slice(1) static APP_NAME = name.charAt(0).toUpperCase() + name.slice(1);
static APP_VERSION = version static APP_VERSION = version;
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 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, '..');
static IS_MAC = process.platform === 'darwin' static IS_MAC = process.platform === 'darwin';
static DEFAULT_WEB_PREFERENCES = { static DEFAULT_WEB_PREFERENCES = {
nodeIntegration: false, nodeIntegration: false,
contextIsolation: true, contextIsolation: true,
enableRemoteModule: false, enableRemoteModule: false,
preload: join(__dirname, '../preload/index.js') preload: join(__dirname, '../preload/index.js'),
} };
static DEFAULT_TRAY_OPTIONS: TrayOptions = { static DEFAULT_TRAY_OPTIONS: TrayOptions = {
enabled: false, enabled: false,
@@ -56,9 +56,9 @@ export default class Constants {
menu: false, menu: false,
tooltip: 'Vutron App', tooltip: 'Vutron App',
margin: { x: 0, y: 0 }, margin: { x: 0, y: 0 },
showAtStartup: false showAtStartup: false,
} };
static APP_INDEX_URL_DEV = `${debug.env.VITE_DEV_SERVER_URL}/index.html` static APP_INDEX_URL_DEV = `${debug.env.VITE_DEV_SERVER_URL}/index.html`;
static APP_INDEX_URL_PROD = join(__dirname, '../index.html') static APP_INDEX_URL_PROD = join(__dirname, '../index.html');
} }
+28 -27
View File
@@ -1,9 +1,10 @@
import log from 'electron-log/renderer' import type { IpcRendererEvent } from 'electron';
import { contextBridge, ipcRenderer, IpcRendererEvent } from 'electron' import { contextBridge, ipcRenderer } from 'electron';
import log from 'electron-log/renderer';
// Initialize renderer logger // Initialize renderer logger
log.transports.console.level = 'silly' log.transports.console.level = 'silly';
log.transports.console.format = '{h}:{i}:{s}.{ms} {text}' log.transports.console.format = '{h}:{i}:{s}.{ms} {text}';
// Whitelist of valid channels used for IPC communication (Send message from Renderer to Main) // Whitelist of valid channels used for IPC communication (Send message from Renderer to Main)
const mainAvailChannels: string[] = [ const mainAvailChannels: string[] = [
@@ -18,70 +19,70 @@ const mainAvailChannels: string[] = [
'msgFindFilesInFolder', 'msgFindFilesInFolder',
'msgCheckForUpdates', 'msgCheckForUpdates',
'msgDownloadUpdate', 'msgDownloadUpdate',
'msgInstallUpdate' 'msgInstallUpdate',
] ];
const rendererAvailChannels: string[] = [ const rendererAvailChannels: string[] = [
'update-checking', 'update-checking',
'update-available', 'update-available',
'update-not-available', 'update-not-available',
'update-download-progress', 'update-download-progress',
'update-downloaded', 'update-downloaded',
'update-error' 'update-error',
] ];
contextBridge.exposeInMainWorld('mainApi', { contextBridge.exposeInMainWorld('mainApi', {
send: (channel: string, ...data: any[]): void => { send: (channel: string, ...data: any[]): void => {
if (mainAvailChannels.includes(channel)) { if (mainAvailChannels.includes(channel)) {
ipcRenderer.send.apply(null, [channel, ...data]) ipcRenderer.send.apply(null, [channel, ...data]);
if (process.env.NODE_ENV === 'development') { if (process.env.NODE_ENV === 'development') {
log.silly(`[IPC_SEND::${channel}]`, { request: data }) log.silly(`[IPC_SEND::${channel}]`, { request: data });
} }
} else { } else {
throw new Error(`Unknown ipc channel name: ${channel}`) throw new Error(`Unknown ipc channel name: ${channel}`);
} }
}, },
on: ( on: (
channel: string, channel: string,
listener: (event: IpcRendererEvent, ...args: any[]) => void listener: (event: IpcRendererEvent, ...args: any[]) => void,
): void => { ): void => {
if (rendererAvailChannels.includes(channel)) { if (rendererAvailChannels.includes(channel)) {
ipcRenderer.on(channel, listener) ipcRenderer.on(channel, listener);
} else { } else {
throw new Error(`Unknown ipc channel name: ${channel}`) throw new Error(`Unknown ipc channel name: ${channel}`);
} }
}, },
once: ( once: (
channel: string, channel: string,
listener: (event: IpcRendererEvent, ...args: any[]) => void listener: (event: IpcRendererEvent, ...args: any[]) => void,
): void => { ): void => {
if (rendererAvailChannels.includes(channel)) { if (rendererAvailChannels.includes(channel)) {
ipcRenderer.once(channel, listener) ipcRenderer.once(channel, listener);
} else { } else {
throw new Error(`Unknown ipc channel name: ${channel}`) throw new Error(`Unknown ipc channel name: ${channel}`);
} }
}, },
off: ( off: (
channel: string, channel: string,
listener: (event: IpcRendererEvent, ...args: any[]) => void listener: (event: IpcRendererEvent, ...args: any[]) => void,
): void => { ): void => {
if (rendererAvailChannels.includes(channel)) { if (rendererAvailChannels.includes(channel)) {
ipcRenderer.off(channel, listener) ipcRenderer.off(channel, listener);
} else { } else {
throw new Error(`Unknown ipc channel name: ${channel}`) throw new Error(`Unknown ipc channel name: ${channel}`);
} }
}, },
invoke: async (channel: string, ...data: any[]): Promise<any> => { invoke: async (channel: string, ...data: any[]): Promise<any> => {
if (mainAvailChannels.includes(channel)) { if (mainAvailChannels.includes(channel)) {
const result = await ipcRenderer.invoke.apply(null, [channel, ...data]) const result = await ipcRenderer.invoke.apply(null, [channel, ...data]);
if (process.env.NODE_ENV === 'development') { if (process.env.NODE_ENV === 'development') {
log.silly(`[IPC_INVOKE::${channel}]`, { log.silly(`[IPC_INVOKE::${channel}]`, {
request: data, request: data,
result result,
}) });
} }
return result return result;
} }
throw new Error(`Unknown ipc channel name: ${channel}`) throw new Error(`Unknown ipc channel name: ${channel}`);
} },
}) });
+32 -32
View File
@@ -1,73 +1,73 @@
<script setup lang="tsx"> <script setup lang="tsx">
import { DefaultLayout } from '@/renderer/components/layout' import { onMounted } from 'vue';
import GlobalDialog from '@/renderer/components/GlobalDialog.vue' import GlobalDialog from '@/renderer/components/GlobalDialog.vue';
import UpdateDialog from '@/renderer/components/UpdateDialog.vue' import GlobalSnackbar from '@/renderer/components/GlobalSnackbar.vue';
import GlobalSnackbar from '@/renderer/components/GlobalSnackbar.vue' import { DefaultLayout } from '@/renderer/components/layout';
import { onMounted } from 'vue' import UpdateDialog from '@/renderer/components/UpdateDialog.vue';
import { useAppStore } from '@/renderer/store/app' import { useAppStore } from '@/renderer/store/app';
// 使用应用商店来管理应用状态 // 使用应用商店来管理应用状态
const appStore = useAppStore() const appStore = useAppStore();
onMounted(() => { onMounted(() => {
// 在应用启动时检查更新 // 在应用启动时检查更新
setTimeout(() => { setTimeout(() => {
checkForUpdates() checkForUpdates();
}, 3000) // 延迟3秒后检查更新,避免影响应用启动速度 }, 3000); // 延迟3秒后检查更新,避免影响应用启动速度
// 监听主进程发来的更新相关事件 // 监听主进程发来的更新相关事件
window.mainApi.on('update-available', (_, info) => { window.mainApi.on('update-available', (_, info) => {
appStore.setUpdateStatus('available', info.version) appStore.setUpdateStatus('available', info.version);
}) });
window.mainApi.on('update-not-available', (_, info) => { window.mainApi.on('update-not-available', (_, info) => {
appStore.setUpdateStatus('not-available', info.version) appStore.setUpdateStatus('not-available', info.version);
}) });
window.mainApi.on('update-error', (_, error) => { window.mainApi.on('update-error', (_, error) => {
appStore.setUpdateStatus('error', undefined, error.message || String(error)) appStore.setUpdateStatus('error', undefined, error.message || String(error));
}) });
window.mainApi.on('update-download-progress', (_, progress) => { window.mainApi.on('update-download-progress', (_, progress) => {
// 这个事件通过pinia状态管理,不需要在这里单独处理 // 这个事件通过pinia状态管理,不需要在这里单独处理
console.log('下载进度:', progress) console.log('下载进度:', progress);
}) });
window.mainApi.on('update-downloaded', (_, info) => { window.mainApi.on('update-downloaded', (_, info) => {
appStore.setUpdateStatus('downloaded') appStore.setUpdateStatus('downloaded');
console.log('更新已下载,准备安装:', info) console.log('更新已下载,准备安装:', info);
}) });
}) });
const checkForUpdates = async () => { const checkForUpdates = async () => {
try { try {
// 显示检查更新状态 // 显示检查更新状态
appStore.setUpdateStatus('checking') appStore.setUpdateStatus('checking');
// 调用主进程检查更新 // 调用主进程检查更新
const result = await window.mainApi.invoke('msgCheckForUpdates') const result = await window.mainApi.invoke('msgCheckForUpdates');
if (result.error) { if (result.error) {
console.error('检查更新时出错:', result.error) console.error('检查更新时出错:', result.error);
appStore.setUpdateStatus('error', result.error) appStore.setUpdateStatus('error', result.error);
return return;
} }
if (result.updateAvailable) { if (result.updateAvailable) {
// 发现新版本,显示更新提示 // 发现新版本,显示更新提示
appStore.setUpdateStatus('available', result.currentVersion) appStore.setUpdateStatus('available', result.currentVersion);
} else { } else {
// 当前已是最新版本 // 当前已是最新版本
appStore.setUpdateStatus('not-available', result.currentVersion) appStore.setUpdateStatus('not-available', result.currentVersion);
} }
} catch (error) { } catch (error) {
console.error('检查更新时出错:', error) console.error('检查更新时出错:', error);
appStore.setUpdateStatus( appStore.setUpdateStatus(
'error', 'error',
error instanceof Error ? error.message : String(error) error instanceof Error ? error.message : String(error),
) );
} }
} };
</script> </script>
<template> <template>
+56 -56
View File
@@ -1,3 +1,56 @@
<script setup lang="ts">
import { ref, watch } from 'vue';
// 定义 props
interface Props {
modelValue: boolean;
title?: string;
message: string;
confirmText?: string;
cancelText?: string;
confirmColor?: string;
}
const props = withDefaults(defineProps<Props>(), {
title: '确认',
confirmText: '确认',
cancelText: '取消',
confirmColor: 'primary',
});
// 定义 emits
const emit = defineEmits<{
'update:modelValue': [value: boolean];
'confirm': [];
'cancel': [];
}>();
// 本地状态
const dialogVisible = ref(props.modelValue);
// 监听 modelValue 的变化
watch(() => props.modelValue, (newValue) => {
dialogVisible.value = newValue;
});
// 监听本地状态变化并同步到父组件
watch(dialogVisible, (newValue) => {
emit('update:modelValue', newValue);
});
// 处理确认事件
const handleConfirm = () => {
emit('confirm');
dialogVisible.value = false;
};
// 处理取消事件
const handleCancel = () => {
emit('cancel');
dialogVisible.value = false;
};
</script>
<template> <template>
<v-dialog <v-dialog
v-model="dialogVisible" v-model="dialogVisible"
@@ -12,17 +65,17 @@
{{ message }} {{ message }}
</v-card-text> </v-card-text>
<v-card-actions> <v-card-actions>
<v-spacer></v-spacer> <v-spacer />
<v-btn <v-btn
@click="handleCancel"
variant="text" variant="text"
@click="handleCancel"
> >
{{ cancelText }} {{ cancelText }}
</v-btn> </v-btn>
<v-btn <v-btn
@click="handleConfirm"
:color="confirmColor" :color="confirmColor"
variant="flat" variant="flat"
@click="handleConfirm"
> >
{{ confirmText }} {{ confirmText }}
</v-btn> </v-btn>
@@ -30,56 +83,3 @@
</v-card> </v-card>
</v-dialog> </v-dialog>
</template> </template>
<script setup lang="ts">
import { ref, watch } from 'vue'
// 定义 props
interface Props {
modelValue: boolean
title?: string
message: string
confirmText?: string
cancelText?: string
confirmColor?: string
}
const props = withDefaults(defineProps<Props>(), {
title: '确认',
confirmText: '确认',
cancelText: '取消',
confirmColor: 'primary'
})
// 定义 emits
const emit = defineEmits<{
'update:modelValue': [value: boolean]
confirm: []
cancel: []
}>()
// 本地状态
const dialogVisible = ref(props.modelValue)
// 监听 modelValue 的变化
watch(() => props.modelValue, (newValue) => {
dialogVisible.value = newValue
})
// 监听本地状态变化并同步到父组件
watch(dialogVisible, (newValue) => {
emit('update:modelValue', newValue)
})
// 处理确认事件
const handleConfirm = () => {
emit('confirm')
dialogVisible.value = false
}
// 处理取消事件
const handleCancel = () => {
emit('cancel')
dialogVisible.value = false
}
</script>
+88 -84
View File
@@ -1,3 +1,86 @@
<script setup lang="ts">
import type { TreeNode } from '@/types';
import { nextTick, ref, watch } from 'vue';
interface Props {
modelValue: boolean;
isEditMode?: boolean;
parentDirectory?: TreeNode | null;
editingItem?: TreeNode | null;
}
interface Emits {
(e: 'update:modelValue', value: boolean): void;
(e: 'confirm', data: { path: string }): void;
}
const props = withDefaults(defineProps<Props>(), {
isEditMode: false,
parentDirectory: null,
editingItem: null,
});
const emit = defineEmits<Emits>();
// Internal state
const isVisible = ref(false);
const formValid = ref(false);
const directoryPath = ref('');
const inputRef = ref<HTMLInputElement | null>(null);
// Computed
const isEditMode = computed(() => props.isEditMode);
// Watch for visibility changes
watch(() => props.modelValue, (newValue) => {
isVisible.value = newValue;
if (newValue) {
// Reset form when dialog opens
directoryPath.value = props.editingItem?.path || '';
autoFocusInput();
}
});
// Watch for internal visibility changes and emit update
watch(isVisible, (newValue) => {
emit('update:modelValue', newValue);
});
// Validation rules
const requiredRule = (fieldName: string) => (v: string | null | undefined) =>
!!v || `${fieldName}为必填项`;
// Methods
const autoFocusInput = async () => {
await nextTick();
if (inputRef.value) {
;(inputRef.value as HTMLInputElement).focus();
}
};
const confirm = () => {
if (!directoryPath.value.trim()) {
return;
}
// Construct the full path if there's a parent directory
emit('confirm', {
path: directoryPath.value,
});
close();
};
const reset = () => {
directoryPath.value = '';
formValid.value = false;
};
const close = () => {
isVisible.value = false;
};
</script>
<template> <template>
<v-dialog <v-dialog
v-model="isVisible" v-model="isVisible"
@@ -11,24 +94,26 @@
<v-card-text> <v-card-text>
<v-form v-model="formValid"> <v-form v-model="formValid">
<v-text-field <v-text-field
ref="inputRef"
v-model="directoryPath" v-model="directoryPath"
label="目录名称" label="目录名称"
placeholder="请输入目录名称" placeholder="请输入目录名称"
:rules="[requiredRule('目录名称')]" :rules="[requiredRule('目录名称')]"
required required
density="comfortable" density="comfortable"
ref="inputRef"
@keydown.enter="confirm" @keydown.enter="confirm"
/> />
</v-form> </v-form>
</v-card-text> </v-card-text>
<v-card-actions> <v-card-actions>
<v-spacer /> <v-spacer />
<v-btn @click="close">取消</v-btn> <v-btn @click="close">
取消
</v-btn>
<v-btn <v-btn
color="primary" color="primary"
@click="confirm"
:disabled="!formValid" :disabled="!formValid"
@click="confirm"
> >
{{ isEditMode ? '保存' : '添加' }} {{ isEditMode ? '保存' : '添加' }}
</v-btn> </v-btn>
@@ -36,84 +121,3 @@
</v-card> </v-card>
</v-dialog> </v-dialog>
</template> </template>
<script setup lang="ts">
import { ref, watch, nextTick } from 'vue'
import { TreeNode } from '@/types'
interface Props {
modelValue: boolean
isEditMode?: boolean
parentDirectory?: TreeNode | null
editingItem?: TreeNode | null
}
interface Emits {
(e: 'update:modelValue', value: boolean): void
(e: 'confirm', data: { path: string; }): void
}
const props = withDefaults(defineProps<Props>(), {
isEditMode: false,
parentDirectory: null,
editingItem: null
})
const emit = defineEmits<Emits>()
// Internal state
const isVisible = ref(false)
const formValid = ref(false)
const directoryPath = ref('')
const inputRef = ref<HTMLInputElement | null>(null)
// Computed
const isEditMode = computed(() => props.isEditMode)
// Watch for visibility changes
watch(() => props.modelValue, (newValue) => {
isVisible.value = newValue
if (newValue) {
// Reset form when dialog opens
directoryPath.value = props.editingItem?.path || ''
autoFocusInput()
}
})
// Watch for internal visibility changes and emit update
watch(isVisible, (newValue) => {
emit('update:modelValue', newValue)
})
// Validation rules
const requiredRule = (fieldName: string) => (v: string | null | undefined) =>
!!v || `${fieldName}为必填项`
// Methods
const autoFocusInput = async () => {
await nextTick()
if (inputRef.value) {
;(inputRef.value as HTMLInputElement).focus()
}
}
const confirm = () => {
if (!directoryPath.value.trim()) return
// Construct the full path if there's a parent directory
emit('confirm', {
path: directoryPath.value,
})
close()
}
const reset = () => {
directoryPath.value = ''
formValid.value = false
}
const close = () => {
isVisible.value = false
}
</script>
+129 -127
View File
@@ -1,3 +1,125 @@
<script setup lang="ts">
import type { TreeNode } from '@/types';
import { nextTick, ref, watch } from 'vue';
interface Props {
modelValue: boolean;
isEditMode?: boolean;
parentDirectory?: TreeNode | null;
editingItem?: TreeNode | null;
}
interface Emits {
(e: 'update:modelValue', value: boolean): void;
(e: 'confirm', data: { path: string; sourcePath?: string; sourceType?: 'file' | 'folder' }): void;
}
const props = withDefaults(defineProps<Props>(), {
isEditMode: false,
parentDirectory: null,
editingItem: null,
});
const emit = defineEmits<Emits>();
// Internal state
const isVisible = ref(false);
const formValid = ref(false);
const filePath = ref('');
const selectedFilePath = ref<string>('');
const selectedFolderPath = ref<string>('');
const inputRef = ref<HTMLInputElement | null>(null);
// Computed
const isEditMode = computed(() => props.isEditMode);
// Watch for internal visibility changes and emit update
watch(isVisible, (newValue) => {
emit('update:modelValue', newValue);
});
// Validation rules
const requiredRule = (fieldName: string) => (v: string | null | undefined) =>
!!v || `${fieldName}为必填项`;
// Methods
const autoFocusInput = async () => {
await nextTick();
if (inputRef.value) {
;(inputRef.value as HTMLInputElement).focus();
}
};
const selectSourceFile = async () => {
// 使用主进程API来选择文件
const result = await window.mainApi.invoke('msgOpenFile', 'any');
if (result && !result.canceled && result.filePaths.length > 0) {
selectedFilePath.value = result.filePaths[0];
selectedFolderPath.value = ''; // 清空文件夹选择
}
};
const selectSourceFolder = async () => {
// 使用主进程API来选择文件夹
const result = await window.mainApi.invoke('msgSelectDirectory');
if (result && !result.canceled && result.filePath) {
selectedFolderPath.value = result.filePath;
selectedFilePath.value = ''; // 清空文件选择
}
};
const confirm = () => {
if (!filePath.value.trim()) {
return;
}
// 确定选择的源类型和路径
let sourcePathValue: string | undefined;
let sourceType: 'file' | 'folder' | undefined;
if (selectedFilePath.value) {
sourcePathValue = selectedFilePath.value;
sourceType = 'file';
} else if (selectedFolderPath.value) {
sourcePathValue = selectedFolderPath.value;
sourceType = 'folder';
}
emit('confirm', {
path: filePath.value,
sourcePath: sourcePathValue,
sourceType,
});
close();
};
const reset = () => {
filePath.value = '';
selectedFilePath.value = '';
selectedFolderPath.value = '';
formValid.value = false;
};
const close = () => {
isVisible.value = false;
};
// Watch for visibility changes
watch(() => props.modelValue, (newValue) => {
isVisible.value = newValue;
if (newValue) {
// Reset form when dialog opens
filePath.value = props.editingItem?.path || '';
selectedFilePath.value = props.editingItem?.sourceType === 'file' ? props.editingItem?.sourcePath || '' : '';
selectedFolderPath.value = props.editingItem?.sourceType === 'folder' ? props.editingItem?.sourcePath || '' : '';
autoFocusInput();
}
});
</script>
<template> <template>
<v-dialog <v-dialog
v-model="isVisible" v-model="isVisible"
@@ -11,13 +133,13 @@
<v-card-text> <v-card-text>
<v-form v-model="formValid"> <v-form v-model="formValid">
<v-text-field <v-text-field
ref="inputRef"
v-model="filePath" v-model="filePath"
label="文件名" label="文件名"
placeholder="文件名需要带上后缀,如:style skech.doc" placeholder="文件名需要带上后缀,如:style skech.doc"
:rules="[requiredRule('文件名')]" :rules="[requiredRule('文件名')]"
required required
density="comfortable" density="comfortable"
ref="inputRef"
/> />
<!-- 文件选择输入框 --> <!-- 文件选择输入框 -->
@@ -26,7 +148,7 @@
label="选择文件" label="选择文件"
placeholder="点击输入框选择文件" placeholder="点击输入框选择文件"
readonly readonly
:append-inner-icon="'mdi-file'" append-inner-icon="mdi-file"
density="comfortable" density="comfortable"
class="mt-4" class="mt-4"
@click="selectSourceFile" @click="selectSourceFile"
@@ -38,7 +160,7 @@
label="选择文件夹" label="选择文件夹"
placeholder="点击输入框选择文件夹" placeholder="点击输入框选择文件夹"
readonly readonly
:append-inner-icon="'mdi-folder'" append-inner-icon="mdi-folder"
density="comfortable" density="comfortable"
class="mt-4" class="mt-4"
@click="selectSourceFolder" @click="selectSourceFolder"
@@ -64,11 +186,13 @@
</v-card-text> </v-card-text>
<v-card-actions> <v-card-actions>
<v-spacer /> <v-spacer />
<v-btn @click="close">取消</v-btn> <v-btn @click="close">
取消
</v-btn>
<v-btn <v-btn
color="primary" color="primary"
@click="confirm"
:disabled="!formValid || (!selectedFilePath && !selectedFolderPath && !isEditMode)" :disabled="!formValid || (!selectedFilePath && !selectedFolderPath && !isEditMode)"
@click="confirm"
> >
{{ isEditMode ? '保存' : '添加' }} {{ isEditMode ? '保存' : '添加' }}
</v-btn> </v-btn>
@@ -76,125 +200,3 @@
</v-card> </v-card>
</v-dialog> </v-dialog>
</template> </template>
<script setup lang="ts">
import { ref, watch, nextTick } from 'vue'
import { TreeNode } from '@/types'
interface Props {
modelValue: boolean
isEditMode?: boolean
parentDirectory?: TreeNode | null
editingItem?: TreeNode | null
}
interface Emits {
(e: 'update:modelValue', value: boolean): void
(e: 'confirm', data: { path: string; sourcePath?: string; sourceType?: 'file' | 'folder' }): void
}
const props = withDefaults(defineProps<Props>(), {
isEditMode: false,
parentDirectory: null,
editingItem: null
})
const emit = defineEmits<Emits>()
// Internal state
const isVisible = ref(false)
const formValid = ref(false)
const filePath = ref('')
const selectedFilePath = ref<string>('')
const selectedFolderPath = ref<string>('')
const inputRef = ref<HTMLInputElement | null>(null)
// Computed
const isEditMode = computed(() => props.isEditMode)
// Watch for visibility changes
watch(() => props.modelValue, (newValue) => {
isVisible.value = newValue
if (newValue) {
// Reset form when dialog opens
filePath.value = props.editingItem?.path || ''
selectedFilePath.value = props.editingItem?.sourceType === 'file' ? props.editingItem?.sourcePath || '' : ''
selectedFolderPath.value = props.editingItem?.sourceType === 'folder' ? props.editingItem?.sourcePath || '' : ''
autoFocusInput()
}
})
// Watch for internal visibility changes and emit update
watch(isVisible, (newValue) => {
emit('update:modelValue', newValue)
})
// Validation rules
const requiredRule = (fieldName: string) => (v: string | null | undefined) =>
!!v || `${fieldName}为必填项`
// Methods
const autoFocusInput = async () => {
await nextTick()
if (inputRef.value) {
;(inputRef.value as HTMLInputElement).focus()
}
}
const selectSourceFile = async () => {
// 使用主进程API来选择文件
const result = await window.mainApi.invoke('msgOpenFile', 'any')
if (result && !result.canceled && result.filePaths.length > 0) {
selectedFilePath.value = result.filePaths[0]
selectedFolderPath.value = '' // 清空文件夹选择
}
}
const selectSourceFolder = async () => {
// 使用主进程API来选择文件夹
const result = await window.mainApi.invoke('msgSelectDirectory')
if (result && !result.canceled && result.filePath) {
selectedFolderPath.value = result.filePath
selectedFilePath.value = '' // 清空文件选择
}
}
const confirm = () => {
if (!filePath.value.trim()) return
// 确定选择的源类型和路径
let sourcePathValue: string | undefined
let sourceType: 'file' | 'folder' | undefined
if (selectedFilePath.value) {
sourcePathValue = selectedFilePath.value
sourceType = 'file'
} else if (selectedFolderPath.value) {
sourcePathValue = selectedFolderPath.value
sourceType = 'folder'
}
emit('confirm', {
path: filePath.value,
sourcePath: sourcePathValue,
sourceType
})
close()
}
const reset = () => {
filePath.value = ''
selectedFilePath.value = ''
selectedFolderPath.value = ''
formValid.value = false
}
const close = () => {
isVisible.value = false
}
</script>
+113 -116
View File
@@ -1,3 +1,111 @@
<script setup lang="ts">
import { onMounted, onUnmounted, ref } from 'vue';
// 定义对话框选项接口
interface BaseDialogOptions {
title: string;
message: string;
confirmText?: string;
cancelText?: string;
confirmColor?: string;
}
interface ConfirmDialogOptions extends BaseDialogOptions {}
interface MessageDialogOptions extends BaseDialogOptions {
dialogColor?: string;
}
// 全局状态
const confirmDialogVisible = ref(false);
const messageDialogVisible = ref(false);
const currentConfirmOptions = ref<ConfirmDialogOptions>({} as ConfirmDialogOptions);
const currentMessageOptions = ref<MessageDialogOptions>({} as MessageDialogOptions);
// Promise 解析器
let confirmResolver: ((value: boolean) => void) | null = null;
let messageResolver: (() => void) | null = null;
// 显示确认对话框
const showConfirm = (options: ConfirmDialogOptions): Promise<boolean> => {
currentConfirmOptions.value = {
title: options.title || '确认',
message: options.message,
confirmText: options.confirmText || '确认',
cancelText: options.cancelText || '取消',
confirmColor: options.confirmColor || 'primary',
};
confirmDialogVisible.value = true;
return new Promise((resolve) => {
confirmResolver = resolve;
});
};
// 显示消息对话框
const showMessage = (options: MessageDialogOptions): Promise<void> => {
currentMessageOptions.value = {
title: options.title,
message: options.message,
confirmText: options.confirmText || '确定',
confirmColor: options.confirmColor || 'primary',
dialogColor: options.dialogColor || 'white',
};
messageDialogVisible.value = true;
return new Promise((resolve) => {
messageResolver = resolve;
});
};
// 处理确认对话框确认操作
const handleConfirmAction = () => {
confirmDialogVisible.value = false;
if (confirmResolver) {
confirmResolver(true);
confirmResolver = null;
}
};
// 处理确认对话框取消操作
const handleConfirmCancel = () => {
confirmDialogVisible.value = false;
if (confirmResolver) {
confirmResolver(false);
confirmResolver = null;
}
};
// 处理消息对话框操作
const handleMessageAction = () => {
messageDialogVisible.value = false;
if (messageResolver) {
messageResolver();
messageResolver = null;
}
};
// 暴露方法给全局
onMounted(() => {
window.ConfirmDialogManager = {
showConfirm,
showMessage,
};
});
onUnmounted(() => {
if (window.ConfirmDialogManager) {
delete window.ConfirmDialogManager;
}
});
// 导出方法
defineExpose({
showConfirm,
showMessage,
});
</script>
<template> <template>
<div> <div>
<!-- Confirm Dialog --> <!-- Confirm Dialog -->
@@ -14,17 +122,17 @@
{{ currentConfirmOptions.message }} {{ currentConfirmOptions.message }}
</v-card-text> </v-card-text>
<v-card-actions> <v-card-actions>
<v-spacer></v-spacer> <v-spacer />
<v-btn <v-btn
@click="handleConfirmCancel"
variant="text" variant="text"
@click="handleConfirmCancel"
> >
{{ currentConfirmOptions.cancelText }} {{ currentConfirmOptions.cancelText }}
</v-btn> </v-btn>
<v-btn <v-btn
@click="handleConfirmAction"
:color="currentConfirmOptions.confirmColor" :color="currentConfirmOptions.confirmColor"
variant="flat" variant="flat"
@click="handleConfirmAction"
> >
{{ currentConfirmOptions.confirmText }} {{ currentConfirmOptions.confirmText }}
</v-btn> </v-btn>
@@ -46,11 +154,11 @@
{{ currentMessageOptions.message }} {{ currentMessageOptions.message }}
</v-card-text> </v-card-text>
<v-card-actions> <v-card-actions>
<v-spacer></v-spacer> <v-spacer />
<v-btn <v-btn
@click="handleMessageAction"
:color="currentMessageOptions.confirmColor" :color="currentMessageOptions.confirmColor"
variant="flat" variant="flat"
@click="handleMessageAction"
> >
{{ currentMessageOptions.confirmText }} {{ currentMessageOptions.confirmText }}
</v-btn> </v-btn>
@@ -60,117 +168,6 @@
</div> </div>
</template> </template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
// 定义对话框选项接口
interface BaseDialogOptions {
title: string
message: string
confirmText?: string
cancelText?: string
confirmColor?: string
}
interface ConfirmDialogOptions extends BaseDialogOptions {}
interface MessageDialogOptions extends BaseDialogOptions {
dialogColor?: string
}
// 全局状态
const confirmDialogVisible = ref(false)
const messageDialogVisible = ref(false)
const currentConfirmOptions = ref<ConfirmDialogOptions>({} as ConfirmDialogOptions)
const currentMessageOptions = ref<MessageDialogOptions>({} as MessageDialogOptions)
// Promise 解析器
let confirmResolver: ((value: boolean) => void) | null = null
let messageResolver: (() => void) | null = null
// 显示确认对话框
const showConfirm = (options: ConfirmDialogOptions): Promise<boolean> => {
currentConfirmOptions.value = {
title: options.title || '确认',
message: options.message,
confirmText: options.confirmText || '确认',
cancelText: options.cancelText || '取消',
confirmColor: options.confirmColor || 'primary'
}
confirmDialogVisible.value = true
return new Promise((resolve) => {
confirmResolver = resolve
})
}
// 显示消息对话框
const showMessage = (options: MessageDialogOptions): Promise<void> => {
currentMessageOptions.value = {
title: options.title,
message: options.message,
confirmText: options.confirmText || '确定',
confirmColor: options.confirmColor || 'primary',
dialogColor: options.dialogColor || 'white'
}
messageDialogVisible.value = true
return new Promise((resolve) => {
messageResolver = resolve
})
}
// 处理确认对话框确认操作
const handleConfirmAction = () => {
confirmDialogVisible.value = false
if (confirmResolver) {
confirmResolver(true)
confirmResolver = null
}
}
// 处理确认对话框取消操作
const handleConfirmCancel = () => {
confirmDialogVisible.value = false
if (confirmResolver) {
confirmResolver(false)
confirmResolver = null
}
}
// 处理消息对话框操作
const handleMessageAction = () => {
messageDialogVisible.value = false
if (messageResolver) {
messageResolver()
messageResolver = null
}
}
// 暴露方法给全局
onMounted(() => {
window.ConfirmDialogManager = {
showConfirm,
showMessage
}
})
onUnmounted(() => {
if (window.ConfirmDialogManager) {
delete window.ConfirmDialogManager
}
})
// 对话框类型
type DialogType = 'confirm' | 'info' | 'success' | 'warning' | 'error'
// 导出方法
defineExpose({
showConfirm,
showMessage
})
</script>
<style scoped> <style scoped>
/* 样式在此可以添加额外的自定义样式 */ /* 样式在此可以添加额外的自定义样式 */
</style> </style>
+6 -6
View File
@@ -1,3 +1,9 @@
<script setup lang="ts">
import { useSnackbarStore } from '@/renderer/store/snackbar';
const snackbar = useSnackbarStore();
</script>
<template> <template>
<v-snackbar <v-snackbar
v-model="snackbar.isVisible" v-model="snackbar.isVisible"
@@ -26,9 +32,3 @@
</template> </template>
</v-snackbar> </v-snackbar>
</template> </template>
<script setup lang="ts">
import { useSnackbarStore } from '@/renderer/store/snackbar'
const snackbar = useSnackbarStore()
</script>
+61 -61
View File
@@ -1,3 +1,59 @@
<script setup lang="ts">
import { ref, watch } from 'vue';
// 定义 props
interface Props {
modelValue: boolean;
title: string;
message: string;
confirmText?: string;
cancelText?: string;
confirmColor?: string;
dialogColor?: string;
showActions?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
confirmText: '确认',
cancelText: '取消',
confirmColor: 'primary',
dialogColor: 'white',
showActions: true,
});
// 定义 emits
const emit = defineEmits<{
'update:modelValue': [value: boolean];
'confirm': [];
'cancel': [];
}>();
// 本地状态
const dialogVisible = ref(props.modelValue);
// 监听 modelValue 的变化
watch(() => props.modelValue, (newValue) => {
dialogVisible.value = newValue;
});
// 监听本地状态变化并同步到父组件
watch(dialogVisible, (newValue) => {
emit('update:modelValue', newValue);
});
// 处理确认事件
const handleConfirm = () => {
emit('confirm');
dialogVisible.value = false;
};
// 处理取消事件
const handleCancel = () => {
emit('cancel');
dialogVisible.value = false;
};
</script>
<template> <template>
<v-dialog <v-dialog
v-model="dialogVisible" v-model="dialogVisible"
@@ -12,28 +68,28 @@
{{ message }} {{ message }}
</v-card-text> </v-card-text>
<v-card-actions v-if="showActions"> <v-card-actions v-if="showActions">
<v-spacer></v-spacer> <v-spacer />
<v-btn <v-btn
v-if="cancelText" v-if="cancelText"
@click="handleCancel"
variant="text" variant="text"
@click="handleCancel"
> >
{{ cancelText }} {{ cancelText }}
</v-btn> </v-btn>
<v-btn <v-btn
@click="handleConfirm"
:color="confirmColor" :color="confirmColor"
variant="flat" variant="flat"
@click="handleConfirm"
> >
{{ confirmText }} {{ confirmText }}
</v-btn> </v-btn>
</v-card-actions> </v-card-actions>
<v-card-actions v-else> <v-card-actions v-else>
<v-spacer></v-spacer> <v-spacer />
<v-btn <v-btn
@click="handleConfirm"
:color="confirmColor" :color="confirmColor"
variant="flat" variant="flat"
@click="handleConfirm"
> >
{{ confirmText }} {{ confirmText }}
</v-btn> </v-btn>
@@ -41,59 +97,3 @@
</v-card> </v-card>
</v-dialog> </v-dialog>
</template> </template>
<script setup lang="ts">
import { ref, watch } from 'vue'
// 定义 props
interface Props {
modelValue: boolean
title: string
message: string
confirmText?: string
cancelText?: string
confirmColor?: string
dialogColor?: string
showActions?: boolean
}
const props = withDefaults(defineProps<Props>(), {
confirmText: '确认',
cancelText: '取消',
confirmColor: 'primary',
dialogColor: 'white',
showActions: true
})
// 定义 emits
const emit = defineEmits<{
'update:modelValue': [value: boolean]
confirm: []
cancel: []
}>()
// 本地状态
const dialogVisible = ref(props.modelValue)
// 监听 modelValue 的变化
watch(() => props.modelValue, (newValue) => {
dialogVisible.value = newValue
})
// 监听本地状态变化并同步到父组件
watch(dialogVisible, (newValue) => {
emit('update:modelValue', newValue)
})
// 处理确认事件
const handleConfirm = () => {
emit('confirm')
dialogVisible.value = false
}
// 处理取消事件
const handleCancel = () => {
emit('cancel')
dialogVisible.value = false
}
</script>
+58 -52
View File
@@ -1,3 +1,46 @@
<script setup lang="ts">
import type { Template, TreeNode } from '@/types';
import { computed } from 'vue';
interface Props {
template: Template;
}
interface Emits {
(e: 'view', template: Template): void;
(e: 'execute', template: Template): void;
(e: 'edit', template: Template): void;
(e: 'delete', template: Template): void;
(e: 'copy', template: Template): void;
}
const props = defineProps<Props>();
defineEmits<Emits>();
// Count files and directories in tree structure
const nodeCount = computed(() => {
let files = 0;
let directories = 0;
const count = (nodeList: TreeNode[]) => {
nodeList.forEach((node) => {
if (node.type === 'file') {
files++;
} else if (node.type === 'directory') {
directories++;
if (node.children) {
count(node.children);
}
}
});
};
count(props.template.treeStructure || []);
return { files, directories };
});
</script>
<template> <template>
<v-card class="d-flex flex-column" height="100%" variant="outlined"> <v-card class="d-flex flex-column" height="100%" variant="outlined">
<v-card-item> <v-card-item>
@@ -16,7 +59,7 @@
</v-chip-group> </v-chip-group>
</v-card-text> </v-card-text>
<v-divider class="mt-auto"></v-divider> <v-divider class="mt-auto" />
<v-card-actions class="pa-2 justify-space-between"> <v-card-actions class="pa-2 justify-space-between">
<div> <div>
@@ -29,7 +72,7 @@
</div> </div>
<v-menu location="bottom end"> <v-menu location="bottom end">
<template v-slot:activator="{ props }"> <template #activator="{ props }">
<v-btn v-bind="props" icon variant="text" size="small" density="comfortable"> <v-btn v-bind="props" icon variant="text" size="small" density="comfortable">
<v-icon>mdi-dots-vertical</v-icon> <v-icon>mdi-dots-vertical</v-icon>
</v-btn> </v-btn>
@@ -37,72 +80,35 @@
<v-list density="compact"> <v-list density="compact">
<v-list-item @click="$emit('edit', template)"> <v-list-item @click="$emit('edit', template)">
<template v-slot:prepend> <template #prepend>
<v-icon>mdi-pencil</v-icon> <v-icon>mdi-pencil</v-icon>
</template> </template>
<v-list-item-title>修改</v-list-item-title> <v-list-item-title>修改</v-list-item-title>
</v-list-item> </v-list-item>
<v-list-item @click="$emit('copy', template)"> <v-list-item @click="$emit('copy', template)">
<template v-slot:prepend> <template #prepend>
<v-icon color="primary">mdi-content-copy</v-icon> <v-icon color="primary">
mdi-content-copy
</v-icon>
</template> </template>
<v-list-item-title>复制</v-list-item-title> <v-list-item-title>复制</v-list-item-title>
</v-list-item> </v-list-item>
<v-divider></v-divider> <v-divider />
<v-list-item @click="$emit('delete', template)"> <v-list-item @click="$emit('delete', template)">
<template v-slot:prepend> <template #prepend>
<v-icon color="error">mdi-delete</v-icon> <v-icon color="error">
mdi-delete
</v-icon>
</template> </template>
<v-list-item-title class="text-error">删除</v-list-item-title> <v-list-item-title class="text-error">
删除
</v-list-item-title>
</v-list-item> </v-list-item>
</v-list> </v-list>
</v-menu> </v-menu>
</v-card-actions> </v-card-actions>
</v-card> </v-card>
</template> </template>
<script setup lang="ts">
import { computed } from 'vue'
import { Template, TreeNode } from '@/types'
interface Props {
template: Template
}
interface Emits {
(e: 'view', template: Template): void
(e: 'execute', template: Template): void
(e: 'edit', template: Template): void
(e: 'delete', template: Template): void
(e: 'copy', template: Template): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
// Count files and directories in tree structure
const nodeCount = computed(() => {
let files = 0
let directories = 0
const count = (nodeList: TreeNode[]) => {
nodeList.forEach(node => {
if (node.type === 'file') {
files++
} else if (node.type === 'directory') {
directories++
if (node.children) {
count(node.children)
}
}
})
}
count(props.template.treeStructure || [])
return { files, directories }
})
</script>
+41 -43
View File
@@ -1,3 +1,34 @@
<script setup lang="ts">
interface TreeItem {
id: string;
name: string;
type: 'directory' | 'file';
children?: TreeItem[];
open?: boolean;
}
interface Props {
items: TreeItem[];
height?: number;
readOnly?: boolean;
emptyText?: string;
}
withDefaults(defineProps<Props>(), {
height: 600,
readOnly: false,
emptyText: '暂无文件或目录',
});
const emit = defineEmits([
'addNewItemToCurrentDirectory',
'editFile',
'editItem',
'deleteItem',
'addFileToDirectory',
]);
</script>
<template> <template>
<v-card <v-card
variant="outlined" variant="outlined"
@@ -12,7 +43,7 @@
item-value="id" item-value="id"
open-on-click open-on-click
:hoverable="true" :hoverable="true"
:indent-lines="'default'" indent-lines="default"
separate-roots separate-roots
density="compact" density="compact"
> >
@@ -50,7 +81,9 @@
class="ml-2" class="ml-2"
@click.stop="emit('addNewItemToCurrentDirectory', item)" @click.stop="emit('addNewItemToCurrentDirectory', item)"
> >
<v-icon size="small">mdi-folder-plus</v-icon> <v-icon size="small">
mdi-folder-plus
</v-icon>
<v-tooltip <v-tooltip
activator="parent" activator="parent"
location="top" location="top"
@@ -66,7 +99,9 @@
size="small" size="small"
@click.stop="emit('addFileToDirectory', item)" @click.stop="emit('addFileToDirectory', item)"
> >
<v-icon size="small"> mdi-file-plus </v-icon> <v-icon size="small">
mdi-file-plus
</v-icon>
<v-tooltip <v-tooltip
activator="parent" activator="parent"
location="top" location="top"
@@ -81,7 +116,9 @@
size="small" size="small"
@click.stop="emit('editItem', item)" @click.stop="emit('editItem', item)"
> >
<v-icon size="small"> mdi-pencil </v-icon> <v-icon size="small">
mdi-pencil
</v-icon>
<v-tooltip <v-tooltip
activator="parent" activator="parent"
location="top" location="top"
@@ -134,49 +171,10 @@
</p> </p>
</div> </div>
</v-card> </v-card>
</template> </template>
<script setup lang="ts">
interface TreeItem {
id: string
name: string
type: 'directory' | 'file'
children?: TreeItem[]
open?: boolean
}
interface Props {
items: TreeItem[]
height?: number
readOnly?: boolean
emptyText?: string
}
const props = withDefaults(defineProps<Props>(), {
height: 600,
readOnly: false,
emptyText: '暂无文件或目录'
})
const localExpanded = defineModel<string[]>('expanded', {
default: () => []
})
const emit = defineEmits([
'addNewItemToCurrentDirectory',
'editFile',
'editItem',
'deleteItem',
'addFileToDirectory'
])
</script>
<style scoped> <style scoped>
.cursor-pointer { .cursor-pointer {
cursor: pointer; cursor: pointer;
} }
</style> </style>
+97 -93
View File
@@ -1,3 +1,88 @@
<script setup lang="ts">
import { computed, onMounted, onUnmounted } from 'vue';
import { useI18n } from 'vue-i18n';
import { useAppStore } from '@/renderer/store/app';
const { t } = useI18n();
const appStore = useAppStore();
// 从store获取状态
const updateStatus = computed(() => appStore.updateStatus);
const downloadProgress = computed(() => appStore.downloadProgress);
// 对话框显示状态
const dialogVisible = computed({
get: () => {
return appStore.updateDialogVisible
|| updateStatus.value.status === 'available'
|| updateStatus.value.status === 'downloading'
|| updateStatus.value.status === 'downloaded'
|| (updateStatus.value.status === 'error' && !!updateStatus.value.error);
},
set: (value: boolean) => {
if (value) {
appStore.showUpdateDialog();
} else {
appStore.hideUpdateDialog();
}
},
});
const latestVersion = computed(() => {
// 这里应该从API获取最新版本号,暂时使用固定值
return `v${(updateStatus.value.currentVersion || '1.0.0').replace(/^v?/, '').split('.').map((n, i) =>
i === 2 ? (Number.parseInt(n) + 1).toString() : n,
).join('.')}`;
});
const isDownloading = computed(() => updateStatus.value.status === 'downloading');
const isDownloadComplete = computed(() => downloadProgress.value >= 100);
// 方法
const downloadUpdate = async () => {
await appStore.startUpdateDownload();
};
const skipUpdate = () => {
appStore.setUpdateStatus('not-available', updateStatus.value.currentVersion);
dialogVisible.value = false;
};
const cancelDownload = () => {
// 取消下载逻辑
appStore.setUpdateStatus('not-available', updateStatus.value.currentVersion);
dialogVisible.value = false;
};
const laterInstall = () => {
dialogVisible.value = false;
};
const installUpdate = () => {
// 安装更新,这需要触发quitAndInstall
window.mainApi.invoke('msgInstallUpdate');
dialogVisible.value = false;
};
const closeDialog = () => {
appStore.setUpdateStatus('idle');
dialogVisible.value = false;
};
// 监听更新下载进度
onMounted(() => {
// 监听主进程发来的更新相关事件
window.mainApi.on('update-download-progress', (_, progress) => {
appStore.setDownloadProgress(progress.percent);
});
});
onUnmounted(() => {
// 移除事件监听器
window.mainApi.off('update-download-progress', () => {});
});
</script>
<template> <template>
<v-dialog <v-dialog
v-model="dialogVisible" v-model="dialogVisible"
@@ -12,10 +97,12 @@
<v-card-text> <v-card-text>
<div>{{ t('update.currentVersion') }}: {{ updateStatus.currentVersion }}</div> <div>{{ t('update.currentVersion') }}: {{ updateStatus.currentVersion }}</div>
<div>{{ t('update.latestVersion') }}: {{ latestVersion }}</div> <div>{{ t('update.latestVersion') }}: {{ latestVersion }}</div>
<div class="mt-3">{{ t('update.updateDescription') }}</div> <div class="mt-3">
{{ t('update.updateDescription') }}
</div>
</v-card-text> </v-card-text>
<v-card-actions> <v-card-actions>
<v-spacer></v-spacer> <v-spacer />
<v-btn <v-btn
color="primary" color="primary"
variant="text" variant="text"
@@ -26,8 +113,8 @@
<v-btn <v-btn
color="primary" color="primary"
variant="flat" variant="flat"
@click="downloadUpdate"
:loading="isDownloading" :loading="isDownloading"
@click="downloadUpdate"
> >
{{ t('update.downloadUpdate') }} {{ t('update.downloadUpdate') }}
</v-btn> </v-btn>
@@ -47,15 +134,17 @@
> >
<strong>{{ Math.round(downloadProgress) }}%</strong> <strong>{{ Math.round(downloadProgress) }}%</strong>
</v-progress-linear> </v-progress-linear>
<div class="mt-3">{{ t('update.downloadProgressText') }}</div> <div class="mt-3">
{{ t('update.downloadProgressText') }}
</div>
</v-card-text> </v-card-text>
<v-card-actions> <v-card-actions>
<v-spacer></v-spacer> <v-spacer />
<v-btn <v-btn
color="primary" color="primary"
variant="text" variant="text"
@click="cancelDownload"
:disabled="isDownloadComplete" :disabled="isDownloadComplete"
@click="cancelDownload"
> >
{{ t('common.cancel') }} {{ t('common.cancel') }}
</v-btn> </v-btn>
@@ -70,7 +159,7 @@
{{ t('update.updateReadyToInstall') }} {{ t('update.updateReadyToInstall') }}
</v-card-text> </v-card-text>
<v-card-actions> <v-card-actions>
<v-spacer></v-spacer> <v-spacer />
<v-btn <v-btn
color="primary" color="primary"
variant="text" variant="text"
@@ -96,7 +185,7 @@
{{ updateStatus.error }} {{ updateStatus.error }}
</v-card-text> </v-card-text>
<v-card-actions> <v-card-actions>
<v-spacer></v-spacer> <v-spacer />
<v-btn <v-btn
color="primary" color="primary"
variant="flat" variant="flat"
@@ -108,88 +197,3 @@
</v-card> </v-card>
</v-dialog> </v-dialog>
</template> </template>
<script setup lang="ts">
import { computed, onMounted, onUnmounted } from 'vue'
import { useAppStore, UpdateStatus } from '@/renderer/store/app'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const appStore = useAppStore()
// 从store获取状态
const updateStatus = computed(() => appStore.updateStatus)
const downloadProgress = computed(() => appStore.downloadProgress)
// 对话框显示状态
const dialogVisible = computed({
get: () => {
return appStore.updateDialogVisible ||
updateStatus.value.status === 'available' ||
updateStatus.value.status === 'downloading' ||
updateStatus.value.status === 'downloaded' ||
(updateStatus.value.status === 'error' && !!updateStatus.value.error)
},
set: (value: boolean) => {
if (value) {
appStore.showUpdateDialog()
} else {
appStore.hideUpdateDialog()
}
}
})
const latestVersion = computed(() => {
// 这里应该从API获取最新版本号,暂时使用固定值
return 'v' + (updateStatus.value.currentVersion || '1.0.0').replace(/^v?/, '').split('.').map((n, i) =>
i === 2 ? (parseInt(n) + 1).toString() : n
).join('.')
})
const isDownloading = computed(() => updateStatus.value.status === 'downloading')
const isDownloadComplete = computed(() => downloadProgress.value >= 100)
// 方法
const downloadUpdate = async () => {
await appStore.startUpdateDownload()
}
const skipUpdate = () => {
appStore.setUpdateStatus('not-available', updateStatus.value.currentVersion)
dialogVisible.value = false
}
const cancelDownload = () => {
// 取消下载逻辑
appStore.setUpdateStatus('not-available', updateStatus.value.currentVersion)
dialogVisible.value = false
}
const laterInstall = () => {
dialogVisible.value = false
}
const installUpdate = () => {
// 安装更新,这需要触发quitAndInstall
window.mainApi.invoke('msgInstallUpdate')
dialogVisible.value = false
}
const closeDialog = () => {
appStore.setUpdateStatus('idle')
dialogVisible.value = false
}
// 监听更新下载进度
onMounted(() => {
// 监听主进程发来的更新相关事件
window.mainApi.on('update-download-progress', (_, progress) => {
appStore.setDownloadProgress(progress.percent)
})
})
onUnmounted(() => {
// 移除事件监听器
window.mainApi.off('update-download-progress', () => {})
})
</script>
@@ -1,5 +1,5 @@
<script setup lang="tsx"> <script setup lang="tsx">
import HeaderLayout from '@/renderer/components/layout/HeaderLayout.vue' import HeaderLayout from '@/renderer/components/layout/HeaderLayout.vue';
</script> </script>
<template> <template>
+11 -36
View File
@@ -1,51 +1,25 @@
<script setup lang="tsx"> <script setup lang="tsx">
import { useRoute, useRouter } from 'vue-router' import { useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n'
import { mdiFitToScreenOutline, mdiHome } from '@mdi/js'
const router = useRouter() const router = useRouter();
const route: any = useRoute()
const titleKey: string = (route?.meta?.titleKey || 'title.main') as string
const { t } = useI18n()
const handleRoute = (path: string): void => { const handleRoute = (path: string): void => {
router.push(path) router.push(path);
} };
const isCurrentRoute = (path: string): boolean => {
return path === route.path
}
const headerMenus: {
icon: string
text: string
path: string
}[] = [
{
icon: mdiHome,
text: 'title.main',
path: '/'
},
{
icon: mdiFitToScreenOutline,
text: 'title.second',
path: '/second'
}
]
// Get app version // Get app version
const appVersion = ref('') const appVersion = ref('');
onMounted(async () => { onMounted(async () => {
try { try {
appVersion.value = await window.mainApi.invoke('msgRequestGetVersion') appVersion.value = await window.mainApi.invoke('msgRequestGetVersion');
} catch (error) { } catch (error) {
console.error('Failed to get app version:', error) console.error('Failed to get app version:', error);
appVersion.value = 'unknown' appVersion.value = 'unknown';
} }
}) });
</script> </script>
<template> <template>
<v-app-bar color="primary" density="compact"> <v-app-bar color="primary" density="compact">
<v-app-bar-title @click="handleRoute('/')"> <v-app-bar-title @click="handleRoute('/')">
@@ -63,6 +37,7 @@ onMounted(async () => {
</template> </template>
</v-app-bar> </v-app-bar>
</template> </template>
<style scoped> <style scoped>
.v-btn { .v-btn {
opacity: 0.4; opacity: 0.4;
+3 -3
View File
@@ -1,4 +1,4 @@
import DefaultLayout from '@/renderer/components/layout/DefaultLayout.vue' import DefaultLayout from '@/renderer/components/layout/DefaultLayout.vue';
import HeaderLayout from '@/renderer/components/layout/HeaderLayout.vue' import HeaderLayout from '@/renderer/components/layout/HeaderLayout.vue';
export { DefaultLayout, HeaderLayout } export { DefaultLayout, HeaderLayout };
+1 -4
View File
@@ -6,8 +6,5 @@
<body> <body>
<div id="app"></div> <div id="app"></div>
</body> </body>
<script <script type="module" src="./main"></script>
type="module"
src="./main"
></script>
</html> </html>
+9 -9
View File
@@ -1,13 +1,13 @@
import { createApp } from 'vue' import { createApp } from 'vue';
import App from '@/renderer/App.vue' import App from '@/renderer/App.vue';
import router from '@/renderer/router' import i18n from '@/renderer/plugins/i18n';
import vuetify from '@/renderer/plugins/vuetify' import pinia from '@/renderer/plugins/pinia';
import i18n from '@/renderer/plugins/i18n' import vuetify from '@/renderer/plugins/vuetify';
import pinia from '@/renderer/plugins/pinia' import router from '@/renderer/router';
const app = createApp(App) const app = createApp(App);
app.use(vuetify).use(i18n).use(router).use(pinia) app.use(vuetify).use(i18n).use(router).use(pinia);
app.mount('#app') app.mount('#app');
+16 -16
View File
@@ -1,16 +1,16 @@
import { createI18n } from 'vue-i18n' import { createI18n } from 'vue-i18n';
import en from '@/renderer/locales/en.json' import de from '@/renderer/locales/de.json';
import ko from '@/renderer/locales/ko.json' import en from '@/renderer/locales/en.json';
import zhHans from '@/renderer/locales/zh-hans.json' import es from '@/renderer/locales/es.json';
import zhHant from '@/renderer/locales/zh-hant.json' import fr from '@/renderer/locales/fr.json';
import de from '@/renderer/locales/de.json' import ja from '@/renderer/locales/ja.json';
import es from '@/renderer/locales/es.json' import ko from '@/renderer/locales/ko.json';
import ja from '@/renderer/locales/ja.json' import nl from '@/renderer/locales/nl.json';
import fr from '@/renderer/locales/fr.json' import pt from '@/renderer/locales/pt.json';
import ru from '@/renderer/locales/ru.json' import ru from '@/renderer/locales/ru.json';
import pt from '@/renderer/locales/pt.json' import zhHans from '@/renderer/locales/zh-hans.json';
import nl from '@/renderer/locales/nl.json' import zhHant from '@/renderer/locales/zh-hant.json';
import { getCurrentLocale } from '@/renderer/utils' import { getCurrentLocale } from '@/renderer/utils';
export default createI18n({ export default createI18n({
locale: getCurrentLocale(), locale: getCurrentLocale(),
@@ -27,6 +27,6 @@ export default createI18n({
fr, fr,
ru, ru,
pt, pt,
nl nl,
} },
}) });
+3 -3
View File
@@ -1,5 +1,5 @@
import { createPinia } from 'pinia' import { createPinia } from 'pinia';
const pinia = createPinia() const pinia = createPinia();
export default pinia export default pinia;
+26 -26
View File
@@ -1,38 +1,38 @@
import { createVuetify } from 'vuetify' import { createVuetify } from 'vuetify';
import { import {
ko, de,
en, en,
es,
fr,
ja,
ko,
nl,
pt,
ru,
zhHans, zhHans,
zhHant, zhHant,
de, } from 'vuetify/locale';
es, import colors from 'vuetify/util/colors';
ja,
fr,
ru,
pt,
nl
} from 'vuetify/locale'
// import { aliases, mdi } from 'vuetify/iconsets/mdi-svg' // import { aliases, mdi } from 'vuetify/iconsets/mdi-svg'
import 'vuetify/styles' import 'vuetify/styles';
import '@mdi/font/css/materialdesignicons.css'
import colors from 'vuetify/util/colors' import '@mdi/font/css/materialdesignicons.css';
export default createVuetify({ export default createVuetify({
locale: { locale: {
messages: { ko, en, zhHans, zhHant, de, es, ja, fr, ru, pt, nl }, messages: { ko, en, zhHans, zhHant, de, es, ja, fr, ru, pt, nl },
locale: 'zhHans', locale: 'zhHans',
fallback: 'zhHans' fallback: 'zhHans',
}, },
defaults: { defaults: {
VBtn: { VBtn: {
style: [ style: [
{ {
// Do not force capitalization of a button text // Do not force capitalization of a button text
textTransform: 'none' textTransform: 'none',
} },
] ],
} },
}, },
// icons: { // icons: {
// defaultSet: 'mdi', // defaultSet: 'mdi',
@@ -46,15 +46,15 @@ export default createVuetify({
light: { light: {
dark: false, dark: false,
colors: { colors: {
primary: colors.green.darken2 primary: colors.green.darken2,
} },
}, },
dark: { dark: {
dark: true, dark: true,
colors: { colors: {
primary: colors.green.darken4 primary: colors.green.darken4,
} },
} },
} },
} },
}) });
+16 -17
View File
@@ -1,52 +1,51 @@
import { MainScreen } from '@/renderer/screens' import { createRouter, createWebHashHistory } from 'vue-router';
import { createRouter, createWebHashHistory } from 'vue-router'
export default createRouter({ export default createRouter({
history: createWebHashHistory(), history: createWebHashHistory(),
routes: [ routes: [
{ {
path: '/', path: '/',
redirect: '/templates' redirect: '/templates',
}, },
{ {
path: '/templates', path: '/templates',
component: () => import('@/renderer/screens/TemplateListScreen.vue'), component: () => import('@/renderer/screens/TemplateListScreen.vue'),
meta: { meta: {
titleKey: 'title.templates' titleKey: 'title.templates',
} },
}, },
{ {
path: '/create-template', path: '/create-template',
component: () => import('@/renderer/screens/CreateTemplateScreen.vue'), component: () => import('@/renderer/screens/CreateTemplateScreen.vue'),
meta: { meta: {
titleKey: 'title.create-template' titleKey: 'title.create-template',
} },
}, },
{ {
path: '/create-template/:id', path: '/create-template/:id',
component: () => import('@/renderer/screens/CreateTemplateScreen.vue'), component: () => import('@/renderer/screens/CreateTemplateScreen.vue'),
meta: { meta: {
titleKey: 'title.edit-template' titleKey: 'title.edit-template',
}, },
props: true props: true,
}, },
{ {
path: '/second', path: '/second',
component: () => import('@/renderer/screens/SecondScreen.vue'), component: () => import('@/renderer/screens/SecondScreen.vue'),
meta: { meta: {
titleKey: 'title.second' titleKey: 'title.second',
} },
}, },
{ {
path: '/error', path: '/error',
component: () => import('@/renderer/screens/ErrorScreen.vue'), component: () => import('@/renderer/screens/ErrorScreen.vue'),
meta: { meta: {
titleKey: 'title.error' titleKey: 'title.error',
} },
}, },
{ {
path: '/:pathMatch(.*)*', path: '/:pathMatch(.*)*',
redirect: '/' redirect: '/',
} },
] ],
}) });
+372 -385
View File
@@ -1,3 +1,367 @@
<script setup lang="ts">
import type { TreeNode } from '@/types';
import DirectoryDialog from '@/renderer/components/DirectoryDialog.vue';
import FileDialog from '@/renderer/components/FileDialog.vue';
import TemplateTree from '@/renderer/components/TemplateTree.vue';
import { useSnackbarStore } from '@/renderer/store/snackbar';
import { useTemplateStore } from '@/renderer/store/template';
import { generateUniqueId } from '@/renderer/utils';
import { ConfirmDialogService } from '@/renderer/utils/confirm-dialog-service';
const router = useRouter();
const route = useRoute();
const templateStore = useTemplateStore();
const snackbar = useSnackbarStore();
// Extract methods from store
const { getTemplateById } = templateStore;
// Check if we're copying a template
const isCopyingTemplate = computed(() => !route.params.id && !!route.query.copyId);
// Form state
const formValid = ref(false);
const templateName = ref('');
const templateDescription = ref('');
// Dialogs
const showFileDialog = ref(false);
// Directory dialog state
const showDirectoryDialog = ref(false);
const parentDirectoryForNewItems = ref<TreeNode | null>(null);
const currentEditingItem = ref<TreeNode | null>(null);
// Tree state
const expandedNodes = ref<string[]>([]);
const treeHeight = ref(600);
// Item state - 使用树状结构
const treeStructure = ref<TreeNode[]>([]);
// Computed
const isEditMode = computed(() => !!route.params.id);
// Validation rules
const requiredRule = (fieldName: string) => (v: string | null | undefined) =>
!!v || `${fieldName}为必填项`;
// Lifecycle
onMounted(async () => {
await templateStore.loadTemplates();
// Set tree height based on window height
updateTreeHeight();
// Add resize listener
window.addEventListener('resize', updateTreeHeight);
// Handle edit mode
if (isEditMode.value) {
const templateId = route.params.id as string;
const template = getTemplateById(templateId);
if (template) {
templateName.value = template.name;
templateDescription.value = template.description;
treeStructure.value = [...template.treeStructure];
} else {
router.push('/templates');
}
} else if (isCopyingTemplate.value) {
// Handle copy mode
const copyId = route.query.copyId as string;
if (copyId) {
const template = getTemplateById(copyId);
if (template) {
templateName.value = `${template.name} - 副本`;
templateDescription.value = template.description;
treeStructure.value = [...template.treeStructure];
} else {
snackbar.error('找不到要复制的模板');
router.push('/templates');
}
}
}
});
onUnmounted(() => {
window.removeEventListener('resize', updateTreeHeight);
});
// Methods
const updateTreeHeight = () => {
treeHeight.value = window.innerHeight - 200;
};
const addRootDirectory = () => {
parentDirectoryForNewItems.value = null;
currentEditingItem.value = null;
showDirectoryDialog.value = true;
};
const addRootFile = () => {
parentDirectoryForNewItems.value = null;
currentEditingItem.value = null;
showFileDialog.value = true;
};
// Add a file directly to a specific directory
const addFileToDirectory = (directoryItem: TreeNode) => {
parentDirectoryForNewItems.value = directoryItem; // Store the directory node as parent
currentEditingItem.value = null;
showFileDialog.value = true;
};
const addNewItemToCurrentDirectory = (directoryItem: TreeNode) => {
parentDirectoryForNewItems.value = directoryItem; // Store the directory node as parent
currentEditingItem.value = null;
showDirectoryDialog.value = true;
};
const handleDirectoryDialogConfirm = (data: { path: string }) => {
const { path } = data;
const newId = generateUniqueId('dir');
// Check if we're editing an existing directory
if (
currentEditingItem.value
&& currentEditingItem.value.type === 'directory'
) {
// Find and update the existing directory in the tree
const updatedNode: TreeNode = {
...currentEditingItem.value,
name: path,
path,
type: 'directory',
};
treeStructure.value = updateNodeInTree(
treeStructure.value,
currentEditingItem.value.id,
updatedNode,
);
} else {
// Create new directory node
const newNode: TreeNode = {
id: newId,
name: path,
path,
type: 'directory',
};
// Add to tree structure
if (parentDirectoryForNewItems.value) {
// In the parent directory, check if a node with the same name already exists
const parentNode = parentDirectoryForNewItems.value;
if (parentNode.children) {
const duplicateExists = parentNode.children.some(
child => child.name === path,
);
if (duplicateExists) {
snackbar.error('当前目录下已存在同名节点!');
return;
}
}
// Add as child to the parent directory
parentNode.children = parentNode.children || [];
parentNode.children.push(newNode);
} else {
// Check at root level for duplicates
const duplicateExists = treeStructure.value.some(
node => node.name === path,
);
if (duplicateExists) {
snackbar.error('根目录下已存在同名节点!');
return;
}
// Add as root node
treeStructure.value.push(newNode);
}
}
showDirectoryDialog.value = false;
parentDirectoryForNewItems.value = null; // Clear parent context after creation
currentEditingItem.value = null;
};
// 在树状结构中更新节点的辅助函数
const updateNodeInTree = (
nodes: TreeNode[],
oldId: string,
updatedNode: TreeNode,
): TreeNode[] => {
return nodes.map((node) => {
if (node.id === oldId) {
return updatedNode;
}
if (node.children) {
return {
...node,
children: updateNodeInTree(node.children, oldId, updatedNode),
};
}
return node;
});
};
const handleFileDialogConfirm = (data: { path: string; sourcePath?: string; sourceType?: 'file' | 'folder' }) => {
const { path, sourcePath, sourceType } = data;
const newId = generateUniqueId('file');
// Check if we're editing an existing file
if (currentEditingItem.value && currentEditingItem.value.type === 'file') {
// Find and update the existing file in the tree
const updatedNode: TreeNode = {
id: currentEditingItem.value.id,
name: path,
path,
type: 'file',
sourcePath,
sourceType,
};
treeStructure.value = updateNodeInTree(
treeStructure.value,
currentEditingItem.value.id,
updatedNode,
);
} else {
// Check for duplicate names in the same parent directory
if (parentDirectoryForNewItems.value) {
// In the parent directory, check if a node with the same name already exists
const parentNode = parentDirectoryForNewItems.value;
if (parentNode.children) {
const duplicateExists = parentNode.children.some(
child => child.name === path,
);
if (duplicateExists) {
snackbar.error('当前目录下已存在同名节点!');
return;
}
}
} else {
// Check at root level for duplicates
const duplicateExists = treeStructure.value.some(
node => node.name === path,
);
if (duplicateExists) {
snackbar.error('根目录下已存在同名节点!');
return;
}
}
const newFileNode: TreeNode = {
id: newId,
name: path,
path,
type: 'file',
sourcePath,
sourceType,
};
// Add to tree structure
if (parentDirectoryForNewItems.value) {
// Add as child to the parent directory
const parentNode = parentDirectoryForNewItems.value;
parentNode.children = parentNode.children || [];
parentNode.children.push(newFileNode);
} else {
// Add as root node
treeStructure.value.push(newFileNode);
}
}
showFileDialog.value = false;
parentDirectoryForNewItems.value = null; // Clear parent context after creation
currentEditingItem.value = null;
};
const goBack = async () => {
router.push('/templates');
};
// Unified method to edit items (both files and directories)
const editItem = (item: TreeNode) => {
if (item.type === 'directory') {
showEditDirectoryDialog(item);
} else {
showEditFileDialog(item);
}
};
// Show dialog to edit directory
const showEditDirectoryDialog = (directory: TreeNode) => {
// Set up for editing an existing directory
currentEditingItem.value = directory;
parentDirectoryForNewItems.value = null;
showDirectoryDialog.value = true;
};
// Show dialog to edit file
const showEditFileDialog = (file: TreeNode) => {
// Set up for editing an existing file
currentEditingItem.value = file;
parentDirectoryForNewItems.value = null;
showFileDialog.value = true;
};
const deleteItem = async (item: TreeNode) => {
const confirmed = await ConfirmDialogService.confirm({
title: '删除确认',
message: '确定要删除此项吗?',
confirmText: '删除',
cancelText: '取消',
confirmColor: 'error',
});
if (!confirmed) {
return;
}
// 从树状结构中删除项目
treeStructure.value = removeItemFromTree(treeStructure.value, item.id);
};
// 从树状结构中删除项目的辅助函数
const removeItemFromTree = (nodes: TreeNode[], itemId: string): TreeNode[] => {
return nodes
.filter(node => node.id !== itemId) // 过滤掉要删除的节点
.map((node) => {
if (node.children) {
return {
...node,
children: removeItemFromTree(node.children, itemId),
};
}
return node;
});
};
const saveTemplate = () => {
if (!formValid.value) {
return;
}
const templateData = {
name: templateName.value,
description: templateDescription.value,
treeStructure: treeStructure.value,
};
if (isEditMode.value) {
const templateId = route.params.id as string;
templateStore.updateTemplate({
id: templateId,
...templateData,
});
} else {
templateStore.addTemplate(templateData);
}
router.push('/templates');
};
</script>
<template> <template>
<v-container <v-container
fluid fluid
@@ -61,10 +425,10 @@
<div class="d-flex ga-2"> <div class="d-flex ga-2">
<v-btn <v-btn
color="primary" color="primary"
@click="saveTemplate"
:disabled="!formValid" :disabled="!formValid"
prepend-icon="mdi-content-save" prepend-icon="mdi-content-save"
block block
@click="saveTemplate"
> >
保存模板 保存模板
</v-btn> </v-btn>
@@ -77,13 +441,15 @@
class="pa-4" class="pa-4"
> >
<div class="d-flex align-center mb-4"> <div class="d-flex align-center mb-4">
<h3 class="text-h6 mr-2">模板结构</h3> <h3 class="text-h6 mr-2">
模板结构
</h3>
<v-spacer /> <v-spacer />
<v-btn <v-btn
icon icon
variant="text" variant="text"
@click="addRootDirectory"
size="small" size="small"
@click="addRootDirectory"
> >
<v-icon>mdi-folder-plus</v-icon> <v-icon>mdi-folder-plus</v-icon>
<v-tooltip <v-tooltip
@@ -96,8 +462,8 @@
<v-btn <v-btn
icon icon
variant="text" variant="text"
@click="addRootFile"
size="small" size="small"
@click="addRootFile"
> >
<v-icon>mdi-file-plus</v-icon> <v-icon>mdi-file-plus</v-icon>
<v-tooltip <v-tooltip
@@ -107,14 +473,13 @@
添加根文件 添加根文件
</v-tooltip> </v-tooltip>
</v-btn> </v-btn>
</div> </div>
<TemplateTree <TemplateTree
v-model:expanded="expandedNodes"
:items="treeStructure" :items="treeStructure"
:height="treeHeight" :height="treeHeight"
v-model:expanded="expandedNodes" empty-text="暂无文件或目录"
:empty-text="'暂无文件或目录'"
:read-only="false" :read-only="false"
@add-new-item-to-current-directory="addNewItemToCurrentDirectory" @add-new-item-to-current-directory="addNewItemToCurrentDirectory"
@edit-item="editItem" @edit-item="editItem"
@@ -141,387 +506,9 @@
:editing-item="currentEditingItem" :editing-item="currentEditingItem"
@confirm="handleFileDialogConfirm" @confirm="handleFileDialogConfirm"
/> />
</v-container> </v-container>
</template> </template>
<script setup lang="ts">
import { useTemplateStore } from '@/renderer/store/template'
import { TreeNode } from '@/types'
import { useSnackbarStore } from '@/renderer/store/snackbar'
import TemplateTree from '@/renderer/components/TemplateTree.vue'
import DirectoryDialog from '@/renderer/components/DirectoryDialog.vue'
import FileDialog from '@/renderer/components/FileDialog.vue'
import { ConfirmDialogService } from '@/renderer/utils/confirm-dialog-service'
import { generateUniqueId } from '@/renderer/utils'
const router = useRouter()
const route = useRoute()
const templateStore = useTemplateStore()
const snackbar = useSnackbarStore()
// Extract methods from store
const { getTemplateById } = templateStore
// Check if we're copying a template
const isCopyingTemplate = computed(() => !route.params.id && !!route.query.copyId)
// Form state
const formValid = ref(false)
const templateName = ref('')
const templateDescription = ref('')
// Dialogs
const showFileDialog = ref(false)
// Directory dialog state
const showDirectoryDialog = ref(false)
const parentDirectoryForNewItems = ref<TreeNode | null>(null)
const currentEditingItem = ref<TreeNode | null>(null)
// Tree state
const expandedNodes = ref<string[]>([])
const treeHeight = ref(600)
// Item state - 使用树状结构
const treeStructure = ref<TreeNode[]>([])
// Computed
const isEditMode = computed(() => !!route.params.id)
// Validation rules
const requiredRule = (fieldName: string) => (v: string | null | undefined) =>
!!v || `${fieldName}为必填项`
// Lifecycle
onMounted(async () => {
await templateStore.loadTemplates()
// Set tree height based on window height
updateTreeHeight()
// Add resize listener
window.addEventListener('resize', updateTreeHeight)
// Handle edit mode
if (isEditMode.value) {
const templateId = route.params.id as string
const template = getTemplateById(templateId)
if (template) {
templateName.value = template.name
templateDescription.value = template.description
treeStructure.value = [...template.treeStructure]
} else {
router.push('/templates')
}
} else if (isCopyingTemplate.value) {
// Handle copy mode
const copyId = route.query.copyId as string
if (copyId) {
const template = getTemplateById(copyId)
if (template) {
templateName.value = `${template.name} - 副本`
templateDescription.value = template.description
treeStructure.value = [...template.treeStructure]
} else {
snackbar.error('找不到要复制的模板')
router.push('/templates')
}
}
}
})
onUnmounted(() => {
window.removeEventListener('resize', updateTreeHeight)
})
// Methods
const updateTreeHeight = () => {
treeHeight.value = window.innerHeight - 200
}
const addRootDirectory = () => {
parentDirectoryForNewItems.value = null
currentEditingItem.value = null
showDirectoryDialog.value = true
}
const addRootFile = () => {
parentDirectoryForNewItems.value = null
currentEditingItem.value = null
showFileDialog.value = true
}
// Add a file directly to a specific directory
const addFileToDirectory = (directoryItem: TreeNode) => {
parentDirectoryForNewItems.value = directoryItem // Store the directory node as parent
currentEditingItem.value = null
showFileDialog.value = true
}
const addNewItemToCurrentDirectory = (directoryItem: TreeNode) => {
parentDirectoryForNewItems.value = directoryItem // Store the directory node as parent
currentEditingItem.value = null
showDirectoryDialog.value = true
}
const handleDirectoryDialogConfirm = (data: { path: string }) => {
const { path } = data
const newId = generateUniqueId('dir')
// Check if we're editing an existing directory
if (
currentEditingItem.value &&
currentEditingItem.value.type === 'directory'
) {
// Find and update the existing directory in the tree
const updatedNode: TreeNode = {
...currentEditingItem.value,
name: path,
path: path,
type: 'directory'
}
treeStructure.value = updateNodeInTree(
treeStructure.value,
currentEditingItem.value.id,
updatedNode
)
} else {
// Create new directory node
const newNode: TreeNode = {
id: newId,
name: path,
path: path,
type: 'directory'
}
// Add to tree structure
if (parentDirectoryForNewItems.value) {
// In the parent directory, check if a node with the same name already exists
const parentNode = parentDirectoryForNewItems.value
if (parentNode.children) {
const duplicateExists = parentNode.children.some(
(child) => child.name === path
)
if (duplicateExists) {
snackbar.error('当前目录下已存在同名节点!')
return
}
}
// Add as child to the parent directory
parentNode.children = parentNode.children || []
parentNode.children.push(newNode)
} else {
// Check at root level for duplicates
const duplicateExists = treeStructure.value.some(
(node) => node.name === path
)
if (duplicateExists) {
snackbar.error('根目录下已存在同名节点!')
return
}
// Add as root node
treeStructure.value.push(newNode)
}
}
showDirectoryDialog.value = false
parentDirectoryForNewItems.value = null // Clear parent context after creation
currentEditingItem.value = null
}
// 在树状结构中更新节点的辅助函数
const updateNodeInTree = (
nodes: TreeNode[],
oldId: string,
updatedNode: TreeNode
): TreeNode[] => {
return nodes.map((node) => {
if (node.id === oldId) {
return updatedNode
}
if (node.children) {
return {
...node,
children: updateNodeInTree(node.children, oldId, updatedNode)
}
}
return node
})
}
const handleFileDialogConfirm = (data: { path: string; sourcePath?: string; sourceType?: 'file' | 'folder' }) => {
const { path, sourcePath, sourceType } = data
const newId = generateUniqueId('file')
// Check if we're editing an existing file
if (currentEditingItem.value && currentEditingItem.value.type === 'file') {
// Find and update the existing file in the tree
const updatedNode: TreeNode = {
id: currentEditingItem.value.id,
name: path,
path: path,
type: 'file',
sourcePath: sourcePath,
sourceType
}
treeStructure.value = updateNodeInTree(
treeStructure.value,
currentEditingItem.value.id,
updatedNode
)
} else {
// Check for duplicate names in the same parent directory
if (parentDirectoryForNewItems.value) {
// In the parent directory, check if a node with the same name already exists
const parentNode = parentDirectoryForNewItems.value
if (parentNode.children) {
const duplicateExists = parentNode.children.some(
(child) => child.name === path
)
if (duplicateExists) {
snackbar.error('当前目录下已存在同名节点!')
return
}
}
} else {
// Check at root level for duplicates
const duplicateExists = treeStructure.value.some(
(node) => node.name === path
)
if (duplicateExists) {
snackbar.error('根目录下已存在同名节点!')
return
}
}
const newFileNode: TreeNode = {
id: newId,
name: path,
path: path,
type: 'file',
sourcePath,
sourceType
}
// Add to tree structure
if (parentDirectoryForNewItems.value) {
// Add as child to the parent directory
const parentNode = parentDirectoryForNewItems.value
parentNode.children = parentNode.children || []
parentNode.children.push(newFileNode)
} else {
// Add as root node
treeStructure.value.push(newFileNode)
}
}
showFileDialog.value = false
parentDirectoryForNewItems.value = null // Clear parent context after creation
currentEditingItem.value = null
}
const goBack = async () => {
router.push('/templates')
}
// Unified method to edit items (both files and directories)
const editItem = (item: TreeNode) => {
if (item.type === 'directory') {
showEditDirectoryDialog(item)
} else {
showEditFileDialog(item)
}
}
// Show dialog to edit directory
const showEditDirectoryDialog = (directory: TreeNode) => {
// Set up for editing an existing directory
currentEditingItem.value = directory
parentDirectoryForNewItems.value = null
showDirectoryDialog.value = true
}
// Show dialog to edit file
const showEditFileDialog = (file: TreeNode) => {
// Set up for editing an existing file
currentEditingItem.value = file
parentDirectoryForNewItems.value = null
showFileDialog.value = true
}
const deleteItem = async (item: TreeNode) => {
const confirmed = await ConfirmDialogService.confirm({
title: '删除确认',
message: '确定要删除此项吗?',
confirmText: '删除',
cancelText: '取消',
confirmColor: 'error'
})
if (!confirmed) return
// 从树状结构中删除项目
treeStructure.value = removeItemFromTree(treeStructure.value, item.id)
}
// 从树状结构中删除项目的辅助函数
const removeItemFromTree = (nodes: TreeNode[], itemId: string): TreeNode[] => {
return nodes
.filter((node) => node.id !== itemId) // 过滤掉要删除的节点
.map((node) => {
if (node.children) {
return {
...node,
children: removeItemFromTree(node.children, itemId)
}
}
return node
})
}
const saveTemplate = () => {
if (!formValid.value) return
const templateData = {
name: templateName.value,
description: templateDescription.value,
treeStructure: treeStructure.value
}
if (isEditMode.value) {
const templateId = route.params.id as string
templateStore.updateTemplate({
id: templateId,
...templateData
})
} else {
templateStore.addTemplate(templateData)
}
router.push('/templates')
}
</script>
<style scoped> <style scoped>
.border-e { .border-e {
border-right: 1px solid rgba(0, 0, 0, 0.12); border-right: 1px solid rgba(0, 0, 0, 0.12);
+40 -58
View File
@@ -1,79 +1,59 @@
<script setup lang="tsx"> <script setup lang="tsx">
import { useI18n } from 'vue-i18n'
import { useTheme } from 'vuetify'
import { openExternal, openFile } from '@/renderer/utils'
import { useCounterStore } from '@/renderer/store/counter'
import { onMounted, ref, onBeforeMount } from 'vue'
import { import {
mdiBrightness6, mdiBrightness6,
mdiFileDocument, mdiFileDocument,
mdiFolderOpen,
mdiGithub,
mdiPlusCircle,
mdiFolderMultiple, mdiFolderMultiple,
mdiFileDocumentMultiple, mdiGithub,
mdiArrowRight, } from '@mdi/js';
mdiHome import { onMounted, ref } from 'vue';
} from '@mdi/js' import { useI18n } from 'vue-i18n';
import { storeToRefs } from 'pinia' import { useRouter } from 'vue-router';
import { useRouter } from 'vue-router' import { useTheme } from 'vuetify';
import { openExternal } from '@/renderer/utils';
const router = useRouter() const router = useRouter();
const { t, locale, availableLocales } = useI18n() const { t, locale, availableLocales } = useI18n();
const { increaseCount } = useCounterStore() const theme = useTheme();
const { count } = storeToRefs(useCounterStore()) const languages = ref(['en']);
const theme = useTheme() const appVersion = ref('Unknown');
const languages = ref(['en']) const selectedFile = ref('');
const appVersion = ref('Unknown')
const selectedFile = ref('')
onMounted((): void => { onMounted((): void => {
languages.value = availableLocales languages.value = availableLocales;
// Get application version from package.json version string (Using IPC communication) // Get application version from package.json version string (Using IPC communication)
getApplicationVersionFromMainProcess() getApplicationVersionFromMainProcess();
}) });
const getApplicationVersionFromMainProcess = (): void => { const getApplicationVersionFromMainProcess = (): void => {
window.mainApi.invoke('msgRequestGetVersion').then((result: string) => { window.mainApi.invoke('msgRequestGetVersion').then((result: string) => {
appVersion.value = result appVersion.value = result;
}) });
} };
const handleChangeTheme = (): void => { const handleChangeTheme = (): void => {
theme.global.name.value = theme.global.current.value.dark ? 'light' : 'dark' theme.global.name.value = theme.global.current.value.dark ? 'light' : 'dark';
} };
const handleChangeLanguage = (val): void => { const handleChangeLanguage = (val): void => {
locale.value = val locale.value = val;
} };
const handleOpenDocument = async (): Promise<void> => { const handleOpenDocument = async (): Promise<void> => {
await openExternal('https://vutron.cdget.com') await openExternal('https://vutron.cdget.com');
} };
const handleOpenGitHub = async (): Promise<void> => { const handleOpenGitHub = async (): Promise<void> => {
await openExternal('https://github.com/jooy2/vutron') await openExternal('https://github.com/jooy2/vutron');
} };
const handleCountIncrease = (): void => {
increaseCount(1)
}
const handleOpenFile = async () => {
const dialogResult = await openFile('text')
if (!dialogResult.canceled) {
selectedFile.value = dialogResult.filePaths[0]
}
}
const navigateToTemplates = () => { const navigateToTemplates = () => {
router.push('/templates') router.push('/templates');
} };
const navigateToCreateTemplate = () => { const navigateToCreateTemplate = () => {
router.push('/create-template') router.push('/create-template');
} };
</script> </script>
<template> <template>
@@ -103,7 +83,7 @@ const navigateToCreateTemplate = () => {
<p v-if="selectedFile"> <p v-if="selectedFile">
{{ {{
t('desc.selected-file', { t('desc.selected-file', {
filePath: selectedFile filePath: selectedFile,
}) })
}} }}
</p> </p>
@@ -112,17 +92,17 @@ const navigateToCreateTemplate = () => {
<v-btn <v-btn
color="primary" color="primary"
size="x-large" size="x-large"
@click="navigateToCreateTemplate"
class="mr-4" class="mr-4"
prepend-icon="mdi-plus" prepend-icon="mdi-plus"
@click="navigateToCreateTemplate"
> >
创建模板 创建模板
</v-btn> </v-btn>
<v-btn <v-btn
color="success" color="success"
size="x-large" size="x-large"
@click="navigateToTemplates"
prepend-icon="mdi-folder-multiple" prepend-icon="mdi-folder-multiple"
@click="navigateToTemplates"
> >
管理模板 管理模板
</v-btn> </v-btn>
@@ -136,7 +116,9 @@ const navigateToCreateTemplate = () => {
<v-expansion-panels variant="accordion"> <v-expansion-panels variant="accordion">
<v-expansion-panel> <v-expansion-panel>
<v-expansion-panel-title> <v-expansion-panel-title>
<v-icon start>{{ mdiFolderMultiple }}</v-icon> <v-icon start>
{{ mdiFolderMultiple }}
</v-icon>
如何使用模板生成器 如何使用模板生成器
</v-expansion-panel-title> </v-expansion-panel-title>
<v-expansion-panel-text> <v-expansion-panel-text>
@@ -172,8 +154,8 @@ const navigateToCreateTemplate = () => {
<v-btn <v-btn
color="primary" color="primary"
@click="handleOpenDocument"
class="ml-2" class="ml-2"
@click="handleOpenDocument"
> >
<v-icon :icon="mdiFileDocument" start /> <v-icon :icon="mdiFileDocument" start />
{{ t('menu.documentation') }} {{ t('menu.documentation') }}
@@ -181,8 +163,8 @@ const navigateToCreateTemplate = () => {
<v-btn <v-btn
color="primary" color="primary"
@click="handleOpenGitHub"
class="ml-2" class="ml-2"
@click="handleOpenGitHub"
> >
<v-icon :icon="mdiGithub" start /> <v-icon :icon="mdiGithub" start />
{{ t('menu.github') }} {{ t('menu.github') }}
@@ -199,8 +181,8 @@ const navigateToCreateTemplate = () => {
density="compact" density="compact"
:label="t('menu.change-language')" :label="t('menu.change-language')"
:items="languages" :items="languages"
@update:model-value="handleChangeLanguage"
class="ml-2" class="ml-2"
@update:model-value="handleChangeLanguage"
/> />
</v-col> </v-col>
</v-row> </v-row>
+3 -3
View File
@@ -1,8 +1,8 @@
<script setup lang="tsx"> <script setup lang="tsx">
import { useI18n } from 'vue-i18n' import { mdiEmoticonCoolOutline } from '@mdi/js';
import { mdiEmoticonCoolOutline } from '@mdi/js' import { useI18n } from 'vue-i18n';
const { t } = useI18n() const { t } = useI18n();
</script> </script>
<template> <template>
+145 -137
View File
@@ -1,10 +1,139 @@
<script setup lang="ts">
import type { Template, TreeNode } from '@/types';
import { computed, onMounted, ref } from 'vue';
import { useRouter } from 'vue-router';
import TemplateCard from '@/renderer/components/TemplateCard.vue';
import TemplateTree from '@/renderer/components/TemplateTree.vue';
import { useTemplateStore } from '@/renderer/store/template';
import { ConfirmDialogService } from '@/renderer/utils/confirm-dialog-service';
const router = useRouter();
const templateStore = useTemplateStore();
// Initialize templates
onMounted(async () => {
await templateStore.loadTemplates();
});
const { deleteTemplate: storeDeleteTemplate } = templateStore;
const templates = computed(() => templateStore.templates);
// Template detail view
const showTemplateDetail = ref(false);
const selectedTemplate = ref<Template | null>(null);
const expandedTreeNodes = ref<string[]>([]);
// Execute template
const showExecuteDialog = ref(false);
const executePath = ref('');
const baseDirNames = ref('');
const executeFormValid = ref(false);
const templateTree = computed<TreeNode[]>(() => {
if (!selectedTemplate.value) {
return [];
}
// 直接返回模板的树状结构
return selectedTemplate.value.treeStructure || [];
});
const navigateToCreateTemplate = () => {
router.push('/create-template');
};
const viewTemplate = (template: Template) => {
selectedTemplate.value = template;
showTemplateDetail.value = true;
};
const editTemplate = (template: Template) => {
router.push(`/create-template/${template.id}`);
};
const copyTemplate = (template: Template) => {
router.push(`/create-template?copyId=${template.id}`);
};
const deleteTemplate = async (template: Template) => {
const confirmed = await ConfirmDialogService.confirm({
title: '删除模板',
message: `确定要删除模板 "${template.name}" 吗?此操作不可撤销。`,
confirmText: '删除',
cancelText: '取消',
confirmColor: 'error',
});
if (confirmed) {
storeDeleteTemplate(template.id);
}
};
const executeTemplate = (template: Template) => {
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() || !selectedTemplate.value) {
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(async (result: any) => {
if (result.success) {
await ConfirmDialogService.success({
title: '执行成功',
message: '模板执行成功!',
confirmText: '确定',
});
showExecuteDialog.value = false;
} else {
await ConfirmDialogService.error({
title: '执行失败',
message: `模板执行失败: ${result.error}`,
confirmText: '确定',
});
}
})
.catch(async (error: any) => {
await ConfirmDialogService.error({
title: '执行出错',
message: `模板执行出错: ${error.message}`,
confirmText: '确定',
});
});
};
</script>
<template> <template>
<v-container> <v-container>
<v-row> <v-row>
<v-col cols="12"> <v-col cols="12">
<div class="d-flex align-center mb-4 justify-space-between flex-wrap"> <div class="d-flex align-center mb-4 justify-space-between flex-wrap">
<h2 class="mr-2 mb-2"> 文件创建 </h2> <h2 class="mr-2 mb-2">
<v-btn @click="navigateToCreateTemplate" color="primary" prepend-icon="mdi-plus" class="mb-2"> 文件创建
</h2>
<v-btn color="primary" prepend-icon="mdi-plus" class="mb-2" @click="navigateToCreateTemplate">
创建模板 创建模板
</v-btn> </v-btn>
</div> </div>
@@ -18,7 +147,7 @@
<p>还没有创建任何模板。创建您的第一个模板以开始批量生成文件和目录。</p> <p>还没有创建任何模板。创建您的第一个模板以开始批量生成文件和目录。</p>
</v-card-text> </v-card-text>
<v-card-actions class="justify-center"> <v-card-actions class="justify-center">
<v-btn color="primary" size="default" @click="navigateToCreateTemplate" prepend-icon="mdi-plus"> <v-btn color="primary" size="default" prepend-icon="mdi-plus" @click="navigateToCreateTemplate">
创建模板 创建模板
</v-btn> </v-btn>
</v-card-actions> </v-card-actions>
@@ -54,7 +183,7 @@
:items="templateTree" :items="templateTree"
:height="400" :height="400"
:read-only="true" :read-only="true"
:empty-text="'模板结构为空'" empty-text="模板结构为空"
:expanded="expandedTreeNodes" :expanded="expandedTreeNodes"
@update:expanded="expandedTreeNodes = $event" @update:expanded="expandedTreeNodes = $event"
/> />
@@ -68,147 +197,26 @@
<v-card-title>执行模板: {{ selectedTemplate?.name }}</v-card-title> <v-card-title>执行模板: {{ selectedTemplate?.name }}</v-card-title>
<v-card-text> <v-card-text>
<v-form v-model="executeFormValid"> <v-form v-model="executeFormValid">
<v-text-field v-model="executePath" label="选择目录" readonly @click="selectDirectory" <v-text-field
prepend-inner-icon="mdi-folder" :rules="[(v) => !!v || '请选择目录']" /> v-model="executePath" label="选择目录" readonly prepend-inner-icon="mdi-folder"
<v-textarea v-model="baseDirNames" label="基础目录名 (每行一个,支持批量)" placeholder="输入基础目录名,每行一个" :rules="[(v) => !!v || '请选择目录']" @click="selectDirectory"
:rules="[(v) => !!v || '请输入基础目录名']" /> />
<v-textarea
v-model="baseDirNames" label="基础目录名 (每行一个,支持批量)" placeholder="输入基础目录名,每行一个"
:rules="[(v) => !!v || '请输入基础目录名']"
/>
</v-form> </v-form>
</v-card-text> </v-card-text>
<v-card-actions> <v-card-actions>
<v-spacer /> <v-spacer />
<v-btn @click="showExecuteDialog = false"> 取消 </v-btn> <v-btn @click="showExecuteDialog = false">
<v-btn color="success" @click="confirmExecuteTemplate" :disabled="!executeFormValid"> 取消
</v-btn>
<v-btn color="success" :disabled="!executeFormValid" @click="confirmExecuteTemplate">
确认执行 确认执行
</v-btn> </v-btn>
</v-card-actions> </v-card-actions>
</v-card> </v-card>
</v-dialog> </v-dialog>
</v-container> </v-container>
</template> </template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useTemplateStore } from '@/renderer/store/template'
import { TreeNode, Template } from '@/types'
import TemplateTree from '@/renderer/components/TemplateTree.vue'
import TemplateCard from '@/renderer/components/TemplateCard.vue'
import { ConfirmDialogService } from '@/renderer/utils/confirm-dialog-service'
const router = useRouter()
const templateStore = useTemplateStore()
// Initialize templates
onMounted(async () => {
await templateStore.loadTemplates()
})
const { deleteTemplate: storeDeleteTemplate } = templateStore
const templates = computed(() => templateStore.templates)
// Template detail view
const showTemplateDetail = ref(false)
const selectedTemplate = ref<Template | null>(null)
const expandedTreeNodes = ref<string[]>([])
// Execute template
const showExecuteDialog = ref(false)
const executePath = ref('')
const baseDirNames = ref('')
const executeFormValid = ref(false)
const templateTree = computed<TreeNode[]>(() => {
if (!selectedTemplate.value) return []
// 直接返回模板的树状结构
return selectedTemplate.value.treeStructure || []
})
const navigateToCreateTemplate = () => {
router.push('/create-template')
}
const viewTemplate = (template: Template) => {
selectedTemplate.value = template
showTemplateDetail.value = true
}
const editTemplate = (template: Template) => {
router.push(`/create-template/${template.id}`)
}
const copyTemplate = (template: Template) => {
router.push(`/create-template?copyId=${template.id}`)
}
const deleteTemplate = async (template: Template) => {
const confirmed = await ConfirmDialogService.confirm({
title: '删除模板',
message: `确定要删除模板 "${template.name}" 吗?此操作不可撤销。`,
confirmText: '删除',
cancelText: '取消',
confirmColor: 'error'
})
if (confirmed) {
storeDeleteTemplate(template.id)
}
}
const executeTemplate = (template: Template) => {
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() || !selectedTemplate.value) 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(async (result: any) => {
if (result.success) {
await ConfirmDialogService.success({
title: '执行成功',
message: '模板执行成功!',
confirmText: '确定'
})
showExecuteDialog.value = false
} else {
await ConfirmDialogService.error({
title: '执行失败',
message: `模板执行失败: ${result.error}`,
confirmText: '确定'
})
}
})
.catch(async (error: any) => {
await ConfirmDialogService.error({
title: '执行出错',
message: `模板执行出错: ${error.message}`,
confirmText: '确定'
})
})
}
</script>
+7 -7
View File
@@ -1,13 +1,13 @@
import ErrorScreen from '@/renderer/screens/ErrorScreen.vue' import CreateTemplateScreen from '@/renderer/screens/CreateTemplateScreen.vue';
import MainScreen from '@/renderer/screens/MainScreen.vue' import ErrorScreen from '@/renderer/screens/ErrorScreen.vue';
import SecondScreen from '@/renderer/screens/SecondScreen.vue' import MainScreen from '@/renderer/screens/MainScreen.vue';
import TemplateListScreen from '@/renderer/screens/TemplateListScreen.vue' import SecondScreen from '@/renderer/screens/SecondScreen.vue';
import CreateTemplateScreen from '@/renderer/screens/CreateTemplateScreen.vue' import TemplateListScreen from '@/renderer/screens/TemplateListScreen.vue';
export { export {
CreateTemplateScreen,
ErrorScreen, ErrorScreen,
MainScreen, MainScreen,
SecondScreen, SecondScreen,
TemplateListScreen, TemplateListScreen,
CreateTemplateScreen };
}
+22 -22
View File
@@ -1,58 +1,58 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia';
export interface UpdateStatus { export interface UpdateStatus {
status: 'idle' | 'checking' | 'available' | 'not-available' | 'downloading' | 'downloaded' | 'error' status: 'idle' | 'checking' | 'available' | 'not-available' | 'downloading' | 'downloaded' | 'error';
currentVersion?: string currentVersion?: string;
error?: string error?: string;
progress?: number progress?: number;
} }
export const useAppStore = defineStore('app', { export const useAppStore = defineStore('app', {
state: () => ({ state: () => ({
updateStatus: { updateStatus: {
status: 'idle' as const status: 'idle' as const,
} as UpdateStatus, } as UpdateStatus,
updateDialogVisible: false, updateDialogVisible: false,
downloadProgress: 0 downloadProgress: 0,
}), }),
getters: { getters: {
isUpdateAvailable: (state) => state.updateStatus.status === 'available', isUpdateAvailable: state => state.updateStatus.status === 'available',
isCheckingForUpdates: (state) => state.updateStatus.status === 'checking', isCheckingForUpdates: state => state.updateStatus.status === 'checking',
hasUpdateError: (state) => state.updateStatus.status === 'error' hasUpdateError: state => state.updateStatus.status === 'error',
}, },
actions: { actions: {
setUpdateStatus(status: UpdateStatus['status'], currentVersion?: string, error?: string) { setUpdateStatus(status: UpdateStatus['status'], currentVersion?: string, error?: string) {
this.updateStatus = { status, currentVersion, error } this.updateStatus = { status, currentVersion, error };
}, },
showUpdateDialog() { showUpdateDialog() {
this.updateDialogVisible = true this.updateDialogVisible = true;
}, },
hideUpdateDialog() { hideUpdateDialog() {
this.updateDialogVisible = false this.updateDialogVisible = false;
}, },
setDownloadProgress(progress: number) { setDownloadProgress(progress: number) {
this.downloadProgress = progress this.downloadProgress = progress;
}, },
// 启动更新下载 // 启动更新下载
async startUpdateDownload() { async startUpdateDownload() {
try { try {
this.setUpdateStatus('downloading') this.setUpdateStatus('downloading');
const result = await window.mainApi.invoke('msgDownloadUpdate') const result = await window.mainApi.invoke('msgDownloadUpdate');
if (result.success) { if (result.success) {
this.setUpdateStatus('downloaded') this.setUpdateStatus('downloaded');
} else { } else {
this.setUpdateStatus('error', undefined, result.error) this.setUpdateStatus('error', undefined, result.error);
} }
} catch (error) { } catch (error) {
this.setUpdateStatus('error', undefined, error instanceof Error ? error.message : String(error)) this.setUpdateStatus('error', undefined, error instanceof Error ? error.message : String(error));
} }
} },
} },
}) });
+7 -7
View File
@@ -1,15 +1,15 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia';
import { ref } from 'vue' import { ref } from 'vue';
export const useCounterStore = defineStore('counter', () => { export const useCounterStore = defineStore('counter', () => {
const count = ref(0) const count = ref(0);
function increaseCount(amount: number) { function increaseCount(amount: number) {
count.value += amount count.value += amount;
} }
return { return {
count, count,
increaseCount increaseCount,
} };
}) });
+26 -26
View File
@@ -1,36 +1,36 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia';
import { ref } from 'vue' import { ref } from 'vue';
export interface SnackbarAction { export interface SnackbarAction {
text: string text: string;
color?: string color?: string;
handler?: () => void handler?: () => void;
} }
export const useSnackbarStore = defineStore('snackbar', () => { export const useSnackbarStore = defineStore('snackbar', () => {
const isVisible = ref(false) const isVisible = ref(false);
const message = ref('') const message = ref('');
const color = ref('info') const color = ref('info');
const timeout = ref(3000) const timeout = ref(3000);
const actions = ref<SnackbarAction[]>([]) const actions = ref<SnackbarAction[]>([]);
const show = (msg: string, clr = 'info', tout = 3000, actionList: SnackbarAction[] = []) => { const show = (msg: string, clr = 'info', tout = 3000, actionList: SnackbarAction[] = []) => {
message.value = msg message.value = msg;
color.value = clr color.value = clr;
timeout.value = tout timeout.value = tout;
actions.value = actionList actions.value = actionList;
isVisible.value = true isVisible.value = true;
} };
const success = (msg: string, tout = 3000) => show(msg, 'success', tout) const success = (msg: string, tout = 3000) => show(msg, 'success', tout);
const error = (msg: string, tout = 3000) => show(msg, 'error', tout) const error = (msg: string, tout = 3000) => show(msg, 'error', tout);
const warning = (msg: string, tout = 3000) => show(msg, 'warning', tout) const warning = (msg: string, tout = 3000) => show(msg, 'warning', tout);
const info = (msg: string, tout = 3000) => show(msg, 'info', tout) const info = (msg: string, tout = 3000) => show(msg, 'info', tout);
const hide = () => { const hide = () => {
isVisible.value = false isVisible.value = false;
actions.value = [] actions.value = [];
} };
return { return {
isVisible, isVisible,
@@ -43,6 +43,6 @@ export const useSnackbarStore = defineStore('snackbar', () => {
error, error,
warning, warning,
info, info,
hide hide,
} };
}) });
+30 -30
View File
@@ -1,24 +1,24 @@
import { defineStore } from 'pinia' import type { Template } from '@/types';
import { ref } from 'vue' import { defineStore } from 'pinia';
import { TreeNode, Template } from '@/types' import { ref } from 'vue';
export const useTemplateStore = defineStore('template', () => { export const useTemplateStore = defineStore('template', () => {
const templates = ref<Template[]>([]) const templates = ref<Template[]>([]);
const dataFile = 'templates.json' const dataFile = 'templates.json';
// Load templates from local storage or initialize // Load templates from local storage or initialize
async function loadTemplates() { async function loadTemplates() {
try { try {
const stored = await window.mainApi.invoke('msgLoadData', dataFile) const stored = await window.mainApi.invoke('msgLoadData', dataFile);
if (stored) { if (stored) {
templates.value = JSON.parse(stored as string) || [] templates.value = JSON.parse(stored as string) || [];
} else { } else {
// Initialize with sample data if no stored data // Initialize with sample data if no stored data
templates.value = [] templates.value = [];
} }
} catch (error) { } catch (error) {
console.error('Failed to load templates:', error) console.error('Failed to load templates:', error);
templates.value = [] templates.value = [];
} }
} }
@@ -28,54 +28,54 @@ export const useTemplateStore = defineStore('template', () => {
window.mainApi.invoke( window.mainApi.invoke(
'msgSaveData', 'msgSaveData',
dataFile, dataFile,
JSON.stringify(templates.value) JSON.stringify(templates.value),
) );
} catch (error) { } catch (error) {
console.error('Failed to save templates:', error) console.error('Failed to save templates:', error);
} }
} }
// Get all templates // Get all templates
function getTemplates() { function getTemplates() {
return templates.value return templates.value;
} }
// Get template by ID // Get template by ID
function getTemplateById(id: string) { function getTemplateById(id: string) {
return templates.value.find((template) => template.id === id) return templates.value.find(template => template.id === id);
} }
// Add new template // Add new template
function addTemplate(template: Omit<Template, 'id'>) { function addTemplate(template: Omit<Template, 'id'>) {
const newTemplate: Template = { const newTemplate: Template = {
id: `template_${Date.now()}`, id: `template_${Date.now()}`,
...template ...template,
} };
templates.value.push(newTemplate) templates.value.push(newTemplate);
saveTemplates() saveTemplates();
} }
// Update existing template // Update existing template
function updateTemplate(updatedTemplate: Template) { function updateTemplate(updatedTemplate: Template) {
const index = templates.value.findIndex((t) => t.id === updatedTemplate.id) const index = templates.value.findIndex(t => t.id === updatedTemplate.id);
if (index !== -1) { if (index !== -1) {
templates.value[index] = updatedTemplate templates.value[index] = updatedTemplate;
saveTemplates() saveTemplates();
} }
} }
// Delete template // Delete template
function deleteTemplate(id: string) { function deleteTemplate(id: string) {
templates.value = templates.value.filter((template) => template.id !== id) templates.value = templates.value.filter(template => template.id !== id);
saveTemplates() saveTemplates();
} }
// Get template names for dropdowns // Get template names for dropdowns
function getTemplateNames() { function getTemplateNames() {
return templates.value.map((template) => ({ return templates.value.map(template => ({
text: template.name, text: template.name,
value: template.id value: template.id,
})) }));
} }
return { return {
@@ -86,6 +86,6 @@ export const useTemplateStore = defineStore('template', () => {
addTemplate, addTemplate,
updateTemplate, updateTemplate,
deleteTemplate, deleteTemplate,
getTemplateNames getTemplateNames,
} };
}) });
+29 -32
View File
@@ -1,25 +1,22 @@
// 定义对话框选项接口 // 定义对话框选项接口
interface BaseDialogOptions { interface BaseDialogOptions {
title: string title: string;
message: string message: string;
confirmText?: string confirmText?: string;
cancelText?: string cancelText?: string;
confirmColor?: string confirmColor?: string;
} }
interface ConfirmDialogOptions extends BaseDialogOptions { } interface ConfirmDialogOptions extends BaseDialogOptions { }
interface MessageDialogOptions extends BaseDialogOptions { interface MessageDialogOptions extends BaseDialogOptions {
dialogColor?: string dialogColor?: string;
} }
// 对话框类型
type DialogType = 'confirm' | 'info' | 'success' | 'warning' | 'error'
// 全局对话框管理器接口 // 全局对话框管理器接口
interface GlobalDialogManager { interface GlobalDialogManager {
showConfirm: (options: ConfirmDialogOptions) => Promise<boolean> showConfirm: (options: ConfirmDialogOptions) => Promise<boolean>;
showMessage: (options: MessageDialogOptions) => Promise<void> showMessage: (options: MessageDialogOptions) => Promise<void>;
} }
// 确认对话框服务 // 确认对话框服务
@@ -27,11 +24,11 @@ export const ConfirmDialogService = {
// 显示确认对话框 // 显示确认对话框
async confirm(options: ConfirmDialogOptions): Promise<boolean> { async confirm(options: ConfirmDialogOptions): Promise<boolean> {
if (window.ConfirmDialogManager) { if (window.ConfirmDialogManager) {
return (window.ConfirmDialogManager as GlobalDialogManager).showConfirm(options) return (window.ConfirmDialogManager as GlobalDialogManager).showConfirm(options);
} else { } else {
console.error('GlobalDialog component not properly initialized') console.error('GlobalDialog component not properly initialized');
// 降级到原生 confirm // 降级到原生 confirm
return confirm(`${options.title}: ${options.message}`) return confirm(`${options.title}: ${options.message}`);
} }
}, },
@@ -41,12 +38,12 @@ export const ConfirmDialogService = {
return (window.ConfirmDialogManager as GlobalDialogManager).showMessage({ return (window.ConfirmDialogManager as GlobalDialogManager).showMessage({
...options, ...options,
dialogColor: options.dialogColor || 'info', dialogColor: options.dialogColor || 'info',
confirmColor: options.confirmColor || 'white' confirmColor: options.confirmColor || 'white',
}) });
} else { } else {
console.error('GlobalDialog component not properly initialized') console.error('GlobalDialog component not properly initialized');
// 降级到原生 alert // 降级到原生 alert
alert(`${options.title}: ${options.message}`) alert(`${options.title}: ${options.message}`);
} }
}, },
@@ -56,12 +53,12 @@ export const ConfirmDialogService = {
return (window.ConfirmDialogManager as GlobalDialogManager).showMessage({ return (window.ConfirmDialogManager as GlobalDialogManager).showMessage({
...options, ...options,
dialogColor: options.dialogColor || 'success', dialogColor: options.dialogColor || 'success',
confirmColor: options.confirmColor || 'white' confirmColor: options.confirmColor || 'white',
}) });
} else { } else {
console.error('GlobalDialog component not properly initialized') console.error('GlobalDialog component not properly initialized');
// 降级到原生 alert // 降级到原生 alert
alert(`${options.title}: ${options.message}`) alert(`${options.title}: ${options.message}`);
} }
}, },
@@ -71,12 +68,12 @@ export const ConfirmDialogService = {
return (window.ConfirmDialogManager as GlobalDialogManager).showMessage({ return (window.ConfirmDialogManager as GlobalDialogManager).showMessage({
...options, ...options,
dialogColor: options.dialogColor || 'warning', dialogColor: options.dialogColor || 'warning',
confirmColor: options.confirmColor || 'white' confirmColor: options.confirmColor || 'white',
}) });
} else { } else {
console.error('GlobalDialog component not properly initialized') console.error('GlobalDialog component not properly initialized');
// 降级到原生 alert // 降级到原生 alert
alert(`${options.title}: ${options.message}`) alert(`${options.title}: ${options.message}`);
} }
}, },
@@ -86,12 +83,12 @@ export const ConfirmDialogService = {
return (window.ConfirmDialogManager as GlobalDialogManager).showMessage({ return (window.ConfirmDialogManager as GlobalDialogManager).showMessage({
...options, ...options,
dialogColor: options.dialogColor || 'error', dialogColor: options.dialogColor || 'error',
confirmColor: options.confirmColor || 'white' confirmColor: options.confirmColor || 'white',
}) });
} else { } else {
console.error('GlobalDialog component not properly initialized') console.error('GlobalDialog component not properly initialized');
// 降级到原生 alert // 降级到原生 alert
alert(`${options.title}: ${options.message}`) alert(`${options.title}: ${options.message}`);
} }
} },
} };
+6 -6
View File
@@ -1,13 +1,13 @@
export function getCurrentLocale(): string { export function getCurrentLocale(): string {
return navigator?.language?.split('-')[0] || 'en' return navigator?.language?.split('-')[0] || 'en';
} }
export async function openExternal(url: string): Promise<void> { export async function openExternal(url: string): Promise<void> {
await window.mainApi.send('msgOpenExternalLink', url) await window.mainApi.send('msgOpenExternalLink', url);
} }
export async function openFile(type: string): Promise<any> { export async function openFile(type: string): Promise<any> {
return window.mainApi.invoke('msgOpenFile', type) return window.mainApi.invoke('msgOpenFile', type);
} }
/** /**
@@ -16,7 +16,7 @@ export async function openFile(type: string): Promise<any> {
* @returns 唯一ID字符串 * @returns 唯一ID字符串
*/ */
export function generateUniqueId(prefix: string = ''): string { export function generateUniqueId(prefix: string = ''): string {
const timestamp = Date.now().toString(36) // 转换为36进制,更短 const timestamp = Date.now().toString(36); // 转换为36进制,更短
const randomStr = Math.random().toString(36).substring(2, 9) // 7位随机字符 const randomStr = Math.random().toString(36).substring(2, 9); // 7位随机字符
return prefix ? `${prefix}-${timestamp}-${randomStr}` : `${timestamp}-${randomStr}` return prefix ? `${prefix}-${timestamp}-${randomStr}` : `${timestamp}-${randomStr}`;
} }
+11 -11
View File
@@ -1,18 +1,18 @@
// 定义树状结构的节点 // 定义树状结构的节点
export interface TreeNode { export interface TreeNode {
id: string id: string;
name: string name: string;
path: string // 完整路径 path: string; // 完整路径
type: 'directory' | 'file' type: 'directory' | 'file';
children?: TreeNode[] children?: TreeNode[];
sourcePath?: string sourcePath?: string;
sourceType?: 'file' | 'folder' // 记录选择的是文件还是文件夹 sourceType?: 'file' | 'folder'; // 记录选择的是文件还是文件夹
} }
export interface Template { export interface Template {
id: string id: string;
name: string name: string;
description: string description: string;
// 使用树状结构存储 // 使用树状结构存储
treeStructure: TreeNode[] treeStructure: TreeNode[];
} }
+4 -3
View File
@@ -1,6 +1,7 @@
// For fix typescript import errors // For fix typescript import errors
declare module '*.vue' { declare module '*.vue' {
import { defineComponent } from 'vue' import type { defineComponent } from 'vue';
const component: ReturnType<typeof defineComponent>
export default component const component: ReturnType<typeof defineComponent>;
export default component;
} }
+39 -38
View File
@@ -1,17 +1,18 @@
import * as base from '@playwright/test' import type { ElectronApplication, Page } from 'playwright';
import { _electron as electron, Page, ElectronApplication } from 'playwright' import { join } from 'node:path';
import { join } from 'path' import * as base from '@playwright/test';
import { main } from '../package.json' import { _electron as electron } from 'playwright';
import TestUtil from './testUtil.mjs' import { main } from '../package.json';
import TestUtil from './testUtil.mjs';
let appElectron: ElectronApplication let appElectron: ElectronApplication;
let page: Page let page: Page;
const __cwd = process.cwd() const __cwd = process.cwd();
const __isCiProcess = process.env.CI === 'true' const __isCiProcess = process.env.CI === 'true';
const __testPath = join(__cwd, 'tests') const __testPath = join(__cwd, 'tests');
const __testResultPath = join(__testPath, 'results') const __testResultPath = join(__testPath, 'results');
const __testScreenshotPath = join(__testResultPath, 'screenshots') const __testScreenshotPath = join(__testResultPath, 'screenshots');
export const beforeAll = async () => { export const beforeAll = async () => {
// Open Electron app from build directory // Open Electron app from build directory
@@ -23,60 +24,60 @@ export const beforeAll = async () => {
'--ignore-certificate-errors', '--ignore-certificate-errors',
'--ignore-ssl-errors', '--ignore-ssl-errors',
'--ignore-blocklist', '--ignore-blocklist',
'--ignore-gpu-blocklist' '--ignore-gpu-blocklist',
], ],
locale: 'en-US', locale: 'en-US',
colorScheme: 'light', colorScheme: 'light',
env: { env: {
...process.env, ...process.env,
NODE_ENV: 'production' NODE_ENV: 'production',
} },
}) });
page = await appElectron.firstWindow() page = await appElectron.firstWindow();
await page.waitForEvent('load') await page.waitForEvent('load');
page.on('console', console.log) page.on('console', console.log);
page.on('pageerror', console.log) page.on('pageerror', console.log);
const evaluateResult = await appElectron.evaluate( const evaluateResult = await appElectron.evaluate(
async ({ app, BrowserWindow }) => { async ({ app, BrowserWindow }) => {
const currentWindow = BrowserWindow.getFocusedWindow() const currentWindow = BrowserWindow.getFocusedWindow();
// Fix window position for testing // Fix window position for testing
currentWindow.setPosition(50, 50) currentWindow.setPosition(50, 50);
currentWindow.setSize(1080, 560) currentWindow.setSize(1080, 560);
return { return {
packaged: app.isPackaged, packaged: app.isPackaged,
dataPath: app.getPath('userData') dataPath: app.getPath('userData'),
} };
} },
) );
base.expect(evaluateResult.packaged, 'app is not packaged').toBe(false) base.expect(evaluateResult.packaged, 'app is not packaged').toBe(false);
} };
export const afterAll = async () => { export const afterAll = async () => {
await appElectron.close() await appElectron.close();
} };
export const test = base.test.extend({ export const test = base.test.extend({
// eslint-disable-next-line no-empty-pattern // eslint-disable-next-line no-empty-pattern
page: async ({}, use) => { page: async ({}, use) => {
await use(page) await use(page);
}, },
// @ts-expect-error: `util` is not using types in playwright // @ts-expect-error: `util` is not using types in playwright
util: async ({ page }, use, testInfo) => { util: async ({ page }, use, testInfo) => {
await use(new TestUtil(page, testInfo, __testScreenshotPath)) await use(new TestUtil(page, testInfo, __testScreenshotPath));
} },
}) });
export const expect = base.expect export const expect = base.expect;
export default { export default {
test, test,
expect, expect,
beforeAll, beforeAll,
afterAll afterAll,
} };
+15 -15
View File
@@ -1,38 +1,38 @@
import { test, expect, beforeAll, afterAll } from '../fixtures.mjs' import { afterAll, beforeAll, expect, test } from '../fixtures.mjs';
test.beforeAll(beforeAll) test.beforeAll(beforeAll);
test.afterAll(afterAll) test.afterAll(afterAll);
// @ts-expect-error: `util` is not using types in playwright // @ts-expect-error: `util` is not using types in playwright
test('Document element check', async ({ page, util }) => { test('Document element check', async ({ page, util }) => {
try { try {
await expect( await expect(
page.getByTestId('main-logo').first(), page.getByTestId('main-logo').first(),
`Confirm main logo is visible` `Confirm main logo is visible`,
).toBeVisible() ).toBeVisible();
await expect( await expect(
page.getByTestId('select-language').first(), page.getByTestId('select-language').first(),
`Confirm language selector is visible` `Confirm language selector is visible`,
).toBeVisible() ).toBeVisible();
await util.captureScreenshot(page, 'result') await util.captureScreenshot(page, 'result');
} catch (error) { } catch (error) {
throw await util.onTestError(error) throw await util.onTestError(error);
} }
}) });
// @ts-expect-error: `util` is not using types in playwright // @ts-expect-error: `util` is not using types in playwright
test('Counter button click check', async ({ page, util }) => { test('Counter button click check', async ({ page, util }) => {
try { try {
await page.getByTestId('btn-counter').click({ clickCount: 10, delay: 50 }) await page.getByTestId('btn-counter').click({ clickCount: 10, delay: 50 });
const counterValueElement = await page const counterValueElement = await page
.getByTestId('counter-badge') .getByTestId('counter-badge')
.getByRole('status') .getByRole('status')
.innerHTML() .innerHTML();
expect(counterValueElement).toBe('10') expect(counterValueElement).toBe('10');
} catch (error) { } catch (error) {
throw await util.onTestError(error) throw await util.onTestError(error);
} }
}) });
+68 -68
View File
@@ -1,174 +1,174 @@
import { test, expect, beforeAll, afterAll } from '../fixtures.mjs' import { afterAll, beforeAll, expect, test } from '../fixtures.mjs';
test.beforeAll(beforeAll) test.beforeAll(beforeAll);
test.afterAll(afterAll) test.afterAll(afterAll);
// @ts-expect-error: `util` is not using types in playwright // @ts-expect-error: `util` is not using types in playwright
test('Template list page navigation and basic functionality', async ({ test('Template list page navigation and basic functionality', async ({
page, page,
util util,
}) => { }) => {
try { try {
// Navigate to templates page // Navigate to templates page
await page.goto('/') await page.goto('/');
await page.waitForURL('**/#/templates') await page.waitForURL('**/#/templates');
// Check if we're on the templates page // Check if we're on the templates page
await expect(page.getByText('模板列表')).toBeVisible() await expect(page.getByText('模板列表')).toBeVisible();
// Check if "No templates" message is visible initially // Check if "No templates" message is visible initially
await expect(page.getByText('暂无模板,请先创建模板。')).toBeVisible() await expect(page.getByText('暂无模板,请先创建模板。')).toBeVisible();
// Check if "Create Template" button is visible // Check if "Create Template" button is visible
await expect(page.getByText('创建模板')).toBeVisible() await expect(page.getByText('创建模板')).toBeVisible();
await util.captureScreenshot(page, 'template-list-no-templates') await util.captureScreenshot(page, 'template-list-no-templates');
} catch (error) { } catch (error) {
throw await util.onTestError(error) throw await util.onTestError(error);
} }
}) });
// @ts-expect-error: `util` is not using types in playwright // @ts-expect-error: `util` is not using types in playwright
test('Create template page navigation and functionality', async ({ test('Create template page navigation and functionality', async ({
page, page,
util util,
}) => { }) => {
try { try {
// Navigate to templates page // Navigate to templates page
await page.goto('/') await page.goto('/');
await page.waitForURL('**/#/templates') await page.waitForURL('**/#/templates');
// Click create template button // Click create template button
await page.getByText('创建模板').click() await page.getByText('创建模板').click();
await page.waitForURL('**/#/create-template') await page.waitForURL('**/#/create-template');
// Check if we're on the create template page // Check if we're on the create template page
await expect(page.getByText('创建模板')).toBeVisible() await expect(page.getByText('创建模板')).toBeVisible();
// Check if form elements are visible // Check if form elements are visible
await expect(page.getByLabel('模板名称 *')).toBeVisible() await expect(page.getByLabel('模板名称 *')).toBeVisible();
await expect(page.getByLabel('模板描述')).toBeVisible() await expect(page.getByLabel('模板描述')).toBeVisible();
await util.captureScreenshot(page, 'create-template-page') await util.captureScreenshot(page, 'create-template-page');
} catch (error) { } catch (error) {
throw await util.onTestError(error) throw await util.onTestError(error);
} }
}) });
// @ts-expect-error: `util` is not using types in playwright // @ts-expect-error: `util` is not using types in playwright
test('Add files and directories to template', async ({ page, util }) => { test('Add files and directories to template', async ({ page, util }) => {
try { try {
// Navigate to create template page // Navigate to create template page
await page.goto('/#/create-template') await page.goto('/#/create-template');
// Fill in template name and description // Fill in template name and description
await page.getByLabel('模板名称 *').fill('Test Template') await page.getByLabel('模板名称 *').fill('Test Template');
await page.getByLabel('模板描述').fill('This is a test template') await page.getByLabel('模板描述').fill('This is a test template');
// Add a file // Add a file
await page.getByText('添加文件').click() await page.getByText('添加文件').click();
// Wait for dialog to appear and fill in file details // Wait for dialog to appear and fill in file details
await page await page
.locator('[data-testid="add-file-path-input"]') .locator('[data-testid="add-file-path-input"]')
.fill('src/index.js') .fill('src/index.js');
await page await page
.locator('[data-testid="add-file-content-input"]') .locator('[data-testid="add-file-content-input"]')
.fill('console.log("Hello World");') .fill('console.log("Hello World");');
await page.getByText('添加').click() await page.getByText('添加').click();
// Add a directory // Add a directory
await page.getByText('添加目录').click() await page.getByText('添加目录').click();
// Wait for dialog to appear and fill in directory details // Wait for dialog to appear and fill in directory details
await page await page
.locator('[data-testid="add-dir-path-input"]') .locator('[data-testid="add-dir-path-input"]')
.fill('src/components') .fill('src/components');
await page.getByText('添加').click() await page.getByText('添加').click();
// Verify items were added to the tables // Verify items were added to the tables
await expect(page.getByText('src/index.js')).toBeVisible() await expect(page.getByText('src/index.js')).toBeVisible();
await expect(page.getByText('src/components')).toBeVisible() await expect(page.getByText('src/components')).toBeVisible();
await util.captureScreenshot(page, 'template-with-content') await util.captureScreenshot(page, 'template-with-content');
} catch (error) { } catch (error) {
throw await util.onTestError(error) throw await util.onTestError(error);
} }
}) });
// @ts-expect-error: `util` is not using types in playwright // @ts-expect-error: `util` is not using types in playwright
test('Template creation and listing', async ({ page, util }) => { test('Template creation and listing', async ({ page, util }) => {
try { try {
// Navigate to create template page // Navigate to create template page
await page.goto('/#/create-template') await page.goto('/#/create-template');
// Fill in template name and description // Fill in template name and description
await page.getByLabel('模板名称 *').fill('Test Template') await page.getByLabel('模板名称 *').fill('Test Template');
await page.getByLabel('模板描述').fill('This is a test template') await page.getByLabel('模板描述').fill('This is a test template');
// Add a file // Add a file
await page.getByText('添加文件').click() await page.getByText('添加文件').click();
await page await page
.locator('[data-testid="add-file-path-input"]') .locator('[data-testid="add-file-path-input"]')
.fill('src/index.js') .fill('src/index.js');
await page await page
.locator('[data-testid="add-file-content-input"]') .locator('[data-testid="add-file-content-input"]')
.fill('console.log("Hello World");') .fill('console.log("Hello World");');
await page.getByText('添加').click() await page.getByText('添加').click();
// Save template // Save template
await page.getByText('保存模板').click() await page.getByText('保存模板').click();
// Wait for redirect to templates page // Wait for redirect to templates page
await page.waitForURL('**/#/templates') await page.waitForURL('**/#/templates');
// Check if the created template is listed // Check if the created template is listed
await expect(page.getByText('Test Template')).toBeVisible() await expect(page.getByText('Test Template')).toBeVisible();
await expect(page.getByText('This is a test template')).toBeVisible() await expect(page.getByText('This is a test template')).toBeVisible();
await util.captureScreenshot(page, 'template-list-with-template') await util.captureScreenshot(page, 'template-list-with-template');
} catch (error) { } catch (error) {
throw await util.onTestError(error) throw await util.onTestError(error);
} }
}) });
// @ts-expect-error: `util` is not using types in playwright // @ts-expect-error: `util` is not using types in playwright
test('View template details', async ({ page, util }) => { test('View template details', async ({ page, util }) => {
try { try {
// Navigate to templates page // Navigate to templates page
await page.goto('/#/templates') await page.goto('/#/templates');
// Wait for template to appear and click view button // Wait for template to appear and click view button
// First we need to create a template for this test // First we need to create a template for this test
if (await page.getByText('暂无模板,请先创建模板。').isVisible()) { if (await page.getByText('暂无模板,请先创建模板。').isVisible()) {
await page.getByText('创建模板').click() await page.getByText('创建模板').click();
// Create a test template // Create a test template
await page.getByLabel('模板名称 *').fill('View Test Template') await page.getByLabel('模板名称 *').fill('View Test Template');
await page.getByLabel('模板描述').fill('Template for view test') await page.getByLabel('模板描述').fill('Template for view test');
await page.getByText('添加文件').click() await page.getByText('添加文件').click();
await page await page
.locator('[data-testid="add-file-path-input"]') .locator('[data-testid="add-file-path-input"]')
.fill('src/test.js') .fill('src/test.js');
await page await page
.locator('[data-testid="add-file-content-input"]') .locator('[data-testid="add-file-content-input"]')
.fill('console.log("Test");') .fill('console.log("Test");');
await page.getByText('添加').click() await page.getByText('添加').click();
await page.getByText('保存模板').click() await page.getByText('保存模板').click();
await page.waitForURL('**/#/templates') await page.waitForURL('**/#/templates');
} }
// Wait for template to appear and click view button // Wait for template to appear and click view button
await page.getByRole('button', { name: '查看' }).click() await page.getByRole('button', { name: '查看' }).click();
// Check if template detail dialog appears // Check if template detail dialog appears
await expect(page.getByText('模板详情')).toBeVisible() await expect(page.getByText('模板详情')).toBeVisible();
await expect(page.getByText('src/test.js')).toBeVisible() await expect(page.getByText('src/test.js')).toBeVisible();
await util.captureScreenshot(page, 'template-detail-view') await util.captureScreenshot(page, 'template-detail-view');
} catch (error) { } catch (error) {
throw await util.onTestError(error) throw await util.onTestError(error);
} }
}) });
+16 -16
View File
@@ -1,40 +1,40 @@
import { Page } from 'playwright' import type { Page } from 'playwright';
import { TestInfo } from 'playwright/test' import type { TestInfo } from 'playwright/test';
export default class TestUtil { export default class TestUtil {
_page: Page _page: Page;
_testInfo: TestInfo _testInfo: TestInfo;
_testScreenshotPath: string _testScreenshotPath: string;
constructor(page: Page, testInfo: TestInfo, testScreenshotPath: string) { constructor(page: Page, testInfo: TestInfo, testScreenshotPath: string) {
this._page = page this._page = page;
this._testInfo = testInfo this._testInfo = testInfo;
this._testScreenshotPath = testScreenshotPath this._testScreenshotPath = testScreenshotPath;
} }
async captureScreenshot(pageInstance: Page, screenshotName: string) { async captureScreenshot(pageInstance: Page, screenshotName: string) {
if (!pageInstance) { if (!pageInstance) {
return return;
} }
try { try {
const screenshotPath = `${this._testScreenshotPath}/${screenshotName || `unknown_${Date.now()}`}.png` const screenshotPath = `${this._testScreenshotPath}/${screenshotName || `unknown_${Date.now()}`}.png`;
await pageInstance.screenshot({ path: screenshotPath }) await pageInstance.screenshot({ path: screenshotPath });
} catch { } catch {
// Do nothing // Do nothing
} }
} }
async onTestError(error: Error) { async onTestError(error: Error) {
const titleLists = [...this._testInfo.titlePath] const titleLists = [...this._testInfo.titlePath];
titleLists.shift() titleLists.shift();
const title = titleLists.join('-') const title = titleLists.join('-');
await this.captureScreenshot(this._page, `${title}_${Date.now()}`) await this.captureScreenshot(this._page, `${title}_${Date.now()}`);
return new Error(error.message) return new Error(error.message);
} }
} }
+47 -46
View File
@@ -1,30 +1,31 @@
import { fileURLToPath } from 'url' import type { ElectronOptions } from 'vite-plugin-electron';
import { defineConfig, loadEnv } from 'vite' import { rmSync } from 'node:fs';
import ElectronPlugin, { ElectronOptions } from 'vite-plugin-electron' import { builtinModules } from 'node:module';
import RendererPlugin from 'vite-plugin-electron-renderer' import { dirname, resolve } from 'node:path';
import EslintPlugin from 'vite-plugin-eslint' import process from 'node:process';
import VuetifyPlugin from 'vite-plugin-vuetify' import { fileURLToPath } from 'node:url';
import VueJsx from '@vitejs/plugin-vue-jsx' import Vue from '@vitejs/plugin-vue';
import Vue from '@vitejs/plugin-vue' import VueJsx from '@vitejs/plugin-vue-jsx';
import AutoImport from 'unplugin-auto-import/vite' import AutoImport from 'unplugin-auto-import/vite';
import { rmSync } from 'fs' import { defineConfig, loadEnv } from 'vite';
import { resolve, dirname } from 'path' import ElectronPlugin from 'vite-plugin-electron';
import { builtinModules } from 'module' import RendererPlugin from 'vite-plugin-electron-renderer';
import VuetifyPlugin from 'vite-plugin-vuetify';
const isDevEnv = process.env.NODE_ENV === 'development' const isDevEnv = process.env.NODE_ENV === 'development';
export default defineConfig(({ mode }) => { export default defineConfig(({ mode }) => {
process.env = { process.env = {
...(isDevEnv ...(isDevEnv
? { ? {
ELECTRON_ENABLE_LOGGING: 'true' ELECTRON_ENABLE_LOGGING: 'true',
} }
: {}), : {}),
...process.env, ...process.env,
...loadEnv(mode, process.cwd()) ...loadEnv(mode, process.cwd()),
} };
rmSync('dist', { recursive: true, force: true }) rmSync('dist', { recursive: true, force: true });
const electronPluginConfigs: ElectronOptions[] = [ const electronPluginConfigs: ElectronOptions[] = [
{ {
@@ -33,9 +34,9 @@ export default defineConfig(({ mode }) => {
const debugArgs = [ const debugArgs = [
'.', '.',
'--inspect=9228', '--inspect=9228',
'--remote-debugging-port=9229' '--remote-debugging-port=9229',
] ];
startup(debugArgs) startup(debugArgs);
}, },
vite: { vite: {
root: resolve('.'), root: resolve('.'),
@@ -46,24 +47,24 @@ export default defineConfig(({ mode }) => {
assetsDir: '.', assetsDir: '.',
outDir: 'dist/main', outDir: 'dist/main',
rollupOptions: { rollupOptions: {
external: ['electron', ...builtinModules] external: ['electron', ...builtinModules],
} },
} },
} },
}, },
{ {
entry: 'src/preload/index.ts', entry: 'src/preload/index.ts',
onstart({ reload }) { onstart({ reload }) {
reload() reload();
}, },
vite: { vite: {
root: resolve('.'), root: resolve('.'),
build: { build: {
outDir: 'dist/preload' outDir: 'dist/preload',
} },
} },
} },
] ];
if (isDevEnv) { if (isDevEnv) {
electronPluginConfigs.push({ electronPluginConfigs.push({
@@ -71,23 +72,23 @@ export default defineConfig(({ mode }) => {
vite: { vite: {
root: resolve('.'), root: resolve('.'),
build: { build: {
outDir: 'dist/main' outDir: 'dist/main',
} },
} },
}) });
} }
return { return {
define: { define: {
__VUE_I18N_FULL_INSTALL__: true, __VUE_I18N_FULL_INSTALL__: true,
__VUE_I18N_LEGACY_API__: false, __VUE_I18N_LEGACY_API__: false,
__INTLIFY_PROD_DEVTOOLS__: false __INTLIFY_PROD_DEVTOOLS__: false,
}, },
resolve: { resolve: {
extensions: ['.mjs', '.js', '.ts', '.vue', '.json', '.scss'], extensions: ['.mjs', '.js', '.ts', '.vue', '.json', '.scss'],
alias: { alias: {
'@': resolve(dirname(fileURLToPath(import.meta.url)), 'src') '@': resolve(dirname(fileURLToPath(import.meta.url)), 'src'),
} },
}, },
base: './', base: './',
root: resolve('./src/renderer'), root: resolve('./src/renderer'),
@@ -96,7 +97,7 @@ export default defineConfig(({ mode }) => {
build: { build: {
sourcemap: isDevEnv, sourcemap: isDevEnv,
minify: !isDevEnv, minify: !isDevEnv,
outDir: resolve('./dist') outDir: resolve('./dist'),
}, },
plugins: [ plugins: [
Vue(), Vue(),
@@ -108,25 +109,25 @@ export default defineConfig(({ mode }) => {
'vue-router', 'vue-router',
'vue-i18n', 'vue-i18n',
'@vueuse/core', '@vueuse/core',
'pinia' 'pinia',
], ],
dirs: [ dirs: [
'./src/renderer/composables', './src/renderer/composables',
'./src/renderer/utils', './src/renderer/utils',
'./src/renderer/store', './src/renderer/store',
'./src/renderer/components' './src/renderer/components',
], ],
vueTemplate: true vueTemplate: true,
}), }),
// Docs: https://github.com/vuetifyjs/vuetify-loader // Docs: https://github.com/vuetifyjs/vuetify-loader
VuetifyPlugin({ VuetifyPlugin({
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(),
] ],
} };
}) });