Merge branch 'develop'

This commit is contained in:
zhanggy committed 2025-11-05 15:15:02 +08:00
commit ec3f451727
64 files changed
+3856 -3334

No files matched your search

+49 -18
View File
@@ -2,31 +2,62 @@
"workbench.settings.useSplitJSON": true,
"eslint.format.enable": true,
"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",
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"editor.tabSize": 2,
"prettier.embeddedLanguageFormatting": "off",
"prettier.enable": true,
"files.associations": {
"*.mjs": "javascript",
"*.cjs": "javascript",
"*.mts": "typescript"
},
"i18n-ally.localesPaths": [
"src/renderer/locales"
],
"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: ...` - 其他杂项,不触发版本更新
示例:
- `feat: 添加用户认证功能`
- `fix: 修复登录页面的错误`
- `docs: 更新 API 文档`
+31 -31
View File
@@ -1,8 +1,8 @@
/* eslint-disable no-template-curly-in-string */
const dotenv = require('dotenv')
const packageJson = require('../../package.json')
const dotenv = require('dotenv');
const packageJson = require('../../package.json');
dotenv.config()
dotenv.config();
const baseConfig = {
productName: packageJson.name,
@@ -16,12 +16,12 @@ const baseConfig = {
owner: 'Jasery',
repo: 'gdt-tool',
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}',
directories: {
output: './release/${version}'
output: './release/${version}',
},
mac: {
bundleVersion: '1.0',
@@ -33,9 +33,9 @@ const baseConfig = {
target: [
{
target: 'dmg',
arch: ['x64', 'arm64', 'universal']
}
]
arch: ['x64', 'arm64', 'universal'],
},
],
},
dmg: {
contents: [
@@ -43,15 +43,15 @@ const baseConfig = {
x: 410,
y: 150,
type: 'link',
path: '/Applications'
path: '/Applications',
},
{
x: 130,
y: 150,
type: 'file'
}
type: 'file',
},
],
sign: false
sign: false,
},
win: {
icon: 'buildAssets/icons/icon.ico',
@@ -70,15 +70,15 @@ const baseConfig = {
// },
{
target: 'nsis',
arch: 'x64'
}
]
arch: 'x64',
},
],
},
portable: {
artifactName: '${productName} ${version}_${arch} Portable.${ext}'
artifactName: '${productName} ${version}_${arch} Portable.${ext}',
},
nsis: {
oneClick: true
oneClick: true,
},
linux: {
executableName: packageJson.name.toLowerCase(),
@@ -87,21 +87,21 @@ const baseConfig = {
target: [
{
target: 'snap',
arch: 'x64'
arch: 'x64',
},
{
target: 'deb',
arch: 'x64'
arch: 'x64',
},
{
target: 'rpm',
arch: 'x64'
}
]
}
}
arch: 'x64',
},
],
},
};
baseConfig.copyright = `ⓒ ${new Date().getFullYear()} $\{author}`
baseConfig.copyright = `ⓒ ${new Date().getFullYear()} $\{author}`;
baseConfig.files = [
/* A list of files not to be included in the build. */
/*
@@ -111,11 +111,11 @@ baseConfig.files = [
'!dist/main/index.dev.js',
'!docs/**/*',
'!tests/**/*',
'!release/**/*'
]
'!release/**/*',
];
// TODO: Notarize for macOS
baseConfig.mac.identity = null
baseConfig.mac.identity = null;
/* if (process.env.MAC_NOTARIZE === 'true') {
baseConfig.afterSign = './buildAssets/builder/notarize.ts'
} else {
@@ -123,5 +123,5 @@ baseConfig.mac.identity = null
} */
module.exports = {
...baseConfig
}
...baseConfig,
};
+1 -1
View File
@@ -40,7 +40,7 @@ publish: [
releaseType: 'release',
token: process.env.GH_TOKEN || '' // 从环境变量获取
}
]
];
```
## 发布更新
+38 -37
View File
@@ -1,14 +1,15 @@
import { withSidebar } from 'vitepress-sidebar'
import { name, repository, homepage } from '../../../package.json'
import { defineConfig, UserConfig } from 'vitepress'
import { withI18n } from 'vitepress-i18n'
import type { VitePressSidebarOptions } from 'vitepress-sidebar/types'
import type { VitePressI18nOptions } from 'vitepress-i18n/types'
import type { UserConfig } from 'vitepress';
import type { VitePressI18nOptions } from 'vitepress-i18n/types';
import type { VitePressSidebarOptions } from 'vitepress-sidebar/types';
import { defineConfig } from 'vitepress';
import { withI18n } from 'vitepress-i18n';
import { withSidebar } from 'vitepress-sidebar';
import { homepage, name, repository } from '../../../package.json';
const capitalizeFirst = (str: string): string =>
str.charAt(0).toUpperCase() + str.slice(1)
const supportLocales = ['en', 'ko', 'zhHans']
const defaultLocale: string = supportLocales[0]
str.charAt(0).toUpperCase() + str.slice(1);
const supportLocales = ['en', 'ko', 'zhHans'];
const defaultLocale: string = supportLocales[0];
const vitePressI18nConfigs: VitePressI18nOptions = {
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.',
ko: 'Vutron은 Electron 크로스 플랫폼 데스크톱 앱 개발을 위해 미리 구성된 템플릿입니다. Vue 3을 사용하며 적은 노력으로 빠른 개발 환경을 구축할 수 있습니다.',
zhHans:
'Vutron 是用于开发 Electron 跨平台桌面应用程序的预配置模板。它使用 Vue 3,可让您轻松构建快速开发环境。'
'Vutron 是用于开发 Electron 跨平台桌面应用程序的预配置模板。它使用 Vue 3,可让您轻松构建快速开发环境。',
},
themeConfig: {
en: {
nav: [
{
text: 'Getting Started',
link: '/installation-and-build/getting-started'
}
]
link: '/installation-and-build/getting-started',
},
],
},
ko: {
nav: [
{
text: '시작하기',
link: '/ko/installation-and-build/getting-started'
}
]
link: '/ko/installation-and-build/getting-started',
},
],
},
zhHans: {
nav: [
{
text: '入门',
link: '/zhHans/installation-and-build/getting-started'
}
]
}
}
}
link: '/zhHans/installation-and-build/getting-started',
},
],
},
},
};
const vitePressSidebarConfigs: VitePressSidebarOptions = [
...supportLocales.map((lang) => {
@@ -62,14 +63,14 @@ const vitePressSidebarConfigs: VitePressSidebarOptions = [
'introduction.md',
'installation-and-build',
'project-structures',
'electron-how-to'
'electron-how-to',
],
documentRootPath: `/src/${lang}`,
resolvePath: defaultLocale === lang ? '/' : `/${lang}/`,
...(defaultLocale === lang ? {} : { basePath: `/${lang}/` })
}
})
]
...(defaultLocale === lang ? {} : { basePath: `/${lang}/` }),
};
}),
];
const vitePressConfigs: UserConfig = {
title: capitalizeFirst(name),
@@ -77,28 +78,28 @@ const vitePressConfigs: UserConfig = {
outDir: '../dist',
head: [
['link', { rel: 'icon', href: '/logo.png' }],
['link', { rel: 'shortcut icon', href: '/favicon.ico' }]
['link', { rel: 'shortcut icon', href: '/favicon.ico' }],
],
cleanUrls: true,
metaChunk: true,
rewrites: {
'en/:rest*': ':rest*'
'en/:rest*': ':rest*',
},
sitemap: {
hostname: homepage
hostname: homepage,
},
themeConfig: {
logo: { src: '/icon.png', width: 24, height: 24 },
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(
withSidebar(
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
rendererPlugin({
nodeIntegration: true
})
});
```
For more information on this, see the following articles: https://github.com/electron-vite/vite-plugin-electron-renderer
@@ -10,7 +10,7 @@ order: 3
## General
| Script Name | Description |
| --- | --- |
| ----------------- | ------------------------------------------------------------------------------------------------------- |
| `dev` | Start Electron as a development environment |
| `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) |
@@ -33,7 +33,7 @@ order: 3
Used only for contributing to project documentation. Must be run from the `docs` directory location.
| Script Name | Description |
| --- | --- |
| ----------- | ------------------------------------------------------------------ |
| `dev` | Start the local document server. (For development) |
| `build` | Build a local document server. Used only for GitHub page builders. |
| `serve` | Start the local document server. |
@@ -19,7 +19,7 @@
```javascript
rendererPlugin({
nodeIntegration: true
})
});
```
이에 대한 자세한 내용은 다음 문서를 참조하세요: https://github.com/electron-vite/vite-plugin-electron-renderer
@@ -10,7 +10,7 @@ order: 3
## 일반
| Script Name | Description |
| --- | --- |
| ----------------- | ------------------------------------------------------------------------------------------------------- |
| `dev` | Start Electron as a development environment |
| `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) |
@@ -33,7 +33,7 @@ order: 3
프로젝트 문서에 기여하는 경우에만 사용됩니다. `docs` 디렉토리 위치에서 실행해야 합니다.
| Script Name | Description |
| --- | --- |
| ----------- | ------------------------------------------------------------------ |
| `dev` | Start the local document server. (For development) |
| `build` | Build a local document server. Used only for GitHub page builders. |
| `serve` | Start the local document server. |
@@ -19,7 +19,7 @@
```javascript
rendererPlugin({
nodeIntegration: true
})
});
```
欲了解更多信息,请参阅以下文章: https://github.com/electron-vite/vite-plugin-electron-renderer
@@ -10,7 +10,7 @@ order: 3
## 一般情况
| 脚本名称 | 说明 |
| --- | --- |
| ----------------- | ---------------------------------------------------------- |
| `dev` | 启动电子作为开发环境 |
| `dev:debug` | 将 Electron 作为开发环境启动(使用 vite debug) |
| `dev:debug:force` | 以Electron作为开发环境启动(使用vite调试+清理vite缓存) |
+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: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",
"lint": "eslint . --ext .js,.ts,.vue .",
"lint:fix": "eslint . --ext .js,.ts,.vue --fix .",
"lint": "eslint",
"lint:fix": "eslint --fix",
"test": "npm run build:pre && 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",
@@ -75,6 +75,7 @@
}
},
"dependencies": {
"@antfu/eslint-config": "^6.2.0",
"@mdi/font": "^7.4.47",
"@vueuse/core": "^14.0.0",
"electron-log": "^5.4.3",
@@ -87,11 +88,9 @@
"vue-router": "^4.6.3"
},
"devDependencies": {
"@eslint/js": "^9.37.0",
"@mdi/js": "^7.4.47",
"@playwright/test": "^1.56.1",
"@types/fs-extra": "^11.0.4",
"@typescript-eslint/parser": "^8.46.1",
"@vitejs/plugin-vue": "^6.0.1",
"@vitejs/plugin-vue-jsx": "^5.1.1",
"dotenv": "^17.2.3",
@@ -100,11 +99,7 @@
"electron-devtools-installer": "^4.0.0",
"electron-extension-installer": "^2.0.0",
"eslint": "^9.37.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-n": "^17.23.1",
"eslint-plugin-prettier": "^5.5.4",
"eslint-plugin-vue": "^10.5.1",
"eslint-plugin-format": "^1.0.2",
"globals": "^16.4.0",
"jiti": "^2.6.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({
outputDir: 'tests/results',
@@ -6,6 +6,6 @@ export default defineConfig({
workers: process.env.CI ? 1 : undefined,
timeout: 60000,
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 { autoUpdater } from 'electron-updater'
import log from 'electron-log/main'
import { dialog } from 'electron';
import log from 'electron-log/main';
import { autoUpdater } from 'electron-updater';
// 配置日志
autoUpdater.logger = log
log.transports.file.level = 'info'
autoUpdater.logger = log;
log.transports.file.level = 'info';
export class AutoUpdater {
private mainWindow: Electron.BrowserWindow | null = null
private mainWindow: Electron.BrowserWindow | null = null;
constructor(mainWindow: Electron.BrowserWindow) {
this.mainWindow = mainWindow
this.mainWindow = mainWindow;
// 配置更新检查间隔
autoUpdater.autoDownload = false
autoUpdater.allowPrerelease = false
autoUpdater.channel = 'latest'
autoUpdater.autoDownload = false;
autoUpdater.allowPrerelease = false;
autoUpdater.channel = 'latest';
// 如果是私有仓库,设置认证信息
if (process.env.GH_TOKEN || process.env.GITHUB_TOKEN) {
// 使用环境变量中的令牌
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() {
// 检查更新
autoUpdater.on('checking-for-update', () => {
log.info('正在检查更新...')
this.mainWindow?.webContents.send('update-checking')
})
log.info('正在检查更新...');
this.mainWindow?.webContents.send('update-checking');
});
// 发现新版本
autoUpdater.on('update-available', (info) => {
log.info('发现新版本:', info.version)
this.mainWindow?.webContents.send('update-available', info)
})
log.info('发现新版本:', info.version);
this.mainWindow?.webContents.send('update-available', info);
});
// 没有新版本
autoUpdater.on('update-not-available', (info) => {
log.info('当前已是最新版本')
this.mainWindow?.webContents.send('update-not-available', info)
})
log.info('当前已是最新版本');
this.mainWindow?.webContents.send('update-not-available', info);
});
// 更新下载进度
autoUpdater.on('download-progress', (progress) => {
log.info('下载进度:', progress.percent, '%')
this.mainWindow?.webContents.send('update-download-progress', progress)
})
log.info('下载进度:', progress.percent, '%');
this.mainWindow?.webContents.send('update-download-progress', progress);
});
// 更新下载完成
autoUpdater.on('update-downloaded', (info) => {
log.info('更新下载完成')
this.mainWindow?.webContents.send('update-downloaded', info)
log.info('更新下载完成');
this.mainWindow?.webContents.send('update-downloaded', info);
// 提示用户是否重启应用安装更新
dialog.showMessageBox({
@@ -67,31 +67,31 @@ export class AutoUpdater {
detail: '是否现在重启应用以安装更新?',
buttons: ['稍后重启', '立即重启'],
defaultId: 1,
cancelId: 0
cancelId: 0,
}).then((result) => {
if (result.response === 1) {
// 安装更新并重启应用
setImmediate(() => {
autoUpdater.quitAndInstall()
})
autoUpdater.quitAndInstall();
});
}
})
})
});
});
// 更新错误
autoUpdater.on('error', (error) => {
log.error('自动更新出错:', error)
this.mainWindow?.webContents.send('update-error', error)
})
log.error('自动更新出错:', error);
this.mainWindow?.webContents.send('update-error', error);
});
}
public checkForUpdates() {
log.info('开始检查更新...')
autoUpdater.checkForUpdates()
log.info('开始检查更新...');
autoUpdater.checkForUpdates();
}
public downloadUpdate() {
log.info('开始下载更新...')
autoUpdater.downloadUpdate()
log.info('开始下载更新...');
autoUpdater.downloadUpdate();
}
}
+128 -128
View File
@@ -1,17 +1,18 @@
import { ipcMain, shell, IpcMainEvent, dialog } from 'electron'
import Constants from './utils/Constants'
import { join, dirname } from 'path'
import type { IpcMainEvent } from 'electron';
import type { Template, TreeNode } from '../types';
import { dirname, join } from 'node:path';
import { dialog, ipcMain, shell } from 'electron';
import { autoUpdater } from 'electron-updater';
import {
writeFile,
readFile,
pathExists,
ensureDir,
copy,
ensureDir,
pathExists,
readdir,
readFile,
stat,
} from 'fs-extra'
import { autoUpdater } from 'electron-updater'
import { Template, TreeNode } from '../types'
writeFile,
} from 'fs-extra';
import Constants from './utils/Constants';
/*
* IPC Communications
@@ -20,154 +21,151 @@ export default class IPCs {
static initialize(): void {
// Get application version
ipcMain.handle('msgRequestGetVersion', () => {
return Constants.APP_VERSION
})
return Constants.APP_VERSION;
});
// Open url via web browser
ipcMain.on(
'msgOpenExternalLink',
async (event: IpcMainEvent, url: string) => {
await shell.openExternal(url)
}
)
await shell.openExternal(url);
},
);
// Open file
ipcMain.handle(
'msgOpenFile',
async (event: IpcMainEvent, filter: string) => {
const filters = []
const filters = [];
if (filter === 'text') {
filters.push({ name: 'Text', extensions: ['txt', 'json'] })
filters.push({ name: 'Text', extensions: ['txt', 'json'] });
} else if (filter === 'zip') {
filters.push({ name: 'Zip', extensions: ['zip'] })
filters.push({ name: 'Zip', extensions: ['zip'] });
} else if (filter === 'any') {
filters.push({ name: 'All Files', extensions: ['*'] })
filters.push({ name: 'All Files', extensions: ['*'] });
}
const dialogResult = await dialog.showOpenDialog({
properties: ['openFile'],
filters
})
return dialogResult
}
)
filters,
});
return dialogResult;
},
);
// Select directory
ipcMain.handle('msgSelectDirectory', async () => {
const dialogResult = await dialog.showOpenDialog({
properties: ['openDirectory']
})
properties: ['openDirectory'],
});
if (!dialogResult.canceled && dialogResult.filePaths.length > 0) {
return {
canceled: false,
filePath: dialogResult.filePaths[0],
isDirectory: true
}
isDirectory: true,
};
}
return { canceled: true }
})
return { canceled: true };
});
// Select file or directory
ipcMain.handle('msgSelectFileOrFolder', async () => {
const dialogResult = await dialog.showOpenDialog({
properties: ['openFile', 'openDirectory']
})
properties: ['openFile', 'openDirectory'],
});
if (!dialogResult.canceled && dialogResult.filePaths.length > 0) {
const filePath = dialogResult.filePaths[0];
const fs = require('fs');
const fs = require('node:fs');
const isDirectory = fs.statSync(filePath).isDirectory();
return {
canceled: false,
filePath: filePath,
isDirectory: isDirectory
}
filePath,
isDirectory,
};
}
return { canceled: true }
})
return { canceled: true };
});
// Find all files in a folder recursively
ipcMain.handle(
'msgFindFilesInFolder',
async (event: IpcMainEvent, folderPath: string) => {
try {
const foundFiles: string[] = []
const foundFiles: string[] = [];
const searchFiles = async (dir: string) => {
const items = await readdir(dir)
const items = await readdir(dir);
for (const item of items) {
const fullPath = join(dir, item)
const stats = await stat(fullPath)
const fullPath = join(dir, item);
const stats = await stat(fullPath);
if (stats.isDirectory()) {
// Recursively search in subdirectories
await searchFiles(fullPath)
await searchFiles(fullPath);
} else if (stats.isFile()) {
// Add file to the list
foundFiles.push(fullPath)
}
foundFiles.push(fullPath);
}
}
};
await searchFiles(folderPath)
await searchFiles(folderPath);
return foundFiles
return foundFiles;
} catch (error) {
console.error('Error finding files in folder:', error)
return []
console.error('Error finding files in folder:', error);
return [];
}
}
)
},
);
// Load data from local file
ipcMain.handle(
'msgLoadData',
async (event: IpcMainEvent, fileName: string) => {
try {
const userDataPath = join(Constants.APP_DATA_PATH, 'data')
const filePath = join(userDataPath, fileName)
const userDataPath = join(Constants.APP_DATA_PATH, 'data');
const filePath = join(userDataPath, fileName);
// Ensure directory exists
await ensureDir(userDataPath)
await ensureDir(userDataPath);
if (await pathExists(filePath)) {
const data = await readFile(filePath, 'utf-8')
return data
const data = await readFile(filePath, 'utf-8');
return data;
}
return null
return null;
} catch (error) {
console.error('Error loading data:', error)
return null
console.error('Error loading data:', error);
return null;
}
}
)
},
);
// Save data to local file
ipcMain.handle(
'msgSaveData',
async (event: IpcMainEvent, fileName: string, data: string) => {
try {
const userDataPath = join(Constants.APP_DATA_PATH, 'data')
const filePath = join(userDataPath, fileName)
const userDataPath = join(Constants.APP_DATA_PATH, 'data');
const filePath = join(userDataPath, fileName);
// Ensure directory exists
await ensureDir(userDataPath)
await ensureDir(userDataPath);
await writeFile(filePath, data, 'utf-8')
return true
await writeFile(filePath, data, 'utf-8');
return true;
} catch (error) {
console.error('Error saving data:', error)
return false
console.error('Error saving data:', error);
return false;
}
}
)
},
);
// Execute template
ipcMain.handle(
@@ -176,39 +174,41 @@ export default class IPCs {
event: IpcMainEvent,
templateId: string,
targetPath: string,
baseDirNames: string[]
baseDirNames: string[],
) => {
try {
// Load templates data
const userDataPath = join(Constants.APP_DATA_PATH, 'data')
const filePath = join(userDataPath, 'templates.json')
const userDataPath = join(Constants.APP_DATA_PATH, 'data');
const filePath = join(userDataPath, 'templates.json');
let templates: Template[] = []
let templates: Template[] = [];
if (await pathExists(filePath)) {
const templatesData = await readFile(filePath, 'utf-8')
templates = JSON.parse(templatesData)
const templatesData = await readFile(filePath, 'utf-8');
templates = JSON.parse(templatesData);
}
const template = templates.find((t) => t.id === templateId)
const template = templates.find(t => t.id === templateId);
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
for (const baseDirName of baseDirNames) {
if (!baseDirName.trim()) continue
if (!baseDirName.trim()) {
continue;
}
// Create base directory
const baseDirPath = join(targetPath, baseDirName)
await ensureDir(baseDirPath)
const baseDirPath = join(targetPath, baseDirName);
await ensureDir(baseDirPath);
// Helper function to traverse the tree and collect all nodes
const collectAllNodes = (nodes: TreeNode[], allNodes: TreeNode[] = [], parent?: TreeNode) => {
for (const node of nodes) {
allNodes.push({
...node,
path: join(parent?.path || '', node.path)
path: join(parent?.path || '', node.path),
});
if (node.children) {
collectAllNodes(node.children, allNodes, node);
@@ -235,7 +235,7 @@ export default class IPCs {
await copy(node.sourcePath, nodePath);
} else {
// Write content directly
const files = await readdir(node.sourcePath, { withFileTypes: true })
const files = await readdir(node.sourcePath, { withFileTypes: true });
for (const file of files) {
if (file.isFile() && file.name.startsWith(baseDirName)) {
const filePath = join(node.sourcePath, file.name);
@@ -248,91 +248,91 @@ export default class IPCs {
}
}
return { success: true }
return { success: true };
} catch (error) {
console.error('Error executing template:', error)
return { success: false, error: error.message }
console.error('Error executing template:', error);
return { success: false, error: error.message };
}
}
)
},
);
// 检查更新
ipcMain.handle('msgCheckForUpdates', async () => {
if (Constants.IS_DEV_ENV) {
// 在开发环境中模拟更新检查
console.log('开发环境中模拟检查更新...')
return { updateAvailable: false, currentVersion: Constants.APP_VERSION }
console.log('开发环境中模拟检查更新...');
return { updateAvailable: false, currentVersion: Constants.APP_VERSION };
}
return new Promise((resolve) => {
// 监听更新事件
const removeListeners = () => {
autoUpdater.removeListener('update-available', onUpdateAvailable)
autoUpdater.removeListener('update-not-available', onUpdateNotAvailable)
autoUpdater.removeListener('error', onError)
}
autoUpdater.removeListener('update-available', onUpdateAvailable);
autoUpdater.removeListener('update-not-available', onUpdateNotAvailable);
autoUpdater.removeListener('error', onError);
};
const onUpdateAvailable = () => {
removeListeners()
resolve({ updateAvailable: true, currentVersion: Constants.APP_VERSION })
}
removeListeners();
resolve({ updateAvailable: true, currentVersion: Constants.APP_VERSION });
};
const onUpdateNotAvailable = () => {
removeListeners()
resolve({ updateAvailable: false, currentVersion: Constants.APP_VERSION })
}
removeListeners();
resolve({ updateAvailable: false, currentVersion: Constants.APP_VERSION });
};
const onError = (error: Error) => {
removeListeners()
console.error('检查更新时出错:', error)
resolve({ updateAvailable: false, currentVersion: Constants.APP_VERSION, error: error.message })
}
removeListeners();
console.error('检查更新时出错:', error);
resolve({ updateAvailable: false, currentVersion: Constants.APP_VERSION, error: error.message });
};
// 添加事件监听器
autoUpdater.once('update-available', onUpdateAvailable)
autoUpdater.once('update-not-available', onUpdateNotAvailable)
autoUpdater.once('error', onError)
autoUpdater.once('update-available', onUpdateAvailable);
autoUpdater.once('update-not-available', onUpdateNotAvailable);
autoUpdater.once('error', onError);
// 开始检查更新
autoUpdater.checkForUpdates()
})
})
autoUpdater.checkForUpdates();
});
});
// 下载更新
ipcMain.handle('msgDownloadUpdate', async () => {
if (Constants.IS_DEV_ENV) {
// 在开发环境中模拟下载更新
console.log('开发环境中模拟下载更新...')
return { success: true }
console.log('开发环境中模拟下载更新...');
return { success: true };
}
try {
await autoUpdater.downloadUpdate()
return { success: true }
await autoUpdater.downloadUpdate();
return { success: true };
} catch (error) {
console.error('下载更新时出错:', error)
return { success: false, error: error instanceof Error ? error.message : String(error) }
console.error('下载更新时出错:', error);
return { success: false, error: error instanceof Error ? error.message : String(error) };
}
})
});
// 安装更新
ipcMain.handle('msgInstallUpdate', async () => {
if (Constants.IS_DEV_ENV) {
// 在开发环境中模拟安装更新
console.log('开发环境中模拟安装更新...')
return { success: true }
console.log('开发环境中模拟安装更新...');
return { success: true };
}
try {
// 立即退出并安装更新
setImmediate(() => {
autoUpdater.quitAndInstall()
})
return { success: true }
autoUpdater.quitAndInstall();
});
return { success: true };
} catch (error) {
console.error('安装更新时出错:', error)
return { success: false, error: error instanceof Error ? error.message : String(error) }
console.error('安装更新时出错:', 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 {
app,
BrowserWindow,
RenderProcessGoneDetails,
BrowserWindowConstructorOptions
} from 'electron'
import Constants, { TrayOptions } from './utils/Constants'
import IPCs from './IPCs'
import { createTray, hideWindow, showWindow } from './tray'
import log from 'electron-log/main'
} from 'electron';
import log from 'electron-log/main';
import IPCs from './IPCs';
import { createTray, hideWindow, showWindow } from './tray';
import Constants from './utils/Constants';
const options = {
width: Constants.IS_DEV_ENV ? 1500 : 1200,
@@ -16,20 +19,20 @@ const options = {
// all optional values from DEFAULT_TRAY_OPTIONS can de defined here
enabled: true,
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 => {
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.hide()
mainWindow.hide();
}
mainWindow.destroy()
app.exit()
}
mainWindow.destroy();
app.exit();
};
export const createMainWindow = async (): Promise<BrowserWindow> => {
log.silly('Creating new window...')
log.silly('Creating new window...');
let opt: BrowserWindowConstructorOptions = {
title: Constants.APP_NAME,
@@ -38,17 +41,17 @@ export const createMainWindow = async (): Promise<BrowserWindow> => {
height: options.height,
useContentSize: true,
webPreferences: Constants.DEFAULT_WEB_PREFERENCES,
frame: true
}
frame: true,
};
const trayOptions: TrayOptions = options.tray?.enabled
? {
...Constants.DEFAULT_TRAY_OPTIONS,
...options.tray
...options.tray,
}
: {
...Constants.DEFAULT_TRAY_OPTIONS,
enabled: false
}
enabled: false,
};
// trayWindow requires tray.enabled=true
if (trayOptions.enabled && trayOptions.trayWindow) {
@@ -67,93 +70,92 @@ export const createMainWindow = async (): Promise<BrowserWindow> => {
alwaysOnTop: true,
webPreferences: {
...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 => {
event.preventDefault()
exitApp(mainWindow)
})
event.preventDefault();
exitApp(mainWindow);
});
mainWindow.webContents.on('did-frame-finish-load', (): void => {
if (Constants.IS_DEV_ENV && Constants.FEAT_OPEN_DEV_TOOLS_AT_START) {
mainWindow.webContents.openDevTools()
mainWindow.webContents.openDevTools();
}
})
});
if (trayOptions.enabled) {
createTray(mainWindow, trayOptions)
createTray(mainWindow, trayOptions);
}
if (trayOptions.enabled && trayOptions.trayWindow) {
hideWindow(mainWindow)
hideWindow(mainWindow);
if (trayOptions.showAtStartup) {
showWindow(mainWindow)
showWindow(mainWindow);
}
} else {
mainWindow.once('ready-to-show', (): void => {
mainWindow.setAlwaysOnTop(true)
mainWindow.show()
mainWindow.focus()
mainWindow.setAlwaysOnTop(false)
})
mainWindow.setAlwaysOnTop(true);
mainWindow.show();
mainWindow.focus();
mainWindow.setAlwaysOnTop(false);
});
}
// Initialize IPC Communication
IPCs.initialize()
IPCs.initialize();
if (Constants.IS_DEV_ENV) {
await mainWindow.loadURL(Constants.APP_INDEX_URL_DEV)
await mainWindow.loadURL(Constants.APP_INDEX_URL_DEV);
} 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 (
errorWindow: BrowserWindow,
mainWindow: BrowserWindow,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
details?: RenderProcessGoneDetails
_details?: RenderProcessGoneDetails,
): Promise<BrowserWindow> => {
if (!Constants.IS_DEV_ENV) {
mainWindow?.hide()
mainWindow?.hide();
}
errorWindow = new BrowserWindow({
title: Constants.APP_NAME,
show: false,
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) {
await errorWindow.loadURL(`${Constants.APP_INDEX_URL_DEV}#/error`)
await errorWindow.loadURL(`${Constants.APP_INDEX_URL_DEV}#/error`);
} 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 => {
if (!Constants.IS_DEV_ENV && mainWindow && !mainWindow.isDestroyed()) {
mainWindow.destroy()
mainWindow.destroy();
}
errorWindow.show()
errorWindow.focus()
})
errorWindow.show();
errorWindow.focus();
});
errorWindow.webContents.on('did-frame-finish-load', (): void => {
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
// and is removed at build time.
// Do not edit the file unless necessary.
import { installExtension } from 'electron-extension-installer'
import { app } from 'electron'
import { installExtension } from 'electron-extension-installer';
const vue3DevTools = {
id: 'nhdogjmejiglipccpnnnanhbledajbpd',
version: '7.7.0'
}
version: '7.7.0',
};
try {
app.on('ready', async () => {
await installExtension(vue3DevTools, {
loadExtensionOptions: {
allowFileAccess: true
}
})
})
allowFileAccess: true,
},
});
});
} catch {
// Do nothing
}
+39 -39
View File
@@ -1,46 +1,46 @@
import { app, WebContents, RenderProcessGoneDetails } from 'electron'
import Constants from './utils/Constants'
import { createErrorWindow, createMainWindow } from './MainRunner'
import log from 'electron-log/main'
import { join } from 'path'
import { AutoUpdater } from './AutoUpdater'
import type { RenderProcessGoneDetails, WebContents } from 'electron';
import { join } from 'node:path';
import { app } from 'electron';
import log from 'electron-log/main';
import { AutoUpdater } from './AutoUpdater';
import { createErrorWindow, createMainWindow } from './MainRunner';
import Constants from './utils/Constants';
let mainWindow
let errorWindow
let autoUpdater: AutoUpdater | null = null
let mainWindow;
let errorWindow;
const initializeMainLogger = () => {
log.initialize({
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 = () =>
join(app.getPath('userData'), 'logs', 'applog.log')
log.transports.file.level = 'silly'
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.level = 'silly'
join(app.getPath('userData'), 'logs', 'applog.log');
log.transports.file.level = 'silly';
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.level = 'silly';
log.silly(`Start logging... (Path: ${appLogFilePath}) App is ready.`)
}
log.silly(`Start logging... (Path: ${appLogFilePath}) App is ready.`);
};
app.on('ready', async () => {
if (Constants.IS_DEV_ENV) {
import('./index.dev')
import('./index.dev');
// Install Vue DevTools
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, {
loadExtensionOptions: {
allowFileAccess: true,
},
})
});
} 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)
}
*/
initializeMainLogger()
initializeMainLogger();
mainWindow = await createMainWindow()
mainWindow = await createMainWindow();
// Initialize auto updater after main window is created
if (!Constants.IS_DEV_ENV) {
autoUpdater = new AutoUpdater(mainWindow)
autoUpdater = new AutoUpdater(mainWindow);
}
})
});
app.on('activate', async () => {
if (!mainWindow) {
mainWindow = await createMainWindow()
mainWindow = await createMainWindow();
}
})
});
app.on('window-all-closed', () => {
mainWindow = null
errorWindow = null
mainWindow = null;
errorWindow = null;
if (!Constants.IS_MAC) {
app.quit()
app.quit();
}
})
});
app.on(
'render-process-gone',
(
event: Event,
webContents: WebContents,
details: RenderProcessGoneDetails
details: RenderProcessGoneDetails,
) => {
errorWindow = createErrorWindow(errorWindow, mainWindow, details)
}
)
errorWindow = createErrorWindow(errorWindow, mainWindow, details);
},
);
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 Constants from './utils/Constants'
import { join } from 'path'
import { debounce } from 'qsu'
let tray
let trayOptions
import type { BrowserWindow } from 'electron';
import { join } from 'node:path';
import { app, Menu, screen, Tray } from 'electron';
import { debounce } from 'qsu';
import Constants from './utils/Constants';
let tray;
let trayOptions;
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
if (trayOptions.trayWindow) {
trayOptions.menu = false
trayOptions.menu = false;
}
tray = new Tray(join(Constants.PUBLIC_PATH, 'images/vutron-tray-icon.png'))
tray.setToolTip(trayOptions.tooltip)
tray = new Tray(join(Constants.PUBLIC_PATH, 'images/vutron-tray-icon.png'));
tray.setToolTip(trayOptions.tooltip);
if (trayOptions.menu) {
const contextMenu = Menu.buildFromTemplate([
{
label: 'Show App',
click: () => {
showWindow(window)
}
showWindow(window);
},
},
{
label: 'Hide App',
click: () => {
hideWindow(window)
}
hideWindow(window);
},
},
{
label: 'Exit',
click: () => {
app.quit()
}
}
])
app.quit();
},
},
]);
// tray icon only with classic window
tray.setContextMenu(contextMenu)
tray.setContextMenu(contextMenu);
} else {
// handle click on tray icon
tray.on('right-click', function () {
debounce(() => toggleWindow(window), 200)
})
tray.on('click', function () {
debounce(() => toggleWindow(window), 200)
})
tray.on('right-click', () => {
debounce(() => toggleWindow(window), 200);
});
tray.on('click', () => {
debounce(() => toggleWindow(window), 200);
});
// no menu for tray window
window.setMenu(null)
tray.setContextMenu(null)
window.setMenu(null);
tray.setContextMenu(null);
}
// align at startup
alignWindow(window)
return tray
alignWindow(window);
return tray;
}
export function hideWindow(window: BrowserWindow) {
window.hide()
window.hide();
// if (!trayOptions.trayWindow) return;
// hide window when click elsewhere on screen
window.on('blur', () => {
// dont close if devtools
if (!window.webContents.isDevToolsOpened()) {
window.hide()
window.hide();
}
})
});
}
export function toggleWindow(window: BrowserWindow) {
if (window.isVisible()) {
hideWindow(window)
hideWindow(window);
} else {
showWindow(window)
showWindow(window);
}
}
export function showWindow(window: BrowserWindow) {
window.show()
alignWindow(window)
window.show();
alignWindow(window);
}
export function alignWindow(window: BrowserWindow) {
if (!trayOptions.trayWindow) return
if (!trayOptions.trayWindow) {
return;
}
const b = window.getBounds()
const position = calculateWindowPosition(b)
const b = window.getBounds();
const position = calculateWindowPosition(b);
window.setBounds({
width: b.width,
height: b.height,
x: position.x,
y: position.y
})
y: position.y,
});
}
function calculateWindowPosition(b) {
const margin = trayOptions.margin
const screenBounds = screen.getPrimaryDisplay().size
const trayBounds = tray.getBounds()
const bottom = trayBounds.y > screenBounds.height / 2
const margin = trayOptions.margin;
const screenBounds = screen.getPrimaryDisplay().size;
const trayBounds = tray.getBounds();
const bottom = trayBounds.y > screenBounds.height / 2;
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
? 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
return {
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 { name, version, debug } from '../../../package.json'
import { fileURLToPath } from 'url'
import { app } from 'electron'
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
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 {
enabled: boolean
trayWindow: boolean
menu: boolean
tooltip: string
margin: { x: number; y: number }
showAtStartup: boolean
enabled: boolean;
trayWindow: boolean;
menu: boolean;
tooltip: string;
margin: { x: number; y: number };
showAtStartup: boolean;
}
export default class Constants {
@@ -23,32 +23,32 @@ export default class Constants {
* ------------------------------------------------------ */
// 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.
static FEAT_OPEN_DEV_TOOLS_AT_START = true
static FEAT_OPEN_DEV_TOOLS_AT_START = true;
/* ------------------------------------------------------
* END OF FEATURES
* ------------------------------------------------------ */
// 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
? resolve(__dirname, '../../src/public')
: resolve(__dirname, '..')
: resolve(__dirname, '..');
static IS_MAC = process.platform === 'darwin'
static IS_MAC = process.platform === 'darwin';
static DEFAULT_WEB_PREFERENCES = {
nodeIntegration: false,
contextIsolation: true,
enableRemoteModule: false,
preload: join(__dirname, '../preload/index.js')
}
preload: join(__dirname, '../preload/index.js'),
};
static DEFAULT_TRAY_OPTIONS: TrayOptions = {
enabled: false,
@@ -56,9 +56,9 @@ export default class Constants {
menu: false,
tooltip: 'Vutron App',
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_PROD = join(__dirname, '../index.html')
static APP_INDEX_URL_DEV = `${debug.env.VITE_DEV_SERVER_URL}/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 { contextBridge, ipcRenderer, IpcRendererEvent } from 'electron'
import type { IpcRendererEvent } from 'electron';
import { contextBridge, ipcRenderer } from 'electron';
import log from 'electron-log/renderer';
// Initialize renderer logger
log.transports.console.level = 'silly'
log.transports.console.format = '{h}:{i}:{s}.{ms} {text}'
log.transports.console.level = 'silly';
log.transports.console.format = '{h}:{i}:{s}.{ms} {text}';
// Whitelist of valid channels used for IPC communication (Send message from Renderer to Main)
const mainAvailChannels: string[] = [
@@ -18,70 +19,70 @@ const mainAvailChannels: string[] = [
'msgFindFilesInFolder',
'msgCheckForUpdates',
'msgDownloadUpdate',
'msgInstallUpdate'
]
'msgInstallUpdate',
];
const rendererAvailChannels: string[] = [
'update-checking',
'update-available',
'update-not-available',
'update-download-progress',
'update-downloaded',
'update-error'
]
'update-error',
];
contextBridge.exposeInMainWorld('mainApi', {
send: (channel: string, ...data: any[]): void => {
if (mainAvailChannels.includes(channel)) {
ipcRenderer.send.apply(null, [channel, ...data])
ipcRenderer.send.apply(null, [channel, ...data]);
if (process.env.NODE_ENV === 'development') {
log.silly(`[IPC_SEND::${channel}]`, { request: data })
log.silly(`[IPC_SEND::${channel}]`, { request: data });
}
} else {
throw new Error(`Unknown ipc channel name: ${channel}`)
throw new Error(`Unknown ipc channel name: ${channel}`);
}
},
on: (
channel: string,
listener: (event: IpcRendererEvent, ...args: any[]) => void
listener: (event: IpcRendererEvent, ...args: any[]) => void,
): void => {
if (rendererAvailChannels.includes(channel)) {
ipcRenderer.on(channel, listener)
ipcRenderer.on(channel, listener);
} else {
throw new Error(`Unknown ipc channel name: ${channel}`)
throw new Error(`Unknown ipc channel name: ${channel}`);
}
},
once: (
channel: string,
listener: (event: IpcRendererEvent, ...args: any[]) => void
listener: (event: IpcRendererEvent, ...args: any[]) => void,
): void => {
if (rendererAvailChannels.includes(channel)) {
ipcRenderer.once(channel, listener)
ipcRenderer.once(channel, listener);
} else {
throw new Error(`Unknown ipc channel name: ${channel}`)
throw new Error(`Unknown ipc channel name: ${channel}`);
}
},
off: (
channel: string,
listener: (event: IpcRendererEvent, ...args: any[]) => void
listener: (event: IpcRendererEvent, ...args: any[]) => void,
): void => {
if (rendererAvailChannels.includes(channel)) {
ipcRenderer.off(channel, listener)
ipcRenderer.off(channel, listener);
} 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> => {
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') {
log.silly(`[IPC_INVOKE::${channel}]`, {
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">
import { DefaultLayout } from '@/renderer/components/layout'
import GlobalDialog from '@/renderer/components/GlobalDialog.vue'
import UpdateDialog from '@/renderer/components/UpdateDialog.vue'
import GlobalSnackbar from '@/renderer/components/GlobalSnackbar.vue'
import { onMounted } from 'vue'
import { useAppStore } from '@/renderer/store/app'
import { onMounted } from 'vue';
import GlobalDialog from '@/renderer/components/GlobalDialog.vue';
import GlobalSnackbar from '@/renderer/components/GlobalSnackbar.vue';
import { DefaultLayout } from '@/renderer/components/layout';
import UpdateDialog from '@/renderer/components/UpdateDialog.vue';
import { useAppStore } from '@/renderer/store/app';
// 使用应用商店来管理应用状态
const appStore = useAppStore()
const appStore = useAppStore();
onMounted(() => {
// 在应用启动时检查更新
setTimeout(() => {
checkForUpdates()
}, 3000) // 延迟3秒后检查更新,避免影响应用启动速度
checkForUpdates();
}, 3000); // 延迟3秒后检查更新,避免影响应用启动速度
// 监听主进程发来的更新相关事件
window.mainApi.on('update-available', (_, info) => {
appStore.setUpdateStatus('available', info.version)
})
appStore.setUpdateStatus('available', info.version);
});
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) => {
appStore.setUpdateStatus('error', undefined, error.message || String(error))
})
appStore.setUpdateStatus('error', undefined, error.message || String(error));
});
window.mainApi.on('update-download-progress', (_, progress) => {
// 这个事件通过pinia状态管理,不需要在这里单独处理
console.log('下载进度:', progress)
})
console.log('下载进度:', progress);
});
window.mainApi.on('update-downloaded', (_, info) => {
appStore.setUpdateStatus('downloaded')
console.log('更新已下载,准备安装:', info)
})
})
appStore.setUpdateStatus('downloaded');
console.log('更新已下载,准备安装:', info);
});
});
const checkForUpdates = async () => {
try {
// 显示检查更新状态
appStore.setUpdateStatus('checking')
appStore.setUpdateStatus('checking');
// 调用主进程检查更新
const result = await window.mainApi.invoke('msgCheckForUpdates')
const result = await window.mainApi.invoke('msgCheckForUpdates');
if (result.error) {
console.error('检查更新时出错:', result.error)
appStore.setUpdateStatus('error', result.error)
return
console.error('检查更新时出错:', result.error);
appStore.setUpdateStatus('error', result.error);
return;
}
if (result.updateAvailable) {
// 发现新版本,显示更新提示
appStore.setUpdateStatus('available', result.currentVersion)
appStore.setUpdateStatus('available', result.currentVersion);
} else {
// 当前已是最新版本
appStore.setUpdateStatus('not-available', result.currentVersion)
appStore.setUpdateStatus('not-available', result.currentVersion);
}
} catch (error) {
console.error('检查更新时出错:', error)
console.error('检查更新时出错:', error);
appStore.setUpdateStatus(
'error',
error instanceof Error ? error.message : String(error)
)
error instanceof Error ? error.message : String(error),
);
}
}
};
</script>
<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>
<v-dialog
v-model="dialogVisible"
@@ -12,17 +65,17 @@
{{ message }}
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-spacer />
<v-btn
@click="handleCancel"
variant="text"
@click="handleCancel"
>
{{ cancelText }}
</v-btn>
<v-btn
@click="handleConfirm"
:color="confirmColor"
variant="flat"
@click="handleConfirm"
>
{{ confirmText }}
</v-btn>
@@ -30,56 +83,3 @@
</v-card>
</v-dialog>
</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>
<v-dialog
v-model="isVisible"
@@ -11,24 +94,26 @@
<v-card-text>
<v-form v-model="formValid">
<v-text-field
ref="inputRef"
v-model="directoryPath"
label="目录名称"
placeholder="请输入目录名称"
:rules="[requiredRule('目录名称')]"
required
density="comfortable"
ref="inputRef"
@keydown.enter="confirm"
/>
</v-form>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn @click="close">取消</v-btn>
<v-btn @click="close">
取消
</v-btn>
<v-btn
color="primary"
@click="confirm"
:disabled="!formValid"
@click="confirm"
>
{{ isEditMode ? '保存' : '添加' }}
</v-btn>
@@ -36,84 +121,3 @@
</v-card>
</v-dialog>
</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>
<v-dialog
v-model="isVisible"
@@ -11,13 +133,13 @@
<v-card-text>
<v-form v-model="formValid">
<v-text-field
ref="inputRef"
v-model="filePath"
label="文件名"
placeholder="文件名需要带上后缀,如:style skech.doc"
:rules="[requiredRule('文件名')]"
required
density="comfortable"
ref="inputRef"
/>
<!-- 文件选择输入框 -->
@@ -26,7 +148,7 @@
label="选择文件"
placeholder="点击输入框选择文件"
readonly
:append-inner-icon="'mdi-file'"
append-inner-icon="mdi-file"
density="comfortable"
class="mt-4"
@click="selectSourceFile"
@@ -38,7 +160,7 @@
label="选择文件夹"
placeholder="点击输入框选择文件夹"
readonly
:append-inner-icon="'mdi-folder'"
append-inner-icon="mdi-folder"
density="comfortable"
class="mt-4"
@click="selectSourceFolder"
@@ -64,11 +186,13 @@
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn @click="close">取消</v-btn>
<v-btn @click="close">
取消
</v-btn>
<v-btn
color="primary"
@click="confirm"
:disabled="!formValid || (!selectedFilePath && !selectedFolderPath && !isEditMode)"
@click="confirm"
>
{{ isEditMode ? '保存' : '添加' }}
</v-btn>
@@ -76,125 +200,3 @@
</v-card>
</v-dialog>
</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>
<div>
<!-- Confirm Dialog -->
@@ -14,17 +122,17 @@
{{ currentConfirmOptions.message }}
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-spacer />
<v-btn
@click="handleConfirmCancel"
variant="text"
@click="handleConfirmCancel"
>
{{ currentConfirmOptions.cancelText }}
</v-btn>
<v-btn
@click="handleConfirmAction"
:color="currentConfirmOptions.confirmColor"
variant="flat"
@click="handleConfirmAction"
>
{{ currentConfirmOptions.confirmText }}
</v-btn>
@@ -46,11 +154,11 @@
{{ currentMessageOptions.message }}
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-spacer />
<v-btn
@click="handleMessageAction"
:color="currentMessageOptions.confirmColor"
variant="flat"
@click="handleMessageAction"
>
{{ currentMessageOptions.confirmText }}
</v-btn>
@@ -60,117 +168,6 @@
</div>
</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>
+6 -6
View File
@@ -1,3 +1,9 @@
<script setup lang="ts">
import { useSnackbarStore } from '@/renderer/store/snackbar';
const snackbar = useSnackbarStore();
</script>
<template>
<v-snackbar
v-model="snackbar.isVisible"
@@ -26,9 +32,3 @@
</template>
</v-snackbar>
</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>
<v-dialog
v-model="dialogVisible"
@@ -12,28 +68,28 @@
{{ message }}
</v-card-text>
<v-card-actions v-if="showActions">
<v-spacer></v-spacer>
<v-spacer />
<v-btn
v-if="cancelText"
@click="handleCancel"
variant="text"
@click="handleCancel"
>
{{ cancelText }}
</v-btn>
<v-btn
@click="handleConfirm"
:color="confirmColor"
variant="flat"
@click="handleConfirm"
>
{{ confirmText }}
</v-btn>
</v-card-actions>
<v-card-actions v-else>
<v-spacer></v-spacer>
<v-spacer />
<v-btn
@click="handleConfirm"
:color="confirmColor"
variant="flat"
@click="handleConfirm"
>
{{ confirmText }}
</v-btn>
@@ -41,59 +97,3 @@
</v-card>
</v-dialog>
</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>
<v-card class="d-flex flex-column" height="100%" variant="outlined">
<v-card-item>
@@ -16,7 +59,7 @@
</v-chip-group>
</v-card-text>
<v-divider class="mt-auto"></v-divider>
<v-divider class="mt-auto" />
<v-card-actions class="pa-2 justify-space-between">
<div>
@@ -29,7 +72,7 @@
</div>
<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-icon>mdi-dots-vertical</v-icon>
</v-btn>
@@ -37,72 +80,35 @@
<v-list density="compact">
<v-list-item @click="$emit('edit', template)">
<template v-slot:prepend>
<template #prepend>
<v-icon>mdi-pencil</v-icon>
</template>
<v-list-item-title>修改</v-list-item-title>
</v-list-item>
<v-list-item @click="$emit('copy', template)">
<template v-slot:prepend>
<v-icon color="primary">mdi-content-copy</v-icon>
<template #prepend>
<v-icon color="primary">
mdi-content-copy
</v-icon>
</template>
<v-list-item-title>复制</v-list-item-title>
</v-list-item>
<v-divider></v-divider>
<v-divider />
<v-list-item @click="$emit('delete', template)">
<template v-slot:prepend>
<v-icon color="error">mdi-delete</v-icon>
<template #prepend>
<v-icon color="error">
mdi-delete
</v-icon>
</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>
</v-menu>
</v-card-actions>
</v-card>
</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>
<v-card
variant="outlined"
@@ -12,7 +43,7 @@
item-value="id"
open-on-click
:hoverable="true"
:indent-lines="'default'"
indent-lines="default"
separate-roots
density="compact"
>
@@ -50,7 +81,9 @@
class="ml-2"
@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
activator="parent"
location="top"
@@ -66,7 +99,9 @@
size="small"
@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
activator="parent"
location="top"
@@ -81,7 +116,9 @@
size="small"
@click.stop="emit('editItem', item)"
>
<v-icon size="small"> mdi-pencil </v-icon>
<v-icon size="small">
mdi-pencil
</v-icon>
<v-tooltip
activator="parent"
location="top"
@@ -134,49 +171,10 @@
</p>
</div>
</v-card>
</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>
.cursor-pointer {
cursor: pointer;
}
</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>
<v-dialog
v-model="dialogVisible"
@@ -12,10 +97,12 @@
<v-card-text>
<div>{{ t('update.currentVersion') }}: {{ updateStatus.currentVersion }}</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-actions>
<v-spacer></v-spacer>
<v-spacer />
<v-btn
color="primary"
variant="text"
@@ -26,8 +113,8 @@
<v-btn
color="primary"
variant="flat"
@click="downloadUpdate"
:loading="isDownloading"
@click="downloadUpdate"
>
{{ t('update.downloadUpdate') }}
</v-btn>
@@ -47,15 +134,17 @@
>
<strong>{{ Math.round(downloadProgress) }}%</strong>
</v-progress-linear>
<div class="mt-3">{{ t('update.downloadProgressText') }}</div>
<div class="mt-3">
{{ t('update.downloadProgressText') }}
</div>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-spacer />
<v-btn
color="primary"
variant="text"
@click="cancelDownload"
:disabled="isDownloadComplete"
@click="cancelDownload"
>
{{ t('common.cancel') }}
</v-btn>
@@ -70,7 +159,7 @@
{{ t('update.updateReadyToInstall') }}
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-spacer />
<v-btn
color="primary"
variant="text"
@@ -96,7 +185,7 @@
{{ updateStatus.error }}
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-spacer />
<v-btn
color="primary"
variant="flat"
@@ -108,88 +197,3 @@
</v-card>
</v-dialog>
</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">
import HeaderLayout from '@/renderer/components/layout/HeaderLayout.vue'
import HeaderLayout from '@/renderer/components/layout/HeaderLayout.vue';
</script>
<template>
+11 -36
View File
@@ -1,51 +1,25 @@
<script setup lang="tsx">
import { useRoute, useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { mdiFitToScreenOutline, mdiHome } from '@mdi/js'
import { useRouter } from 'vue-router';
const router = useRouter()
const route: any = useRoute()
const titleKey: string = (route?.meta?.titleKey || 'title.main') as string
const { t } = useI18n()
const router = useRouter();
const handleRoute = (path: string): void => {
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'
}
]
router.push(path);
};
// Get app version
const appVersion = ref('')
const appVersion = ref('');
onMounted(async () => {
try {
appVersion.value = await window.mainApi.invoke('msgRequestGetVersion')
appVersion.value = await window.mainApi.invoke('msgRequestGetVersion');
} catch (error) {
console.error('Failed to get app version:', error)
appVersion.value = 'unknown'
console.error('Failed to get app version:', error);
appVersion.value = 'unknown';
}
})
});
</script>
<template>
<v-app-bar color="primary" density="compact">
<v-app-bar-title @click="handleRoute('/')">
@@ -63,6 +37,7 @@ onMounted(async () => {
</template>
</v-app-bar>
</template>
<style scoped>
.v-btn {
opacity: 0.4;
+3 -3
View File
@@ -1,4 +1,4 @@
import DefaultLayout from '@/renderer/components/layout/DefaultLayout.vue'
import HeaderLayout from '@/renderer/components/layout/HeaderLayout.vue'
import DefaultLayout from '@/renderer/components/layout/DefaultLayout.vue';
import HeaderLayout from '@/renderer/components/layout/HeaderLayout.vue';
export { DefaultLayout, HeaderLayout }
export { DefaultLayout, HeaderLayout };
+1 -4
View File
@@ -6,8 +6,5 @@
<body>
<div id="app"></div>
</body>
<script
type="module"
src="./main"
></script>
<script type="module" src="./main"></script>
</html>
+9 -32
View File
@@ -1,36 +1,13 @@
import { createApp } from 'vue'
import { createApp } from 'vue';
import App from '@/renderer/App.vue'
import router from '@/renderer/router'
import vuetify from '@/renderer/plugins/vuetify'
import i18n from '@/renderer/plugins/i18n'
import pinia from '@/renderer/plugins/pinia'
import App from '@/renderer/App.vue';
import i18n from '@/renderer/plugins/i18n';
import pinia from '@/renderer/plugins/pinia';
import vuetify from '@/renderer/plugins/vuetify';
import router from '@/renderer/router';
// Add API key defined in contextBridge to window object type
declare global {
interface Window {
mainApi?: any
ConfirmDialogManager?: {
showConfirm: (options: {
title: string;
message: string;
confirmText?: string;
cancelText?: string;
confirmColor?: string;
}) => Promise<boolean>;
showMessage: (options: {
title: string;
message: string;
confirmText?: string;
confirmColor?: string;
dialogColor?: string;
}) => Promise<void>;
};
}
}
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 en from '@/renderer/locales/en.json'
import ko from '@/renderer/locales/ko.json'
import zhHans from '@/renderer/locales/zh-hans.json'
import zhHant from '@/renderer/locales/zh-hant.json'
import de from '@/renderer/locales/de.json'
import es from '@/renderer/locales/es.json'
import ja from '@/renderer/locales/ja.json'
import fr from '@/renderer/locales/fr.json'
import ru from '@/renderer/locales/ru.json'
import pt from '@/renderer/locales/pt.json'
import nl from '@/renderer/locales/nl.json'
import { getCurrentLocale } from '@/renderer/utils'
import { createI18n } from 'vue-i18n';
import de from '@/renderer/locales/de.json';
import en from '@/renderer/locales/en.json';
import es from '@/renderer/locales/es.json';
import fr from '@/renderer/locales/fr.json';
import ja from '@/renderer/locales/ja.json';
import ko from '@/renderer/locales/ko.json';
import nl from '@/renderer/locales/nl.json';
import pt from '@/renderer/locales/pt.json';
import ru from '@/renderer/locales/ru.json';
import zhHans from '@/renderer/locales/zh-hans.json';
import zhHant from '@/renderer/locales/zh-hant.json';
import { getCurrentLocale } from '@/renderer/utils';
export default createI18n({
locale: getCurrentLocale(),
@@ -27,6 +27,6 @@ export default createI18n({
fr,
ru,
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 {
ko,
de,
en,
es,
fr,
ja,
ko,
nl,
pt,
ru,
zhHans,
zhHant,
de,
es,
ja,
fr,
ru,
pt,
nl
} from 'vuetify/locale'
} from 'vuetify/locale';
import colors from 'vuetify/util/colors';
// import { aliases, mdi } from 'vuetify/iconsets/mdi-svg'
import 'vuetify/styles'
import '@mdi/font/css/materialdesignicons.css'
import 'vuetify/styles';
import colors from 'vuetify/util/colors'
import '@mdi/font/css/materialdesignicons.css';
export default createVuetify({
locale: {
messages: { ko, en, zhHans, zhHant, de, es, ja, fr, ru, pt, nl },
locale: 'zhHans',
fallback: 'zhHans'
fallback: 'zhHans',
},
defaults: {
VBtn: {
style: [
{
// Do not force capitalization of a button text
textTransform: 'none'
}
]
}
textTransform: 'none',
},
],
},
},
// icons: {
// defaultSet: 'mdi',
@@ -46,15 +46,15 @@ export default createVuetify({
light: {
dark: false,
colors: {
primary: colors.green.darken2
}
primary: colors.green.darken2,
},
},
dark: {
dark: true,
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({
history: createWebHashHistory(),
routes: [
{
path: '/',
redirect: '/templates'
redirect: '/templates',
},
{
path: '/templates',
component: () => import('@/renderer/screens/TemplateListScreen.vue'),
meta: {
titleKey: 'title.templates'
}
titleKey: 'title.templates',
},
},
{
path: '/create-template',
component: () => import('@/renderer/screens/CreateTemplateScreen.vue'),
meta: {
titleKey: 'title.create-template'
}
titleKey: 'title.create-template',
},
},
{
path: '/create-template/:id',
component: () => import('@/renderer/screens/CreateTemplateScreen.vue'),
meta: {
titleKey: 'title.edit-template'
titleKey: 'title.edit-template',
},
props: true
props: true,
},
{
path: '/second',
component: () => import('@/renderer/screens/SecondScreen.vue'),
meta: {
titleKey: 'title.second'
}
titleKey: 'title.second',
},
},
{
path: '/error',
component: () => import('@/renderer/screens/ErrorScreen.vue'),
meta: {
titleKey: 'title.error'
}
titleKey: 'title.error',
},
},
{
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>
<v-container
fluid
@@ -61,10 +425,10 @@
<div class="d-flex ga-2">
<v-btn
color="primary"
@click="saveTemplate"
:disabled="!formValid"
prepend-icon="mdi-content-save"
block
@click="saveTemplate"
>
保存模板
</v-btn>
@@ -77,13 +441,15 @@
class="pa-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-btn
icon
variant="text"
@click="addRootDirectory"
size="small"
@click="addRootDirectory"
>
<v-icon>mdi-folder-plus</v-icon>
<v-tooltip
@@ -96,8 +462,8 @@
<v-btn
icon
variant="text"
@click="addRootFile"
size="small"
@click="addRootFile"
>
<v-icon>mdi-file-plus</v-icon>
<v-tooltip
@@ -107,14 +473,13 @@
添加根文件
</v-tooltip>
</v-btn>
</div>
<TemplateTree
v-model:expanded="expandedNodes"
:items="treeStructure"
:height="treeHeight"
v-model:expanded="expandedNodes"
:empty-text="'暂无文件或目录'"
empty-text="暂无文件或目录"
:read-only="false"
@add-new-item-to-current-directory="addNewItemToCurrentDirectory"
@edit-item="editItem"
@@ -141,387 +506,9 @@
:editing-item="currentEditingItem"
@confirm="handleFileDialogConfirm"
/>
</v-container>
</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>
.border-e {
border-right: 1px solid rgba(0, 0, 0, 0.12);
+40 -58
View File
@@ -1,79 +1,59 @@
<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 {
mdiBrightness6,
mdiFileDocument,
mdiFolderOpen,
mdiGithub,
mdiPlusCircle,
mdiFolderMultiple,
mdiFileDocumentMultiple,
mdiArrowRight,
mdiHome
} from '@mdi/js'
import { storeToRefs } from 'pinia'
import { useRouter } from 'vue-router'
mdiGithub,
} from '@mdi/js';
import { onMounted, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import { useTheme } from 'vuetify';
import { openExternal } from '@/renderer/utils';
const router = useRouter()
const { t, locale, availableLocales } = useI18n()
const { increaseCount } = useCounterStore()
const { count } = storeToRefs(useCounterStore())
const theme = useTheme()
const languages = ref(['en'])
const appVersion = ref('Unknown')
const selectedFile = ref('')
const router = useRouter();
const { t, locale, availableLocales } = useI18n();
const theme = useTheme();
const languages = ref(['en']);
const appVersion = ref('Unknown');
const selectedFile = ref('');
onMounted((): void => {
languages.value = availableLocales
languages.value = availableLocales;
// Get application version from package.json version string (Using IPC communication)
getApplicationVersionFromMainProcess()
})
getApplicationVersionFromMainProcess();
});
const getApplicationVersionFromMainProcess = (): void => {
window.mainApi.invoke('msgRequestGetVersion').then((result: string) => {
appVersion.value = result
})
}
appVersion.value = result;
});
};
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 => {
locale.value = val
}
locale.value = val;
};
const handleOpenDocument = async (): Promise<void> => {
await openExternal('https://vutron.cdget.com')
}
await openExternal('https://vutron.cdget.com');
};
const handleOpenGitHub = async (): Promise<void> => {
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]
}
}
await openExternal('https://github.com/jooy2/vutron');
};
const navigateToTemplates = () => {
router.push('/templates')
}
router.push('/templates');
};
const navigateToCreateTemplate = () => {
router.push('/create-template')
}
router.push('/create-template');
};
</script>
<template>
@@ -103,7 +83,7 @@ const navigateToCreateTemplate = () => {
<p v-if="selectedFile">
{{
t('desc.selected-file', {
filePath: selectedFile
filePath: selectedFile,
})
}}
</p>
@@ -112,17 +92,17 @@ const navigateToCreateTemplate = () => {
<v-btn
color="primary"
size="x-large"
@click="navigateToCreateTemplate"
class="mr-4"
prepend-icon="mdi-plus"
@click="navigateToCreateTemplate"
>
创建模板
</v-btn>
<v-btn
color="success"
size="x-large"
@click="navigateToTemplates"
prepend-icon="mdi-folder-multiple"
@click="navigateToTemplates"
>
管理模板
</v-btn>
@@ -136,7 +116,9 @@ const navigateToCreateTemplate = () => {
<v-expansion-panels variant="accordion">
<v-expansion-panel>
<v-expansion-panel-title>
<v-icon start>{{ mdiFolderMultiple }}</v-icon>
<v-icon start>
{{ mdiFolderMultiple }}
</v-icon>
如何使用模板生成器
</v-expansion-panel-title>
<v-expansion-panel-text>
@@ -172,8 +154,8 @@ const navigateToCreateTemplate = () => {
<v-btn
color="primary"
@click="handleOpenDocument"
class="ml-2"
@click="handleOpenDocument"
>
<v-icon :icon="mdiFileDocument" start />
{{ t('menu.documentation') }}
@@ -181,8 +163,8 @@ const navigateToCreateTemplate = () => {
<v-btn
color="primary"
@click="handleOpenGitHub"
class="ml-2"
@click="handleOpenGitHub"
>
<v-icon :icon="mdiGithub" start />
{{ t('menu.github') }}
@@ -199,8 +181,8 @@ const navigateToCreateTemplate = () => {
density="compact"
:label="t('menu.change-language')"
:items="languages"
@update:model-value="handleChangeLanguage"
class="ml-2"
@update:model-value="handleChangeLanguage"
/>
</v-col>
</v-row>
+3 -3
View File
@@ -1,8 +1,8 @@
<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>
<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>
<v-container>
<v-row>
<v-col cols="12">
<div class="d-flex align-center mb-4 justify-space-between flex-wrap">
<h2 class="mr-2 mb-2"> 文件创建 </h2>
<v-btn @click="navigateToCreateTemplate" color="primary" prepend-icon="mdi-plus" class="mb-2">
<h2 class="mr-2 mb-2">
文件创建
</h2>
<v-btn color="primary" prepend-icon="mdi-plus" class="mb-2" @click="navigateToCreateTemplate">
创建模板
</v-btn>
</div>
@@ -18,7 +147,7 @@
<p>还没有创建任何模板。创建您的第一个模板以开始批量生成文件和目录。</p>
</v-card-text>
<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-card-actions>
@@ -54,7 +183,7 @@
:items="templateTree"
:height="400"
:read-only="true"
:empty-text="'模板结构为空'"
empty-text="模板结构为空"
:expanded="expandedTreeNodes"
@update:expanded="expandedTreeNodes = $event"
/>
@@ -68,147 +197,26 @@
<v-card-title>执行模板: {{ selectedTemplate?.name }}</v-card-title>
<v-card-text>
<v-form v-model="executeFormValid">
<v-text-field v-model="executePath" label="选择目录" readonly @click="selectDirectory"
prepend-inner-icon="mdi-folder" :rules="[(v) => !!v || '请选择目录']" />
<v-textarea v-model="baseDirNames" label="基础目录名 (每行一个,支持批量)" placeholder="输入基础目录名,每行一个"
:rules="[(v) => !!v || '请输入基础目录名']" />
<v-text-field
v-model="executePath" label="选择目录" readonly prepend-inner-icon="mdi-folder"
:rules="[(v) => !!v || '请选择目录']" @click="selectDirectory"
/>
<v-textarea
v-model="baseDirNames" label="基础目录名 (每行一个,支持批量)" placeholder="输入基础目录名,每行一个"
:rules="[(v) => !!v || '请输入基础目录名']"
/>
</v-form>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn @click="showExecuteDialog = false"> 取消 </v-btn>
<v-btn color="success" @click="confirmExecuteTemplate" :disabled="!executeFormValid">
<v-btn @click="showExecuteDialog = false">
取消
</v-btn>
<v-btn color="success" :disabled="!executeFormValid" @click="confirmExecuteTemplate">
确认执行
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</v-container>
</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 MainScreen from '@/renderer/screens/MainScreen.vue'
import SecondScreen from '@/renderer/screens/SecondScreen.vue'
import TemplateListScreen from '@/renderer/screens/TemplateListScreen.vue'
import CreateTemplateScreen from '@/renderer/screens/CreateTemplateScreen.vue'
import CreateTemplateScreen from '@/renderer/screens/CreateTemplateScreen.vue';
import ErrorScreen from '@/renderer/screens/ErrorScreen.vue';
import MainScreen from '@/renderer/screens/MainScreen.vue';
import SecondScreen from '@/renderer/screens/SecondScreen.vue';
import TemplateListScreen from '@/renderer/screens/TemplateListScreen.vue';
export {
CreateTemplateScreen,
ErrorScreen,
MainScreen,
SecondScreen,
TemplateListScreen,
CreateTemplateScreen
}
};
+22 -22
View File
@@ -1,58 +1,58 @@
import { defineStore } from 'pinia'
import { defineStore } from 'pinia';
export interface UpdateStatus {
status: 'idle' | 'checking' | 'available' | 'not-available' | 'downloading' | 'downloaded' | 'error'
currentVersion?: string
error?: string
progress?: number
status: 'idle' | 'checking' | 'available' | 'not-available' | 'downloading' | 'downloaded' | 'error';
currentVersion?: string;
error?: string;
progress?: number;
}
export const useAppStore = defineStore('app', {
state: () => ({
updateStatus: {
status: 'idle' as const
status: 'idle' as const,
} as UpdateStatus,
updateDialogVisible: false,
downloadProgress: 0
downloadProgress: 0,
}),
getters: {
isUpdateAvailable: (state) => state.updateStatus.status === 'available',
isCheckingForUpdates: (state) => state.updateStatus.status === 'checking',
hasUpdateError: (state) => state.updateStatus.status === 'error'
isUpdateAvailable: state => state.updateStatus.status === 'available',
isCheckingForUpdates: state => state.updateStatus.status === 'checking',
hasUpdateError: state => state.updateStatus.status === 'error',
},
actions: {
setUpdateStatus(status: UpdateStatus['status'], currentVersion?: string, error?: string) {
this.updateStatus = { status, currentVersion, error }
this.updateStatus = { status, currentVersion, error };
},
showUpdateDialog() {
this.updateDialogVisible = true
this.updateDialogVisible = true;
},
hideUpdateDialog() {
this.updateDialogVisible = false
this.updateDialogVisible = false;
},
setDownloadProgress(progress: number) {
this.downloadProgress = progress
this.downloadProgress = progress;
},
// 启动更新下载
async startUpdateDownload() {
try {
this.setUpdateStatus('downloading')
const result = await window.mainApi.invoke('msgDownloadUpdate')
this.setUpdateStatus('downloading');
const result = await window.mainApi.invoke('msgDownloadUpdate');
if (result.success) {
this.setUpdateStatus('downloaded')
this.setUpdateStatus('downloaded');
} else {
this.setUpdateStatus('error', undefined, result.error)
this.setUpdateStatus('error', undefined, result.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 { ref } from 'vue'
import { defineStore } from 'pinia';
import { ref } from 'vue';
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const count = ref(0);
function increaseCount(amount: number) {
count.value += amount
count.value += amount;
}
return {
count,
increaseCount
}
})
increaseCount,
};
});
+26 -26
View File
@@ -1,36 +1,36 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { defineStore } from 'pinia';
import { ref } from 'vue';
export interface SnackbarAction {
text: string
color?: string
handler?: () => void
text: string;
color?: string;
handler?: () => void;
}
export const useSnackbarStore = defineStore('snackbar', () => {
const isVisible = ref(false)
const message = ref('')
const color = ref('info')
const timeout = ref(3000)
const actions = ref<SnackbarAction[]>([])
const isVisible = ref(false);
const message = ref('');
const color = ref('info');
const timeout = ref(3000);
const actions = ref<SnackbarAction[]>([]);
const show = (msg: string, clr = 'info', tout = 3000, actionList: SnackbarAction[] = []) => {
message.value = msg
color.value = clr
timeout.value = tout
actions.value = actionList
isVisible.value = true
}
message.value = msg;
color.value = clr;
timeout.value = tout;
actions.value = actionList;
isVisible.value = true;
};
const success = (msg: string, tout = 3000) => show(msg, 'success', tout)
const error = (msg: string, tout = 3000) => show(msg, 'error', tout)
const warning = (msg: string, tout = 3000) => show(msg, 'warning', tout)
const info = (msg: string, tout = 3000) => show(msg, 'info', tout)
const success = (msg: string, tout = 3000) => show(msg, 'success', tout);
const error = (msg: string, tout = 3000) => show(msg, 'error', tout);
const warning = (msg: string, tout = 3000) => show(msg, 'warning', tout);
const info = (msg: string, tout = 3000) => show(msg, 'info', tout);
const hide = () => {
isVisible.value = false
actions.value = []
}
isVisible.value = false;
actions.value = [];
};
return {
isVisible,
@@ -43,6 +43,6 @@ export const useSnackbarStore = defineStore('snackbar', () => {
error,
warning,
info,
hide
}
})
hide,
};
});
+30 -30
View File
@@ -1,24 +1,24 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { TreeNode, Template } from '@/types'
import type { Template } from '@/types';
import { defineStore } from 'pinia';
import { ref } from 'vue';
export const useTemplateStore = defineStore('template', () => {
const templates = ref<Template[]>([])
const dataFile = 'templates.json'
const templates = ref<Template[]>([]);
const dataFile = 'templates.json';
// Load templates from local storage or initialize
async function loadTemplates() {
try {
const stored = await window.mainApi.invoke('msgLoadData', dataFile)
const stored = await window.mainApi.invoke('msgLoadData', dataFile);
if (stored) {
templates.value = JSON.parse(stored as string) || []
templates.value = JSON.parse(stored as string) || [];
} else {
// Initialize with sample data if no stored data
templates.value = []
templates.value = [];
}
} catch (error) {
console.error('Failed to load templates:', error)
templates.value = []
console.error('Failed to load templates:', error);
templates.value = [];
}
}
@@ -28,54 +28,54 @@ export const useTemplateStore = defineStore('template', () => {
window.mainApi.invoke(
'msgSaveData',
dataFile,
JSON.stringify(templates.value)
)
JSON.stringify(templates.value),
);
} catch (error) {
console.error('Failed to save templates:', error)
console.error('Failed to save templates:', error);
}
}
// Get all templates
function getTemplates() {
return templates.value
return templates.value;
}
// Get template by ID
function getTemplateById(id: string) {
return templates.value.find((template) => template.id === id)
return templates.value.find(template => template.id === id);
}
// Add new template
function addTemplate(template: Omit<Template, 'id'>) {
const newTemplate: Template = {
id: `template_${Date.now()}`,
...template
}
templates.value.push(newTemplate)
saveTemplates()
...template,
};
templates.value.push(newTemplate);
saveTemplates();
}
// Update existing 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) {
templates.value[index] = updatedTemplate
saveTemplates()
templates.value[index] = updatedTemplate;
saveTemplates();
}
}
// Delete template
function deleteTemplate(id: string) {
templates.value = templates.value.filter((template) => template.id !== id)
saveTemplates()
templates.value = templates.value.filter(template => template.id !== id);
saveTemplates();
}
// Get template names for dropdowns
function getTemplateNames() {
return templates.value.map((template) => ({
return templates.value.map(template => ({
text: template.name,
value: template.id
}))
value: template.id,
}));
}
return {
@@ -86,6 +86,6 @@ export const useTemplateStore = defineStore('template', () => {
addTemplate,
updateTemplate,
deleteTemplate,
getTemplateNames
}
})
getTemplateNames,
};
});
+7
View File
@@ -16,4 +16,11 @@ interface Window {
dialogColor?: string;
}) => Promise<void>;
};
mainApi: {
send: (channel: string, ...data: any[]) => void;
on: (channel: string, listener: (event: any, ...args: any[]) => void) => void;
once: (channel: string, listener: (event: any, ...args: any[]) => void) => void;
off: (channel: string, listener: (event: any, ...args: any[]) => void) => void;
invoke: (channel: string, ...data: any[]) => Promise<any>;
};
}
+29 -32
View File
@@ -1,25 +1,22 @@
// 定义对话框选项接口
interface BaseDialogOptions {
title: string
message: string
confirmText?: string
cancelText?: string
confirmColor?: string
title: string;
message: string;
confirmText?: string;
cancelText?: string;
confirmColor?: string;
}
interface ConfirmDialogOptions extends BaseDialogOptions { }
interface MessageDialogOptions extends BaseDialogOptions {
dialogColor?: string
dialogColor?: string;
}
// 对话框类型
type DialogType = 'confirm' | 'info' | 'success' | 'warning' | 'error'
// 全局对话框管理器接口
interface GlobalDialogManager {
showConfirm: (options: ConfirmDialogOptions) => Promise<boolean>
showMessage: (options: MessageDialogOptions) => Promise<void>
showConfirm: (options: ConfirmDialogOptions) => Promise<boolean>;
showMessage: (options: MessageDialogOptions) => Promise<void>;
}
// 确认对话框服务
@@ -27,11 +24,11 @@ export const ConfirmDialogService = {
// 显示确认对话框
async confirm(options: ConfirmDialogOptions): Promise<boolean> {
if (window.ConfirmDialogManager) {
return (window.ConfirmDialogManager as GlobalDialogManager).showConfirm(options)
return (window.ConfirmDialogManager as GlobalDialogManager).showConfirm(options);
} else {
console.error('GlobalDialog component not properly initialized')
console.error('GlobalDialog component not properly initialized');
// 降级到原生 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({
...options,
dialogColor: options.dialogColor || 'info',
confirmColor: options.confirmColor || 'white'
})
confirmColor: options.confirmColor || 'white',
});
} else {
console.error('GlobalDialog component not properly initialized')
console.error('GlobalDialog component not properly initialized');
// 降级到原生 alert
alert(`${options.title}: ${options.message}`)
alert(`${options.title}: ${options.message}`);
}
},
@@ -56,12 +53,12 @@ export const ConfirmDialogService = {
return (window.ConfirmDialogManager as GlobalDialogManager).showMessage({
...options,
dialogColor: options.dialogColor || 'success',
confirmColor: options.confirmColor || 'white'
})
confirmColor: options.confirmColor || 'white',
});
} else {
console.error('GlobalDialog component not properly initialized')
console.error('GlobalDialog component not properly initialized');
// 降级到原生 alert
alert(`${options.title}: ${options.message}`)
alert(`${options.title}: ${options.message}`);
}
},
@@ -71,12 +68,12 @@ export const ConfirmDialogService = {
return (window.ConfirmDialogManager as GlobalDialogManager).showMessage({
...options,
dialogColor: options.dialogColor || 'warning',
confirmColor: options.confirmColor || 'white'
})
confirmColor: options.confirmColor || 'white',
});
} else {
console.error('GlobalDialog component not properly initialized')
console.error('GlobalDialog component not properly initialized');
// 降级到原生 alert
alert(`${options.title}: ${options.message}`)
alert(`${options.title}: ${options.message}`);
}
},
@@ -86,12 +83,12 @@ export const ConfirmDialogService = {
return (window.ConfirmDialogManager as GlobalDialogManager).showMessage({
...options,
dialogColor: options.dialogColor || 'error',
confirmColor: options.confirmColor || 'white'
})
confirmColor: options.confirmColor || 'white',
});
} else {
console.error('GlobalDialog component not properly initialized')
console.error('GlobalDialog component not properly initialized');
// 降级到原生 alert
alert(`${options.title}: ${options.message}`)
alert(`${options.title}: ${options.message}`);
}
}
}
},
};
+6 -6
View File
@@ -1,13 +1,13 @@
export function getCurrentLocale(): string {
return navigator?.language?.split('-')[0] || 'en'
return navigator?.language?.split('-')[0] || 'en';
}
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> {
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字符串
*/
export function generateUniqueId(prefix: string = ''): string {
const timestamp = Date.now().toString(36) // 转换为36进制,更短
const randomStr = Math.random().toString(36).substring(2, 9) // 7位随机字符
return prefix ? `${prefix}-${timestamp}-${randomStr}` : `${timestamp}-${randomStr}`
const timestamp = Date.now().toString(36); // 转换为36进制,更短
const randomStr = Math.random().toString(36).substring(2, 9); // 7位随机字符
return prefix ? `${prefix}-${timestamp}-${randomStr}` : `${timestamp}-${randomStr}`;
}
+18
View File
@@ -0,0 +1,18 @@
// 定义树状结构的节点
export interface TreeNode {
id: string;
name: string;
path: string; // 完整路径
type: 'directory' | 'file';
children?: TreeNode[];
sourcePath?: string;
sourceType?: 'file' | 'folder'; // 记录选择的是文件还是文件夹
}
export interface Template {
id: string;
name: string;
description: string;
// 使用树状结构存储
treeStructure: TreeNode[];
}
-18
View File
@@ -1,18 +0,0 @@
// 定义树状结构的节点
export interface TreeNode {
id: string
name: string
path: string // 完整路径
type: 'directory' | 'file'
children?: TreeNode[]
sourcePath?: string
sourceType?: 'file' | 'folder' // 记录选择的是文件还是文件夹
}
export interface Template {
id: string
name: string
description: string
// 使用树状结构存储
treeStructure: TreeNode[]
}
+4 -3
View File
@@ -1,6 +1,7 @@
// For fix typescript import errors
declare module '*.vue' {
import { defineComponent } from 'vue'
const component: ReturnType<typeof defineComponent>
export default component
import type { defineComponent } from 'vue';
const component: ReturnType<typeof defineComponent>;
export default component;
}
+39 -38
View File
@@ -1,17 +1,18 @@
import * as base from '@playwright/test'
import { _electron as electron, Page, ElectronApplication } from 'playwright'
import { join } from 'path'
import { main } from '../package.json'
import TestUtil from './testUtil.mjs'
import type { ElectronApplication, Page } from 'playwright';
import { join } from 'node:path';
import * as base from '@playwright/test';
import { _electron as electron } from 'playwright';
import { main } from '../package.json';
import TestUtil from './testUtil.mjs';
let appElectron: ElectronApplication
let page: Page
let appElectron: ElectronApplication;
let page: Page;
const __cwd = process.cwd()
const __isCiProcess = process.env.CI === 'true'
const __testPath = join(__cwd, 'tests')
const __testResultPath = join(__testPath, 'results')
const __testScreenshotPath = join(__testResultPath, 'screenshots')
const __cwd = process.cwd();
const __isCiProcess = process.env.CI === 'true';
const __testPath = join(__cwd, 'tests');
const __testResultPath = join(__testPath, 'results');
const __testScreenshotPath = join(__testResultPath, 'screenshots');
export const beforeAll = async () => {
// Open Electron app from build directory
@@ -23,60 +24,60 @@ export const beforeAll = async () => {
'--ignore-certificate-errors',
'--ignore-ssl-errors',
'--ignore-blocklist',
'--ignore-gpu-blocklist'
'--ignore-gpu-blocklist',
],
locale: 'en-US',
colorScheme: 'light',
env: {
...process.env,
NODE_ENV: 'production'
}
})
page = await appElectron.firstWindow()
NODE_ENV: 'production',
},
});
page = await appElectron.firstWindow();
await page.waitForEvent('load')
await page.waitForEvent('load');
page.on('console', console.log)
page.on('pageerror', console.log)
page.on('console', console.log);
page.on('pageerror', console.log);
const evaluateResult = await appElectron.evaluate(
async ({ app, BrowserWindow }) => {
const currentWindow = BrowserWindow.getFocusedWindow()
const currentWindow = BrowserWindow.getFocusedWindow();
// Fix window position for testing
currentWindow.setPosition(50, 50)
currentWindow.setSize(1080, 560)
currentWindow.setPosition(50, 50);
currentWindow.setSize(1080, 560);
return {
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 () => {
await appElectron.close()
}
await appElectron.close();
};
export const test = base.test.extend({
// eslint-disable-next-line no-empty-pattern
page: async ({}, use) => {
await use(page)
await use(page);
},
// @ts-expect-error: `util` is not using types in playwright
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 {
test,
expect,
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.afterAll(afterAll)
test.beforeAll(beforeAll);
test.afterAll(afterAll);
// @ts-expect-error: `util` is not using types in playwright
test('Document element check', async ({ page, util }) => {
try {
await expect(
page.getByTestId('main-logo').first(),
`Confirm main logo is visible`
).toBeVisible()
`Confirm main logo is visible`,
).toBeVisible();
await expect(
page.getByTestId('select-language').first(),
`Confirm language selector is visible`
).toBeVisible()
`Confirm language selector is visible`,
).toBeVisible();
await util.captureScreenshot(page, 'result')
await util.captureScreenshot(page, 'result');
} catch (error) {
throw await util.onTestError(error)
throw await util.onTestError(error);
}
})
});
// @ts-expect-error: `util` is not using types in playwright
test('Counter button click check', async ({ page, util }) => {
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
.getByTestId('counter-badge')
.getByRole('status')
.innerHTML()
.innerHTML();
expect(counterValueElement).toBe('10')
expect(counterValueElement).toBe('10');
} 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.afterAll(afterAll)
test.beforeAll(beforeAll);
test.afterAll(afterAll);
// @ts-expect-error: `util` is not using types in playwright
test('Template list page navigation and basic functionality', async ({
page,
util
util,
}) => {
try {
// Navigate to templates page
await page.goto('/')
await page.waitForURL('**/#/templates')
await page.goto('/');
await page.waitForURL('**/#/templates');
// 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
await expect(page.getByText('暂无模板,请先创建模板。')).toBeVisible()
await expect(page.getByText('暂无模板,请先创建模板。')).toBeVisible();
// 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) {
throw await util.onTestError(error)
throw await util.onTestError(error);
}
})
});
// @ts-expect-error: `util` is not using types in playwright
test('Create template page navigation and functionality', async ({
page,
util
util,
}) => {
try {
// Navigate to templates page
await page.goto('/')
await page.waitForURL('**/#/templates')
await page.goto('/');
await page.waitForURL('**/#/templates');
// Click create template button
await page.getByText('创建模板').click()
await page.waitForURL('**/#/create-template')
await page.getByText('创建模板').click();
await page.waitForURL('**/#/create-template');
// 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
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) {
throw await util.onTestError(error)
throw await util.onTestError(error);
}
})
});
// @ts-expect-error: `util` is not using types in playwright
test('Add files and directories to template', async ({ page, util }) => {
try {
// Navigate to create template page
await page.goto('/#/create-template')
await page.goto('/#/create-template');
// Fill in template name and description
await page.getByLabel('模板名称 *').fill('Test Template')
await page.getByLabel('模板描述').fill('This is a test template')
await page.getByLabel('模板名称 *').fill('Test Template');
await page.getByLabel('模板描述').fill('This is a test template');
// Add a file
await page.getByText('添加文件').click()
await page.getByText('添加文件').click();
// Wait for dialog to appear and fill in file details
await page
.locator('[data-testid="add-file-path-input"]')
.fill('src/index.js')
.fill('src/index.js');
await page
.locator('[data-testid="add-file-content-input"]')
.fill('console.log("Hello World");')
await page.getByText('添加').click()
.fill('console.log("Hello World");');
await page.getByText('添加').click();
// Add a directory
await page.getByText('添加目录').click()
await page.getByText('添加目录').click();
// Wait for dialog to appear and fill in directory details
await page
.locator('[data-testid="add-dir-path-input"]')
.fill('src/components')
await page.getByText('添加').click()
.fill('src/components');
await page.getByText('添加').click();
// Verify items were added to the tables
await expect(page.getByText('src/index.js')).toBeVisible()
await expect(page.getByText('src/components')).toBeVisible()
await expect(page.getByText('src/index.js')).toBeVisible();
await expect(page.getByText('src/components')).toBeVisible();
await util.captureScreenshot(page, 'template-with-content')
await util.captureScreenshot(page, 'template-with-content');
} catch (error) {
throw await util.onTestError(error)
throw await util.onTestError(error);
}
})
});
// @ts-expect-error: `util` is not using types in playwright
test('Template creation and listing', async ({ page, util }) => {
try {
// Navigate to create template page
await page.goto('/#/create-template')
await page.goto('/#/create-template');
// Fill in template name and description
await page.getByLabel('模板名称 *').fill('Test Template')
await page.getByLabel('模板描述').fill('This is a test template')
await page.getByLabel('模板名称 *').fill('Test Template');
await page.getByLabel('模板描述').fill('This is a test template');
// Add a file
await page.getByText('添加文件').click()
await page.getByText('添加文件').click();
await page
.locator('[data-testid="add-file-path-input"]')
.fill('src/index.js')
.fill('src/index.js');
await page
.locator('[data-testid="add-file-content-input"]')
.fill('console.log("Hello World");')
await page.getByText('添加').click()
.fill('console.log("Hello World");');
await page.getByText('添加').click();
// Save template
await page.getByText('保存模板').click()
await page.getByText('保存模板').click();
// Wait for redirect to templates page
await page.waitForURL('**/#/templates')
await page.waitForURL('**/#/templates');
// Check if the created template is listed
await expect(page.getByText('Test Template')).toBeVisible()
await expect(page.getByText('This is a test template')).toBeVisible()
await expect(page.getByText('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) {
throw await util.onTestError(error)
throw await util.onTestError(error);
}
})
});
// @ts-expect-error: `util` is not using types in playwright
test('View template details', async ({ page, util }) => {
try {
// Navigate to templates page
await page.goto('/#/templates')
await page.goto('/#/templates');
// Wait for template to appear and click view button
// First we need to create a template for this test
if (await page.getByText('暂无模板,请先创建模板。').isVisible()) {
await page.getByText('创建模板').click()
await page.getByText('创建模板').click();
// Create a test template
await page.getByLabel('模板名称 *').fill('View Test Template')
await page.getByLabel('模板描述').fill('Template for view test')
await page.getByLabel('模板名称 *').fill('View Test Template');
await page.getByLabel('模板描述').fill('Template for view test');
await page.getByText('添加文件').click()
await page.getByText('添加文件').click();
await page
.locator('[data-testid="add-file-path-input"]')
.fill('src/test.js')
.fill('src/test.js');
await page
.locator('[data-testid="add-file-content-input"]')
.fill('console.log("Test");')
await page.getByText('添加').click()
.fill('console.log("Test");');
await page.getByText('添加').click();
await page.getByText('保存模板').click()
await page.waitForURL('**/#/templates')
await page.getByText('保存模板').click();
await page.waitForURL('**/#/templates');
}
// 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
await expect(page.getByText('模板详情')).toBeVisible()
await expect(page.getByText('src/test.js')).toBeVisible()
await expect(page.getByText('模板详情')).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) {
throw await util.onTestError(error)
throw await util.onTestError(error);
}
})
});
+16 -16
View File
@@ -1,40 +1,40 @@
import { Page } from 'playwright'
import { TestInfo } from 'playwright/test'
import type { Page } from 'playwright';
import type { TestInfo } from 'playwright/test';
export default class TestUtil {
_page: Page
_page: Page;
_testInfo: TestInfo
_testInfo: TestInfo;
_testScreenshotPath: string
_testScreenshotPath: string;
constructor(page: Page, testInfo: TestInfo, testScreenshotPath: string) {
this._page = page
this._testInfo = testInfo
this._testScreenshotPath = testScreenshotPath
this._page = page;
this._testInfo = testInfo;
this._testScreenshotPath = testScreenshotPath;
}
async captureScreenshot(pageInstance: Page, screenshotName: string) {
if (!pageInstance) {
return
return;
}
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 {
// Do nothing
}
}
async onTestError(error: Error) {
const titleLists = [...this._testInfo.titlePath]
titleLists.shift()
const title = titleLists.join('-')
const titleLists = [...this._testInfo.titlePath];
titleLists.shift();
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);
}
}
+4 -2
View File
@@ -8,7 +8,8 @@
"jsx": "preserve",
"noImplicitAny": false,
"allowSyntheticDefaultImports": true,
"declaration": true,
"declaration": false,
"declarationMap": false,
"resolveJsonModule": true,
"esModuleInterop": true,
"sourceMap": true,
@@ -19,7 +20,8 @@
},
"lib": ["esnext", "dom"]
},
"include": ["src/*.ts", "src/*.d.ts", "src/renderer", "src/types"],
"include": ["src/*.ts", "src/*.d.ts", "src/renderer"],
"exclude": ["src/main", "src/preload"],
"references": [
{
"path": "./tsconfig.node.json"
+47 -46
View File
@@ -1,30 +1,31 @@
import { fileURLToPath } from 'url'
import { defineConfig, loadEnv } from 'vite'
import ElectronPlugin, { ElectronOptions } from 'vite-plugin-electron'
import RendererPlugin from 'vite-plugin-electron-renderer'
import EslintPlugin from 'vite-plugin-eslint'
import VuetifyPlugin from 'vite-plugin-vuetify'
import VueJsx from '@vitejs/plugin-vue-jsx'
import Vue from '@vitejs/plugin-vue'
import AutoImport from 'unplugin-auto-import/vite'
import { rmSync } from 'fs'
import { resolve, dirname } from 'path'
import { builtinModules } from 'module'
import type { ElectronOptions } from 'vite-plugin-electron';
import { rmSync } from 'node:fs';
import { builtinModules } from 'node:module';
import { dirname, resolve } from 'node:path';
import process from 'node:process';
import { fileURLToPath } from 'node:url';
import Vue from '@vitejs/plugin-vue';
import VueJsx from '@vitejs/plugin-vue-jsx';
import AutoImport from 'unplugin-auto-import/vite';
import { defineConfig, loadEnv } from 'vite';
import ElectronPlugin from 'vite-plugin-electron';
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 }) => {
process.env = {
...(isDevEnv
? {
ELECTRON_ENABLE_LOGGING: 'true'
ELECTRON_ENABLE_LOGGING: 'true',
}
: {}),
...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[] = [
{
@@ -33,9 +34,9 @@ export default defineConfig(({ mode }) => {
const debugArgs = [
'.',
'--inspect=9228',
'--remote-debugging-port=9229'
]
startup(debugArgs)
'--remote-debugging-port=9229',
];
startup(debugArgs);
},
vite: {
root: resolve('.'),
@@ -46,24 +47,24 @@ export default defineConfig(({ mode }) => {
assetsDir: '.',
outDir: 'dist/main',
rollupOptions: {
external: ['electron', ...builtinModules]
}
}
}
external: ['electron', ...builtinModules],
},
},
},
},
{
entry: 'src/preload/index.ts',
onstart({ reload }) {
reload()
reload();
},
vite: {
root: resolve('.'),
build: {
outDir: 'dist/preload'
}
}
}
]
outDir: 'dist/preload',
},
},
},
];
if (isDevEnv) {
electronPluginConfigs.push({
@@ -71,23 +72,23 @@ export default defineConfig(({ mode }) => {
vite: {
root: resolve('.'),
build: {
outDir: 'dist/main'
}
}
})
outDir: 'dist/main',
},
},
});
}
return {
define: {
__VUE_I18N_FULL_INSTALL__: true,
__VUE_I18N_LEGACY_API__: false,
__INTLIFY_PROD_DEVTOOLS__: false
__INTLIFY_PROD_DEVTOOLS__: false,
},
resolve: {
extensions: ['.mjs', '.js', '.ts', '.vue', '.json', '.scss'],
alias: {
'@': resolve(dirname(fileURLToPath(import.meta.url)), 'src')
}
'@': resolve(dirname(fileURLToPath(import.meta.url)), 'src'),
},
},
base: './',
root: resolve('./src/renderer'),
@@ -96,7 +97,7 @@ export default defineConfig(({ mode }) => {
build: {
sourcemap: isDevEnv,
minify: !isDevEnv,
outDir: resolve('./dist')
outDir: resolve('./dist'),
},
plugins: [
Vue(),
@@ -108,25 +109,25 @@ export default defineConfig(({ mode }) => {
'vue-router',
'vue-i18n',
'@vueuse/core',
'pinia'
'pinia',
],
dirs: [
'./src/renderer/composables',
'./src/renderer/utils',
'./src/renderer/store',
'./src/renderer/components'
'./src/renderer/components',
],
vueTemplate: true
vueTemplate: true,
}),
// Docs: https://github.com/vuetifyjs/vuetify-loader
VuetifyPlugin({
autoImport: true
autoImport: true,
}),
// Docs: https://github.com/gxmari007/vite-plugin-eslint
// EslintPlugin(),
// Docs: https://github.com/electron-vite/vite-plugin-electron
ElectronPlugin(electronPluginConfigs),
RendererPlugin()
]
}
})
RendererPlugin(),
],
};
});