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