lint
This commit is contained in:
1 parent
fc250c54ed
commit
5eec516459
62 files changed
+3912
-3376
No files matched your search
+43
-43
@@ -1,63 +1,63 @@
|
||||
import { dialog, ipcMain, app } from 'electron'
|
||||
import { autoUpdater } from 'electron-updater'
|
||||
import log from 'electron-log/main'
|
||||
import { dialog } from 'electron';
|
||||
import log from 'electron-log/main';
|
||||
import { autoUpdater } from 'electron-updater';
|
||||
|
||||
// 配置日志
|
||||
autoUpdater.logger = log
|
||||
log.transports.file.level = 'info'
|
||||
autoUpdater.logger = log;
|
||||
log.transports.file.level = 'info';
|
||||
|
||||
export class AutoUpdater {
|
||||
private mainWindow: Electron.BrowserWindow | null = null
|
||||
private mainWindow: Electron.BrowserWindow | null = null;
|
||||
|
||||
constructor(mainWindow: Electron.BrowserWindow) {
|
||||
this.mainWindow = mainWindow
|
||||
|
||||
this.mainWindow = mainWindow;
|
||||
|
||||
// 配置更新检查间隔
|
||||
autoUpdater.autoDownload = false
|
||||
autoUpdater.allowPrerelease = false
|
||||
autoUpdater.channel = 'latest'
|
||||
|
||||
autoUpdater.autoDownload = false;
|
||||
autoUpdater.allowPrerelease = false;
|
||||
autoUpdater.channel = 'latest';
|
||||
|
||||
// 如果是私有仓库,设置认证信息
|
||||
if (process.env.GH_TOKEN || process.env.GITHUB_TOKEN) {
|
||||
// 使用环境变量中的令牌
|
||||
autoUpdater.requestHeaders = {
|
||||
'Authorization': `token ${process.env.GH_TOKEN || process.env.GITHUB_TOKEN}`
|
||||
}
|
||||
Authorization: `token ${process.env.GH_TOKEN || process.env.GITHUB_TOKEN}`,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// 设置事件监听器
|
||||
this.setupEventListeners()
|
||||
this.setupEventListeners();
|
||||
}
|
||||
|
||||
private setupEventListeners() {
|
||||
// 检查更新
|
||||
autoUpdater.on('checking-for-update', () => {
|
||||
log.info('正在检查更新...')
|
||||
this.mainWindow?.webContents.send('update-checking')
|
||||
})
|
||||
log.info('正在检查更新...');
|
||||
this.mainWindow?.webContents.send('update-checking');
|
||||
});
|
||||
|
||||
// 发现新版本
|
||||
autoUpdater.on('update-available', (info) => {
|
||||
log.info('发现新版本:', info.version)
|
||||
this.mainWindow?.webContents.send('update-available', info)
|
||||
})
|
||||
log.info('发现新版本:', info.version);
|
||||
this.mainWindow?.webContents.send('update-available', info);
|
||||
});
|
||||
|
||||
// 没有新版本
|
||||
autoUpdater.on('update-not-available', (info) => {
|
||||
log.info('当前已是最新版本')
|
||||
this.mainWindow?.webContents.send('update-not-available', info)
|
||||
})
|
||||
log.info('当前已是最新版本');
|
||||
this.mainWindow?.webContents.send('update-not-available', info);
|
||||
});
|
||||
|
||||
// 更新下载进度
|
||||
autoUpdater.on('download-progress', (progress) => {
|
||||
log.info('下载进度:', progress.percent, '%')
|
||||
this.mainWindow?.webContents.send('update-download-progress', progress)
|
||||
})
|
||||
log.info('下载进度:', progress.percent, '%');
|
||||
this.mainWindow?.webContents.send('update-download-progress', progress);
|
||||
});
|
||||
|
||||
// 更新下载完成
|
||||
autoUpdater.on('update-downloaded', (info) => {
|
||||
log.info('更新下载完成')
|
||||
this.mainWindow?.webContents.send('update-downloaded', info)
|
||||
log.info('更新下载完成');
|
||||
this.mainWindow?.webContents.send('update-downloaded', info);
|
||||
|
||||
// 提示用户是否重启应用安装更新
|
||||
dialog.showMessageBox({
|
||||
@@ -67,31 +67,31 @@ export class AutoUpdater {
|
||||
detail: '是否现在重启应用以安装更新?',
|
||||
buttons: ['稍后重启', '立即重启'],
|
||||
defaultId: 1,
|
||||
cancelId: 0
|
||||
cancelId: 0,
|
||||
}).then((result) => {
|
||||
if (result.response === 1) {
|
||||
// 安装更新并重启应用
|
||||
setImmediate(() => {
|
||||
autoUpdater.quitAndInstall()
|
||||
})
|
||||
autoUpdater.quitAndInstall();
|
||||
});
|
||||
}
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
// 更新错误
|
||||
autoUpdater.on('error', (error) => {
|
||||
log.error('自动更新出错:', error)
|
||||
this.mainWindow?.webContents.send('update-error', error)
|
||||
})
|
||||
log.error('自动更新出错:', error);
|
||||
this.mainWindow?.webContents.send('update-error', error);
|
||||
});
|
||||
}
|
||||
|
||||
public checkForUpdates() {
|
||||
log.info('开始检查更新...')
|
||||
autoUpdater.checkForUpdates()
|
||||
log.info('开始检查更新...');
|
||||
autoUpdater.checkForUpdates();
|
||||
}
|
||||
|
||||
public downloadUpdate() {
|
||||
log.info('开始下载更新...')
|
||||
autoUpdater.downloadUpdate()
|
||||
log.info('开始下载更新...');
|
||||
autoUpdater.downloadUpdate();
|
||||
}
|
||||
}
|
||||
}
|
||||
+128
-128
@@ -1,17 +1,18 @@
|
||||
import { ipcMain, shell, IpcMainEvent, dialog } from 'electron'
|
||||
import Constants from './utils/Constants'
|
||||
import { join, dirname } from 'path'
|
||||
import type { IpcMainEvent } from 'electron';
|
||||
import type { Template, TreeNode } from '../types';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { dialog, ipcMain, shell } from 'electron';
|
||||
import { autoUpdater } from 'electron-updater';
|
||||
import {
|
||||
writeFile,
|
||||
readFile,
|
||||
pathExists,
|
||||
ensureDir,
|
||||
copy,
|
||||
ensureDir,
|
||||
pathExists,
|
||||
readdir,
|
||||
readFile,
|
||||
stat,
|
||||
} from 'fs-extra'
|
||||
import { autoUpdater } from 'electron-updater'
|
||||
import { Template, TreeNode } from '../types'
|
||||
writeFile,
|
||||
} from 'fs-extra';
|
||||
import Constants from './utils/Constants';
|
||||
|
||||
/*
|
||||
* IPC Communications
|
||||
@@ -20,154 +21,151 @@ export default class IPCs {
|
||||
static initialize(): void {
|
||||
// Get application version
|
||||
ipcMain.handle('msgRequestGetVersion', () => {
|
||||
return Constants.APP_VERSION
|
||||
})
|
||||
return Constants.APP_VERSION;
|
||||
});
|
||||
|
||||
// Open url via web browser
|
||||
ipcMain.on(
|
||||
'msgOpenExternalLink',
|
||||
async (event: IpcMainEvent, url: string) => {
|
||||
await shell.openExternal(url)
|
||||
}
|
||||
)
|
||||
await shell.openExternal(url);
|
||||
},
|
||||
);
|
||||
|
||||
// Open file
|
||||
ipcMain.handle(
|
||||
'msgOpenFile',
|
||||
async (event: IpcMainEvent, filter: string) => {
|
||||
const filters = []
|
||||
const filters = [];
|
||||
if (filter === 'text') {
|
||||
filters.push({ name: 'Text', extensions: ['txt', 'json'] })
|
||||
filters.push({ name: 'Text', extensions: ['txt', 'json'] });
|
||||
} else if (filter === 'zip') {
|
||||
filters.push({ name: 'Zip', extensions: ['zip'] })
|
||||
filters.push({ name: 'Zip', extensions: ['zip'] });
|
||||
} else if (filter === 'any') {
|
||||
filters.push({ name: 'All Files', extensions: ['*'] })
|
||||
filters.push({ name: 'All Files', extensions: ['*'] });
|
||||
}
|
||||
const dialogResult = await dialog.showOpenDialog({
|
||||
properties: ['openFile'],
|
||||
filters
|
||||
})
|
||||
return dialogResult
|
||||
}
|
||||
)
|
||||
filters,
|
||||
});
|
||||
return dialogResult;
|
||||
},
|
||||
);
|
||||
|
||||
// Select directory
|
||||
ipcMain.handle('msgSelectDirectory', async () => {
|
||||
const dialogResult = await dialog.showOpenDialog({
|
||||
properties: ['openDirectory']
|
||||
})
|
||||
properties: ['openDirectory'],
|
||||
});
|
||||
|
||||
if (!dialogResult.canceled && dialogResult.filePaths.length > 0) {
|
||||
return {
|
||||
canceled: false,
|
||||
filePath: dialogResult.filePaths[0],
|
||||
isDirectory: true
|
||||
}
|
||||
isDirectory: true,
|
||||
};
|
||||
}
|
||||
|
||||
return { canceled: true }
|
||||
})
|
||||
return { canceled: true };
|
||||
});
|
||||
|
||||
// Select file or directory
|
||||
ipcMain.handle('msgSelectFileOrFolder', async () => {
|
||||
const dialogResult = await dialog.showOpenDialog({
|
||||
properties: ['openFile', 'openDirectory']
|
||||
})
|
||||
properties: ['openFile', 'openDirectory'],
|
||||
});
|
||||
|
||||
if (!dialogResult.canceled && dialogResult.filePaths.length > 0) {
|
||||
const filePath = dialogResult.filePaths[0];
|
||||
const fs = require('fs');
|
||||
const fs = require('node:fs');
|
||||
const isDirectory = fs.statSync(filePath).isDirectory();
|
||||
|
||||
return {
|
||||
canceled: false,
|
||||
filePath: filePath,
|
||||
isDirectory: isDirectory
|
||||
}
|
||||
filePath,
|
||||
isDirectory,
|
||||
};
|
||||
}
|
||||
|
||||
return { canceled: true }
|
||||
})
|
||||
|
||||
|
||||
return { canceled: true };
|
||||
});
|
||||
|
||||
// Find all files in a folder recursively
|
||||
ipcMain.handle(
|
||||
'msgFindFilesInFolder',
|
||||
async (event: IpcMainEvent, folderPath: string) => {
|
||||
try {
|
||||
|
||||
const foundFiles: string[] = []
|
||||
const foundFiles: string[] = [];
|
||||
|
||||
const searchFiles = async (dir: string) => {
|
||||
const items = await readdir(dir)
|
||||
const items = await readdir(dir);
|
||||
|
||||
for (const item of items) {
|
||||
const fullPath = join(dir, item)
|
||||
const stats = await stat(fullPath)
|
||||
const fullPath = join(dir, item);
|
||||
const stats = await stat(fullPath);
|
||||
|
||||
if (stats.isDirectory()) {
|
||||
// Recursively search in subdirectories
|
||||
await searchFiles(fullPath)
|
||||
await searchFiles(fullPath);
|
||||
} else if (stats.isFile()) {
|
||||
// Add file to the list
|
||||
foundFiles.push(fullPath)
|
||||
foundFiles.push(fullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
await searchFiles(folderPath)
|
||||
await searchFiles(folderPath);
|
||||
|
||||
return foundFiles
|
||||
return foundFiles;
|
||||
} catch (error) {
|
||||
console.error('Error finding files in folder:', error)
|
||||
return []
|
||||
console.error('Error finding files in folder:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
// Load data from local file
|
||||
ipcMain.handle(
|
||||
'msgLoadData',
|
||||
async (event: IpcMainEvent, fileName: string) => {
|
||||
try {
|
||||
const userDataPath = join(Constants.APP_DATA_PATH, 'data')
|
||||
const filePath = join(userDataPath, fileName)
|
||||
const userDataPath = join(Constants.APP_DATA_PATH, 'data');
|
||||
const filePath = join(userDataPath, fileName);
|
||||
|
||||
// Ensure directory exists
|
||||
await ensureDir(userDataPath)
|
||||
await ensureDir(userDataPath);
|
||||
|
||||
if (await pathExists(filePath)) {
|
||||
const data = await readFile(filePath, 'utf-8')
|
||||
return data
|
||||
const data = await readFile(filePath, 'utf-8');
|
||||
return data;
|
||||
}
|
||||
return null
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('Error loading data:', error)
|
||||
return null
|
||||
console.error('Error loading data:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
// Save data to local file
|
||||
ipcMain.handle(
|
||||
'msgSaveData',
|
||||
async (event: IpcMainEvent, fileName: string, data: string) => {
|
||||
try {
|
||||
const userDataPath = join(Constants.APP_DATA_PATH, 'data')
|
||||
const filePath = join(userDataPath, fileName)
|
||||
const userDataPath = join(Constants.APP_DATA_PATH, 'data');
|
||||
const filePath = join(userDataPath, fileName);
|
||||
|
||||
// Ensure directory exists
|
||||
await ensureDir(userDataPath)
|
||||
await ensureDir(userDataPath);
|
||||
|
||||
await writeFile(filePath, data, 'utf-8')
|
||||
return true
|
||||
await writeFile(filePath, data, 'utf-8');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error saving data:', error)
|
||||
return false
|
||||
console.error('Error saving data:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
// Execute template
|
||||
ipcMain.handle(
|
||||
@@ -176,39 +174,41 @@ export default class IPCs {
|
||||
event: IpcMainEvent,
|
||||
templateId: string,
|
||||
targetPath: string,
|
||||
baseDirNames: string[]
|
||||
baseDirNames: string[],
|
||||
) => {
|
||||
try {
|
||||
// Load templates data
|
||||
const userDataPath = join(Constants.APP_DATA_PATH, 'data')
|
||||
const filePath = join(userDataPath, 'templates.json')
|
||||
const userDataPath = join(Constants.APP_DATA_PATH, 'data');
|
||||
const filePath = join(userDataPath, 'templates.json');
|
||||
|
||||
let templates: Template[] = []
|
||||
let templates: Template[] = [];
|
||||
if (await pathExists(filePath)) {
|
||||
const templatesData = await readFile(filePath, 'utf-8')
|
||||
templates = JSON.parse(templatesData)
|
||||
const templatesData = await readFile(filePath, 'utf-8');
|
||||
templates = JSON.parse(templatesData);
|
||||
}
|
||||
|
||||
const template = templates.find((t) => t.id === templateId)
|
||||
const template = templates.find(t => t.id === templateId);
|
||||
|
||||
if (!template) {
|
||||
return { success: false, error: 'Template not found' }
|
||||
return { success: false, error: 'Template not found' };
|
||||
}
|
||||
|
||||
// Create directories and files for each base directory name
|
||||
for (const baseDirName of baseDirNames) {
|
||||
if (!baseDirName.trim()) continue
|
||||
if (!baseDirName.trim()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Create base directory
|
||||
const baseDirPath = join(targetPath, baseDirName)
|
||||
await ensureDir(baseDirPath)
|
||||
const baseDirPath = join(targetPath, baseDirName);
|
||||
await ensureDir(baseDirPath);
|
||||
|
||||
// Helper function to traverse the tree and collect all nodes
|
||||
const collectAllNodes = (nodes: TreeNode[], allNodes: TreeNode[] = [], parent?: TreeNode) => {
|
||||
for (const node of nodes) {
|
||||
allNodes.push({
|
||||
...node,
|
||||
path: join(parent?.path || '', node.path)
|
||||
path: join(parent?.path || '', node.path),
|
||||
});
|
||||
if (node.children) {
|
||||
collectAllNodes(node.children, allNodes, node);
|
||||
@@ -235,7 +235,7 @@ export default class IPCs {
|
||||
await copy(node.sourcePath, nodePath);
|
||||
} else {
|
||||
// Write content directly
|
||||
const files = await readdir(node.sourcePath, { withFileTypes: true })
|
||||
const files = await readdir(node.sourcePath, { withFileTypes: true });
|
||||
for (const file of files) {
|
||||
if (file.isFile() && file.name.startsWith(baseDirName)) {
|
||||
const filePath = join(node.sourcePath, file.name);
|
||||
@@ -248,91 +248,91 @@ export default class IPCs {
|
||||
}
|
||||
}
|
||||
|
||||
return { success: true }
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('Error executing template:', error)
|
||||
return { success: false, error: error.message }
|
||||
console.error('Error executing template:', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
// 检查更新
|
||||
ipcMain.handle('msgCheckForUpdates', async () => {
|
||||
if (Constants.IS_DEV_ENV) {
|
||||
// 在开发环境中模拟更新检查
|
||||
console.log('开发环境中模拟检查更新...')
|
||||
return { updateAvailable: false, currentVersion: Constants.APP_VERSION }
|
||||
console.log('开发环境中模拟检查更新...');
|
||||
return { updateAvailable: false, currentVersion: Constants.APP_VERSION };
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
// 监听更新事件
|
||||
const removeListeners = () => {
|
||||
autoUpdater.removeListener('update-available', onUpdateAvailable)
|
||||
autoUpdater.removeListener('update-not-available', onUpdateNotAvailable)
|
||||
autoUpdater.removeListener('error', onError)
|
||||
}
|
||||
autoUpdater.removeListener('update-available', onUpdateAvailable);
|
||||
autoUpdater.removeListener('update-not-available', onUpdateNotAvailable);
|
||||
autoUpdater.removeListener('error', onError);
|
||||
};
|
||||
|
||||
const onUpdateAvailable = () => {
|
||||
removeListeners()
|
||||
resolve({ updateAvailable: true, currentVersion: Constants.APP_VERSION })
|
||||
}
|
||||
removeListeners();
|
||||
resolve({ updateAvailable: true, currentVersion: Constants.APP_VERSION });
|
||||
};
|
||||
|
||||
const onUpdateNotAvailable = () => {
|
||||
removeListeners()
|
||||
resolve({ updateAvailable: false, currentVersion: Constants.APP_VERSION })
|
||||
}
|
||||
removeListeners();
|
||||
resolve({ updateAvailable: false, currentVersion: Constants.APP_VERSION });
|
||||
};
|
||||
|
||||
const onError = (error: Error) => {
|
||||
removeListeners()
|
||||
console.error('检查更新时出错:', error)
|
||||
resolve({ updateAvailable: false, currentVersion: Constants.APP_VERSION, error: error.message })
|
||||
}
|
||||
removeListeners();
|
||||
console.error('检查更新时出错:', error);
|
||||
resolve({ updateAvailable: false, currentVersion: Constants.APP_VERSION, error: error.message });
|
||||
};
|
||||
|
||||
// 添加事件监听器
|
||||
autoUpdater.once('update-available', onUpdateAvailable)
|
||||
autoUpdater.once('update-not-available', onUpdateNotAvailable)
|
||||
autoUpdater.once('error', onError)
|
||||
autoUpdater.once('update-available', onUpdateAvailable);
|
||||
autoUpdater.once('update-not-available', onUpdateNotAvailable);
|
||||
autoUpdater.once('error', onError);
|
||||
|
||||
// 开始检查更新
|
||||
autoUpdater.checkForUpdates()
|
||||
})
|
||||
})
|
||||
autoUpdater.checkForUpdates();
|
||||
});
|
||||
});
|
||||
|
||||
// 下载更新
|
||||
ipcMain.handle('msgDownloadUpdate', async () => {
|
||||
if (Constants.IS_DEV_ENV) {
|
||||
// 在开发环境中模拟下载更新
|
||||
console.log('开发环境中模拟下载更新...')
|
||||
return { success: true }
|
||||
console.log('开发环境中模拟下载更新...');
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
try {
|
||||
await autoUpdater.downloadUpdate()
|
||||
return { success: true }
|
||||
await autoUpdater.downloadUpdate();
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('下载更新时出错:', error)
|
||||
return { success: false, error: error instanceof Error ? error.message : String(error) }
|
||||
console.error('下载更新时出错:', error);
|
||||
return { success: false, error: error instanceof Error ? error.message : String(error) };
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
// 安装更新
|
||||
ipcMain.handle('msgInstallUpdate', async () => {
|
||||
if (Constants.IS_DEV_ENV) {
|
||||
// 在开发环境中模拟安装更新
|
||||
console.log('开发环境中模拟安装更新...')
|
||||
return { success: true }
|
||||
console.log('开发环境中模拟安装更新...');
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
try {
|
||||
// 立即退出并安装更新
|
||||
setImmediate(() => {
|
||||
autoUpdater.quitAndInstall()
|
||||
})
|
||||
return { success: true }
|
||||
autoUpdater.quitAndInstall();
|
||||
});
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('安装更新时出错:', error)
|
||||
return { success: false, error: error instanceof Error ? error.message : String(error) }
|
||||
console.error('安装更新时出错:', error);
|
||||
return { success: false, error: error instanceof Error ? error.message : String(error) };
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
+61
-59
@@ -1,13 +1,16 @@
|
||||
import type {
|
||||
BrowserWindowConstructorOptions,
|
||||
RenderProcessGoneDetails,
|
||||
} from 'electron';
|
||||
import type { TrayOptions } from './utils/Constants';
|
||||
import {
|
||||
app,
|
||||
BrowserWindow,
|
||||
RenderProcessGoneDetails,
|
||||
BrowserWindowConstructorOptions
|
||||
} from 'electron'
|
||||
import Constants, { TrayOptions } from './utils/Constants'
|
||||
import IPCs from './IPCs'
|
||||
import { createTray, hideWindow, showWindow } from './tray'
|
||||
import log from 'electron-log/main'
|
||||
} from 'electron';
|
||||
import log from 'electron-log/main';
|
||||
import IPCs from './IPCs';
|
||||
import { createTray, hideWindow, showWindow } from './tray';
|
||||
import Constants from './utils/Constants';
|
||||
|
||||
const options = {
|
||||
width: Constants.IS_DEV_ENV ? 1500 : 1200,
|
||||
@@ -16,20 +19,20 @@ const options = {
|
||||
// all optional values from DEFAULT_TRAY_OPTIONS can de defined here
|
||||
enabled: true,
|
||||
menu: false, // true, to use a tray menu ; false to toggle visibility on click on tray icon
|
||||
trayWindow: false // true, to use a tray floating window attached to top try icon
|
||||
}
|
||||
}
|
||||
trayWindow: false, // true, to use a tray floating window attached to top try icon
|
||||
},
|
||||
};
|
||||
|
||||
const exitApp = (mainWindow: BrowserWindow): void => {
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.hide()
|
||||
mainWindow.hide();
|
||||
}
|
||||
mainWindow.destroy()
|
||||
app.exit()
|
||||
}
|
||||
mainWindow.destroy();
|
||||
app.exit();
|
||||
};
|
||||
|
||||
export const createMainWindow = async (): Promise<BrowserWindow> => {
|
||||
log.silly('Creating new window...')
|
||||
log.silly('Creating new window...');
|
||||
|
||||
let opt: BrowserWindowConstructorOptions = {
|
||||
title: Constants.APP_NAME,
|
||||
@@ -38,17 +41,17 @@ export const createMainWindow = async (): Promise<BrowserWindow> => {
|
||||
height: options.height,
|
||||
useContentSize: true,
|
||||
webPreferences: Constants.DEFAULT_WEB_PREFERENCES,
|
||||
frame: true
|
||||
}
|
||||
frame: true,
|
||||
};
|
||||
const trayOptions: TrayOptions = options.tray?.enabled
|
||||
? {
|
||||
...Constants.DEFAULT_TRAY_OPTIONS,
|
||||
...options.tray
|
||||
...options.tray,
|
||||
}
|
||||
: {
|
||||
...Constants.DEFAULT_TRAY_OPTIONS,
|
||||
enabled: false
|
||||
}
|
||||
enabled: false,
|
||||
};
|
||||
|
||||
// trayWindow requires tray.enabled=true
|
||||
if (trayOptions.enabled && trayOptions.trayWindow) {
|
||||
@@ -67,93 +70,92 @@ export const createMainWindow = async (): Promise<BrowserWindow> => {
|
||||
alwaysOnTop: true,
|
||||
webPreferences: {
|
||||
...Constants.DEFAULT_WEB_PREFERENCES,
|
||||
backgroundThrottling: false
|
||||
}
|
||||
}
|
||||
backgroundThrottling: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
const mainWindow = new BrowserWindow(opt)
|
||||
const mainWindow = new BrowserWindow(opt);
|
||||
|
||||
mainWindow.setMenu(null)
|
||||
mainWindow.setMenu(null);
|
||||
|
||||
mainWindow.on('close', (event: Event): void => {
|
||||
event.preventDefault()
|
||||
exitApp(mainWindow)
|
||||
})
|
||||
event.preventDefault();
|
||||
exitApp(mainWindow);
|
||||
});
|
||||
|
||||
mainWindow.webContents.on('did-frame-finish-load', (): void => {
|
||||
if (Constants.IS_DEV_ENV && Constants.FEAT_OPEN_DEV_TOOLS_AT_START) {
|
||||
mainWindow.webContents.openDevTools()
|
||||
mainWindow.webContents.openDevTools();
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
if (trayOptions.enabled) {
|
||||
createTray(mainWindow, trayOptions)
|
||||
createTray(mainWindow, trayOptions);
|
||||
}
|
||||
|
||||
if (trayOptions.enabled && trayOptions.trayWindow) {
|
||||
hideWindow(mainWindow)
|
||||
hideWindow(mainWindow);
|
||||
if (trayOptions.showAtStartup) {
|
||||
showWindow(mainWindow)
|
||||
showWindow(mainWindow);
|
||||
}
|
||||
} else {
|
||||
mainWindow.once('ready-to-show', (): void => {
|
||||
mainWindow.setAlwaysOnTop(true)
|
||||
mainWindow.show()
|
||||
mainWindow.focus()
|
||||
mainWindow.setAlwaysOnTop(false)
|
||||
})
|
||||
mainWindow.setAlwaysOnTop(true);
|
||||
mainWindow.show();
|
||||
mainWindow.focus();
|
||||
mainWindow.setAlwaysOnTop(false);
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize IPC Communication
|
||||
IPCs.initialize()
|
||||
IPCs.initialize();
|
||||
|
||||
if (Constants.IS_DEV_ENV) {
|
||||
await mainWindow.loadURL(Constants.APP_INDEX_URL_DEV)
|
||||
await mainWindow.loadURL(Constants.APP_INDEX_URL_DEV);
|
||||
} else {
|
||||
await mainWindow.loadFile(Constants.APP_INDEX_URL_PROD)
|
||||
await mainWindow.loadFile(Constants.APP_INDEX_URL_PROD);
|
||||
}
|
||||
|
||||
return mainWindow
|
||||
}
|
||||
return mainWindow;
|
||||
};
|
||||
|
||||
export const createErrorWindow = async (
|
||||
errorWindow: BrowserWindow,
|
||||
mainWindow: BrowserWindow,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
details?: RenderProcessGoneDetails
|
||||
_details?: RenderProcessGoneDetails,
|
||||
): Promise<BrowserWindow> => {
|
||||
if (!Constants.IS_DEV_ENV) {
|
||||
mainWindow?.hide()
|
||||
mainWindow?.hide();
|
||||
}
|
||||
|
||||
errorWindow = new BrowserWindow({
|
||||
title: Constants.APP_NAME,
|
||||
show: false,
|
||||
resizable: Constants.IS_DEV_ENV,
|
||||
webPreferences: Constants.DEFAULT_WEB_PREFERENCES
|
||||
})
|
||||
webPreferences: Constants.DEFAULT_WEB_PREFERENCES,
|
||||
});
|
||||
|
||||
errorWindow.setMenu(null)
|
||||
errorWindow.setMenu(null);
|
||||
|
||||
if (Constants.IS_DEV_ENV) {
|
||||
await errorWindow.loadURL(`${Constants.APP_INDEX_URL_DEV}#/error`)
|
||||
await errorWindow.loadURL(`${Constants.APP_INDEX_URL_DEV}#/error`);
|
||||
} else {
|
||||
await errorWindow.loadFile(Constants.APP_INDEX_URL_PROD, { hash: 'error' })
|
||||
await errorWindow.loadFile(Constants.APP_INDEX_URL_PROD, { hash: 'error' });
|
||||
}
|
||||
|
||||
errorWindow.on('ready-to-show', (): void => {
|
||||
if (!Constants.IS_DEV_ENV && mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.destroy()
|
||||
mainWindow.destroy();
|
||||
}
|
||||
errorWindow.show()
|
||||
errorWindow.focus()
|
||||
})
|
||||
errorWindow.show();
|
||||
errorWindow.focus();
|
||||
});
|
||||
|
||||
errorWindow.webContents.on('did-frame-finish-load', (): void => {
|
||||
if (Constants.IS_DEV_ENV) {
|
||||
errorWindow.webContents.openDevTools()
|
||||
errorWindow.webContents.openDevTools();
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
return errorWindow
|
||||
}
|
||||
return errorWindow;
|
||||
};
|
||||
@@ -1,22 +1,22 @@
|
||||
import { app } from 'electron';
|
||||
// Warning: This file is only used in the development environment
|
||||
// and is removed at build time.
|
||||
// Do not edit the file unless necessary.
|
||||
import { installExtension } from 'electron-extension-installer'
|
||||
import { app } from 'electron'
|
||||
import { installExtension } from 'electron-extension-installer';
|
||||
|
||||
const vue3DevTools = {
|
||||
id: 'nhdogjmejiglipccpnnnanhbledajbpd',
|
||||
version: '7.7.0'
|
||||
}
|
||||
version: '7.7.0',
|
||||
};
|
||||
|
||||
try {
|
||||
app.on('ready', async () => {
|
||||
await installExtension(vue3DevTools, {
|
||||
loadExtensionOptions: {
|
||||
allowFileAccess: true
|
||||
}
|
||||
})
|
||||
})
|
||||
allowFileAccess: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
} catch {
|
||||
// Do nothing
|
||||
}
|
||||
+41
-41
@@ -1,46 +1,46 @@
|
||||
import { app, WebContents, RenderProcessGoneDetails } from 'electron'
|
||||
import Constants from './utils/Constants'
|
||||
import { createErrorWindow, createMainWindow } from './MainRunner'
|
||||
import log from 'electron-log/main'
|
||||
import { join } from 'path'
|
||||
import { AutoUpdater } from './AutoUpdater'
|
||||
import type { RenderProcessGoneDetails, WebContents } from 'electron';
|
||||
import { join } from 'node:path';
|
||||
import { app } from 'electron';
|
||||
import log from 'electron-log/main';
|
||||
import { AutoUpdater } from './AutoUpdater';
|
||||
import { createErrorWindow, createMainWindow } from './MainRunner';
|
||||
import Constants from './utils/Constants';
|
||||
|
||||
let mainWindow
|
||||
let errorWindow
|
||||
let autoUpdater: AutoUpdater | null = null
|
||||
let mainWindow;
|
||||
let errorWindow;
|
||||
|
||||
const initializeMainLogger = () => {
|
||||
log.initialize({
|
||||
includeFutureSessions: false,
|
||||
preload: true
|
||||
})
|
||||
preload: true,
|
||||
});
|
||||
|
||||
const appLogFilePath = join(app.getPath('userData'), 'logs', 'applog.log')
|
||||
const appLogFilePath = join(app.getPath('userData'), 'logs', 'applog.log');
|
||||
|
||||
log.transports.file.resolvePathFn = () =>
|
||||
join(app.getPath('userData'), 'logs', 'applog.log')
|
||||
log.transports.file.level = 'silly'
|
||||
log.transports.file.format = '[{y}{m}{d} {h}:{i}:{s}.{ms}|{level}]{text}'
|
||||
log.transports.console.format = '{h}:{i}:{s}.{ms} {text}'
|
||||
log.transports.console.level = 'silly'
|
||||
join(app.getPath('userData'), 'logs', 'applog.log');
|
||||
log.transports.file.level = 'silly';
|
||||
log.transports.file.format = '[{y}{m}{d} {h}:{i}:{s}.{ms}|{level}]{text}';
|
||||
log.transports.console.format = '{h}:{i}:{s}.{ms} {text}';
|
||||
log.transports.console.level = 'silly';
|
||||
|
||||
log.silly(`Start logging... (Path: ${appLogFilePath}) App is ready.`)
|
||||
}
|
||||
log.silly(`Start logging... (Path: ${appLogFilePath}) App is ready.`);
|
||||
};
|
||||
|
||||
app.on('ready', async () => {
|
||||
if (Constants.IS_DEV_ENV) {
|
||||
import('./index.dev')
|
||||
|
||||
import('./index.dev');
|
||||
|
||||
// Install Vue DevTools
|
||||
try {
|
||||
const { default: installExtension, VUEJS_DEVTOOLS } = await import('electron-devtools-installer')
|
||||
const { default: installExtension, VUEJS_DEVTOOLS } = await import('electron-devtools-installer');
|
||||
await installExtension(VUEJS_DEVTOOLS, {
|
||||
loadExtensionOptions: {
|
||||
allowFileAccess: true,
|
||||
},
|
||||
})
|
||||
});
|
||||
} catch (err) {
|
||||
console.log('Failed to install Vue DevTools: ', err)
|
||||
console.log('Failed to install Vue DevTools: ', err);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,42 +51,42 @@ app.on('ready', async () => {
|
||||
systemPreferences.setUserDefault('NSDisabledCharacterPaletteMenuItem', 'boolean', true)
|
||||
}
|
||||
*/
|
||||
initializeMainLogger()
|
||||
initializeMainLogger();
|
||||
|
||||
mainWindow = await createMainWindow();
|
||||
|
||||
mainWindow = await createMainWindow()
|
||||
|
||||
// Initialize auto updater after main window is created
|
||||
if (!Constants.IS_DEV_ENV) {
|
||||
autoUpdater = new AutoUpdater(mainWindow)
|
||||
autoUpdater = new AutoUpdater(mainWindow);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
app.on('activate', async () => {
|
||||
if (!mainWindow) {
|
||||
mainWindow = await createMainWindow()
|
||||
mainWindow = await createMainWindow();
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
mainWindow = null
|
||||
errorWindow = null
|
||||
mainWindow = null;
|
||||
errorWindow = null;
|
||||
|
||||
if (!Constants.IS_MAC) {
|
||||
app.quit()
|
||||
app.quit();
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
app.on(
|
||||
'render-process-gone',
|
||||
(
|
||||
event: Event,
|
||||
webContents: WebContents,
|
||||
details: RenderProcessGoneDetails
|
||||
details: RenderProcessGoneDetails,
|
||||
) => {
|
||||
errorWindow = createErrorWindow(errorWindow, mainWindow, details)
|
||||
}
|
||||
)
|
||||
errorWindow = createErrorWindow(errorWindow, mainWindow, details);
|
||||
},
|
||||
);
|
||||
|
||||
process.on('uncaughtException', () => {
|
||||
errorWindow = createErrorWindow(errorWindow, mainWindow)
|
||||
})
|
||||
errorWindow = createErrorWindow(errorWindow, mainWindow);
|
||||
});
|
||||
+54
-50
@@ -1,111 +1,115 @@
|
||||
import { app, screen, Menu, Tray, BrowserWindow } from 'electron'
|
||||
import Constants from './utils/Constants'
|
||||
import { join } from 'path'
|
||||
import { debounce } from 'qsu'
|
||||
let tray
|
||||
let trayOptions
|
||||
import type { BrowserWindow } from 'electron';
|
||||
import { join } from 'node:path';
|
||||
import { app, Menu, screen, Tray } from 'electron';
|
||||
import { debounce } from 'qsu';
|
||||
import Constants from './utils/Constants';
|
||||
|
||||
let tray;
|
||||
let trayOptions;
|
||||
|
||||
export function createTray(window: BrowserWindow, options) {
|
||||
trayOptions = options || Constants.DEFAULT_TRAY_OPTIONS
|
||||
trayOptions = options || Constants.DEFAULT_TRAY_OPTIONS;
|
||||
// menu or trayWindow, you need to choose
|
||||
if (trayOptions.trayWindow) {
|
||||
trayOptions.menu = false
|
||||
trayOptions.menu = false;
|
||||
}
|
||||
|
||||
tray = new Tray(join(Constants.PUBLIC_PATH, 'images/vutron-tray-icon.png'))
|
||||
tray.setToolTip(trayOptions.tooltip)
|
||||
tray = new Tray(join(Constants.PUBLIC_PATH, 'images/vutron-tray-icon.png'));
|
||||
tray.setToolTip(trayOptions.tooltip);
|
||||
if (trayOptions.menu) {
|
||||
const contextMenu = Menu.buildFromTemplate([
|
||||
{
|
||||
label: 'Show App',
|
||||
click: () => {
|
||||
showWindow(window)
|
||||
}
|
||||
showWindow(window);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Hide App',
|
||||
click: () => {
|
||||
hideWindow(window)
|
||||
}
|
||||
hideWindow(window);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Exit',
|
||||
click: () => {
|
||||
app.quit()
|
||||
}
|
||||
}
|
||||
])
|
||||
app.quit();
|
||||
},
|
||||
},
|
||||
]);
|
||||
// tray icon only with classic window
|
||||
tray.setContextMenu(contextMenu)
|
||||
tray.setContextMenu(contextMenu);
|
||||
} else {
|
||||
// handle click on tray icon
|
||||
tray.on('right-click', function () {
|
||||
debounce(() => toggleWindow(window), 200)
|
||||
})
|
||||
tray.on('click', function () {
|
||||
debounce(() => toggleWindow(window), 200)
|
||||
})
|
||||
tray.on('right-click', () => {
|
||||
debounce(() => toggleWindow(window), 200);
|
||||
});
|
||||
tray.on('click', () => {
|
||||
debounce(() => toggleWindow(window), 200);
|
||||
});
|
||||
// no menu for tray window
|
||||
window.setMenu(null)
|
||||
tray.setContextMenu(null)
|
||||
window.setMenu(null);
|
||||
tray.setContextMenu(null);
|
||||
}
|
||||
// align at startup
|
||||
alignWindow(window)
|
||||
return tray
|
||||
alignWindow(window);
|
||||
return tray;
|
||||
}
|
||||
|
||||
export function hideWindow(window: BrowserWindow) {
|
||||
window.hide()
|
||||
window.hide();
|
||||
// if (!trayOptions.trayWindow) return;
|
||||
// hide window when click elsewhere on screen
|
||||
window.on('blur', () => {
|
||||
// dont close if devtools
|
||||
if (!window.webContents.isDevToolsOpened()) {
|
||||
window.hide()
|
||||
window.hide();
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
export function toggleWindow(window: BrowserWindow) {
|
||||
if (window.isVisible()) {
|
||||
hideWindow(window)
|
||||
hideWindow(window);
|
||||
} else {
|
||||
showWindow(window)
|
||||
showWindow(window);
|
||||
}
|
||||
}
|
||||
|
||||
export function showWindow(window: BrowserWindow) {
|
||||
window.show()
|
||||
alignWindow(window)
|
||||
window.show();
|
||||
alignWindow(window);
|
||||
}
|
||||
|
||||
export function alignWindow(window: BrowserWindow) {
|
||||
if (!trayOptions.trayWindow) return
|
||||
if (!trayOptions.trayWindow) {
|
||||
return;
|
||||
}
|
||||
|
||||
const b = window.getBounds()
|
||||
const position = calculateWindowPosition(b)
|
||||
const b = window.getBounds();
|
||||
const position = calculateWindowPosition(b);
|
||||
window.setBounds({
|
||||
width: b.width,
|
||||
height: b.height,
|
||||
x: position.x,
|
||||
y: position.y
|
||||
})
|
||||
y: position.y,
|
||||
});
|
||||
}
|
||||
|
||||
function calculateWindowPosition(b) {
|
||||
const margin = trayOptions.margin
|
||||
const screenBounds = screen.getPrimaryDisplay().size
|
||||
const trayBounds = tray.getBounds()
|
||||
const bottom = trayBounds.y > screenBounds.height / 2
|
||||
const margin = trayOptions.margin;
|
||||
const screenBounds = screen.getPrimaryDisplay().size;
|
||||
const trayBounds = tray.getBounds();
|
||||
const bottom = trayBounds.y > screenBounds.height / 2;
|
||||
const x = Math.floor(
|
||||
trayBounds.x - b.width / 2 - margin.x + trayBounds.width / 2
|
||||
)
|
||||
trayBounds.x - b.width / 2 - margin.x + trayBounds.width / 2,
|
||||
);
|
||||
const y = bottom
|
||||
? Math.floor(trayBounds.y - b.height - margin.y + trayBounds.height / 2)
|
||||
: Math.floor(trayBounds.y + margin.y + trayBounds.height / 2)
|
||||
: Math.floor(trayBounds.y + margin.y + trayBounds.height / 2);
|
||||
// constraint into screen
|
||||
return {
|
||||
x: Math.max(0, Math.min(screenBounds.width - b.width, x)),
|
||||
y: Math.max(0, Math.min(screenBounds.height - b.height, y))
|
||||
}
|
||||
y: Math.max(0, Math.min(screenBounds.height - b.height, y)),
|
||||
};
|
||||
}
|
||||
+24
-24
@@ -1,17 +1,17 @@
|
||||
import { join, dirname, resolve } from 'path'
|
||||
import { name, version, debug } from '../../../package.json'
|
||||
import { fileURLToPath } from 'url'
|
||||
import { app } from 'electron'
|
||||
import { dirname, join, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { app } from 'electron';
|
||||
import { debug, name, version } from '../../../package.json';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
export interface TrayOptions {
|
||||
enabled: boolean
|
||||
trayWindow: boolean
|
||||
menu: boolean
|
||||
tooltip: string
|
||||
margin: { x: number; y: number }
|
||||
showAtStartup: boolean
|
||||
enabled: boolean;
|
||||
trayWindow: boolean;
|
||||
menu: boolean;
|
||||
tooltip: string;
|
||||
margin: { x: number; y: number };
|
||||
showAtStartup: boolean;
|
||||
}
|
||||
|
||||
export default class Constants {
|
||||
@@ -23,32 +23,32 @@ export default class Constants {
|
||||
* ------------------------------------------------------ */
|
||||
// To show devtools at startup. It requires IS_DEV_ENV=true.
|
||||
// Note: For debugging purpose, window won't be closed if click elsewhere, if devtools is open.
|
||||
static FEAT_OPEN_DEV_TOOLS_AT_START = true
|
||||
static FEAT_OPEN_DEV_TOOLS_AT_START = true;
|
||||
/* ------------------------------------------------------
|
||||
* END OF FEATURES
|
||||
* ------------------------------------------------------ */
|
||||
|
||||
// Display app name (uppercase first letter)
|
||||
static APP_NAME = name.charAt(0).toUpperCase() + name.slice(1)
|
||||
static APP_NAME = name.charAt(0).toUpperCase() + name.slice(1);
|
||||
|
||||
static APP_VERSION = version
|
||||
static APP_VERSION = version;
|
||||
|
||||
static IS_DEV_ENV = process.env.NODE_ENV === 'development'
|
||||
static IS_DEV_ENV = process.env.NODE_ENV === 'development';
|
||||
|
||||
static APP_DATA_PATH = app.getPath('userData')
|
||||
static APP_DATA_PATH = app.getPath('userData');
|
||||
|
||||
static PUBLIC_PATH = Constants.IS_DEV_ENV
|
||||
? resolve(__dirname, '../../src/public')
|
||||
: resolve(__dirname, '..')
|
||||
: resolve(__dirname, '..');
|
||||
|
||||
static IS_MAC = process.platform === 'darwin'
|
||||
static IS_MAC = process.platform === 'darwin';
|
||||
|
||||
static DEFAULT_WEB_PREFERENCES = {
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
enableRemoteModule: false,
|
||||
preload: join(__dirname, '../preload/index.js')
|
||||
}
|
||||
preload: join(__dirname, '../preload/index.js'),
|
||||
};
|
||||
|
||||
static DEFAULT_TRAY_OPTIONS: TrayOptions = {
|
||||
enabled: false,
|
||||
@@ -56,9 +56,9 @@ export default class Constants {
|
||||
menu: false,
|
||||
tooltip: 'Vutron App',
|
||||
margin: { x: 0, y: 0 },
|
||||
showAtStartup: false
|
||||
}
|
||||
showAtStartup: false,
|
||||
};
|
||||
|
||||
static APP_INDEX_URL_DEV = `${debug.env.VITE_DEV_SERVER_URL}/index.html`
|
||||
static APP_INDEX_URL_PROD = join(__dirname, '../index.html')
|
||||
static APP_INDEX_URL_DEV = `${debug.env.VITE_DEV_SERVER_URL}/index.html`;
|
||||
static APP_INDEX_URL_PROD = join(__dirname, '../index.html');
|
||||
}
|
||||
+28
-27
@@ -1,9 +1,10 @@
|
||||
import log from 'electron-log/renderer'
|
||||
import { contextBridge, ipcRenderer, IpcRendererEvent } from 'electron'
|
||||
import type { IpcRendererEvent } from 'electron';
|
||||
import { contextBridge, ipcRenderer } from 'electron';
|
||||
import log from 'electron-log/renderer';
|
||||
|
||||
// Initialize renderer logger
|
||||
log.transports.console.level = 'silly'
|
||||
log.transports.console.format = '{h}:{i}:{s}.{ms} {text}'
|
||||
log.transports.console.level = 'silly';
|
||||
log.transports.console.format = '{h}:{i}:{s}.{ms} {text}';
|
||||
|
||||
// Whitelist of valid channels used for IPC communication (Send message from Renderer to Main)
|
||||
const mainAvailChannels: string[] = [
|
||||
@@ -18,70 +19,70 @@ const mainAvailChannels: string[] = [
|
||||
'msgFindFilesInFolder',
|
||||
'msgCheckForUpdates',
|
||||
'msgDownloadUpdate',
|
||||
'msgInstallUpdate'
|
||||
]
|
||||
'msgInstallUpdate',
|
||||
];
|
||||
const rendererAvailChannels: string[] = [
|
||||
'update-checking',
|
||||
'update-available',
|
||||
'update-not-available',
|
||||
'update-download-progress',
|
||||
'update-downloaded',
|
||||
'update-error'
|
||||
]
|
||||
'update-error',
|
||||
];
|
||||
|
||||
contextBridge.exposeInMainWorld('mainApi', {
|
||||
send: (channel: string, ...data: any[]): void => {
|
||||
if (mainAvailChannels.includes(channel)) {
|
||||
ipcRenderer.send.apply(null, [channel, ...data])
|
||||
ipcRenderer.send.apply(null, [channel, ...data]);
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
log.silly(`[IPC_SEND::${channel}]`, { request: data })
|
||||
log.silly(`[IPC_SEND::${channel}]`, { request: data });
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Unknown ipc channel name: ${channel}`)
|
||||
throw new Error(`Unknown ipc channel name: ${channel}`);
|
||||
}
|
||||
},
|
||||
on: (
|
||||
channel: string,
|
||||
listener: (event: IpcRendererEvent, ...args: any[]) => void
|
||||
listener: (event: IpcRendererEvent, ...args: any[]) => void,
|
||||
): void => {
|
||||
if (rendererAvailChannels.includes(channel)) {
|
||||
ipcRenderer.on(channel, listener)
|
||||
ipcRenderer.on(channel, listener);
|
||||
} else {
|
||||
throw new Error(`Unknown ipc channel name: ${channel}`)
|
||||
throw new Error(`Unknown ipc channel name: ${channel}`);
|
||||
}
|
||||
},
|
||||
once: (
|
||||
channel: string,
|
||||
listener: (event: IpcRendererEvent, ...args: any[]) => void
|
||||
listener: (event: IpcRendererEvent, ...args: any[]) => void,
|
||||
): void => {
|
||||
if (rendererAvailChannels.includes(channel)) {
|
||||
ipcRenderer.once(channel, listener)
|
||||
ipcRenderer.once(channel, listener);
|
||||
} else {
|
||||
throw new Error(`Unknown ipc channel name: ${channel}`)
|
||||
throw new Error(`Unknown ipc channel name: ${channel}`);
|
||||
}
|
||||
},
|
||||
off: (
|
||||
channel: string,
|
||||
listener: (event: IpcRendererEvent, ...args: any[]) => void
|
||||
listener: (event: IpcRendererEvent, ...args: any[]) => void,
|
||||
): void => {
|
||||
if (rendererAvailChannels.includes(channel)) {
|
||||
ipcRenderer.off(channel, listener)
|
||||
ipcRenderer.off(channel, listener);
|
||||
} else {
|
||||
throw new Error(`Unknown ipc channel name: ${channel}`)
|
||||
throw new Error(`Unknown ipc channel name: ${channel}`);
|
||||
}
|
||||
},
|
||||
invoke: async (channel: string, ...data: any[]): Promise<any> => {
|
||||
if (mainAvailChannels.includes(channel)) {
|
||||
const result = await ipcRenderer.invoke.apply(null, [channel, ...data])
|
||||
const result = await ipcRenderer.invoke.apply(null, [channel, ...data]);
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
log.silly(`[IPC_INVOKE::${channel}]`, {
|
||||
request: data,
|
||||
result
|
||||
})
|
||||
result,
|
||||
});
|
||||
}
|
||||
return result
|
||||
return result;
|
||||
}
|
||||
|
||||
throw new Error(`Unknown ipc channel name: ${channel}`)
|
||||
}
|
||||
})
|
||||
throw new Error(`Unknown ipc channel name: ${channel}`);
|
||||
},
|
||||
});
|
||||
+32
-32
@@ -1,73 +1,73 @@
|
||||
<script setup lang="tsx">
|
||||
import { DefaultLayout } from '@/renderer/components/layout'
|
||||
import GlobalDialog from '@/renderer/components/GlobalDialog.vue'
|
||||
import UpdateDialog from '@/renderer/components/UpdateDialog.vue'
|
||||
import GlobalSnackbar from '@/renderer/components/GlobalSnackbar.vue'
|
||||
import { onMounted } from 'vue'
|
||||
import { useAppStore } from '@/renderer/store/app'
|
||||
import { onMounted } from 'vue';
|
||||
import GlobalDialog from '@/renderer/components/GlobalDialog.vue';
|
||||
import GlobalSnackbar from '@/renderer/components/GlobalSnackbar.vue';
|
||||
import { DefaultLayout } from '@/renderer/components/layout';
|
||||
import UpdateDialog from '@/renderer/components/UpdateDialog.vue';
|
||||
import { useAppStore } from '@/renderer/store/app';
|
||||
|
||||
// 使用应用商店来管理应用状态
|
||||
const appStore = useAppStore()
|
||||
const appStore = useAppStore();
|
||||
|
||||
onMounted(() => {
|
||||
// 在应用启动时检查更新
|
||||
setTimeout(() => {
|
||||
checkForUpdates()
|
||||
}, 3000) // 延迟3秒后检查更新,避免影响应用启动速度
|
||||
checkForUpdates();
|
||||
}, 3000); // 延迟3秒后检查更新,避免影响应用启动速度
|
||||
|
||||
// 监听主进程发来的更新相关事件
|
||||
window.mainApi.on('update-available', (_, info) => {
|
||||
appStore.setUpdateStatus('available', info.version)
|
||||
})
|
||||
appStore.setUpdateStatus('available', info.version);
|
||||
});
|
||||
|
||||
window.mainApi.on('update-not-available', (_, info) => {
|
||||
appStore.setUpdateStatus('not-available', info.version)
|
||||
})
|
||||
appStore.setUpdateStatus('not-available', info.version);
|
||||
});
|
||||
|
||||
window.mainApi.on('update-error', (_, error) => {
|
||||
appStore.setUpdateStatus('error', undefined, error.message || String(error))
|
||||
})
|
||||
appStore.setUpdateStatus('error', undefined, error.message || String(error));
|
||||
});
|
||||
|
||||
window.mainApi.on('update-download-progress', (_, progress) => {
|
||||
// 这个事件通过pinia状态管理,不需要在这里单独处理
|
||||
console.log('下载进度:', progress)
|
||||
})
|
||||
console.log('下载进度:', progress);
|
||||
});
|
||||
|
||||
window.mainApi.on('update-downloaded', (_, info) => {
|
||||
appStore.setUpdateStatus('downloaded')
|
||||
console.log('更新已下载,准备安装:', info)
|
||||
})
|
||||
})
|
||||
appStore.setUpdateStatus('downloaded');
|
||||
console.log('更新已下载,准备安装:', info);
|
||||
});
|
||||
});
|
||||
|
||||
const checkForUpdates = async () => {
|
||||
try {
|
||||
// 显示检查更新状态
|
||||
appStore.setUpdateStatus('checking')
|
||||
appStore.setUpdateStatus('checking');
|
||||
|
||||
// 调用主进程检查更新
|
||||
const result = await window.mainApi.invoke('msgCheckForUpdates')
|
||||
const result = await window.mainApi.invoke('msgCheckForUpdates');
|
||||
|
||||
if (result.error) {
|
||||
console.error('检查更新时出错:', result.error)
|
||||
appStore.setUpdateStatus('error', result.error)
|
||||
return
|
||||
console.error('检查更新时出错:', result.error);
|
||||
appStore.setUpdateStatus('error', result.error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.updateAvailable) {
|
||||
// 发现新版本,显示更新提示
|
||||
appStore.setUpdateStatus('available', result.currentVersion)
|
||||
appStore.setUpdateStatus('available', result.currentVersion);
|
||||
} else {
|
||||
// 当前已是最新版本
|
||||
appStore.setUpdateStatus('not-available', result.currentVersion)
|
||||
appStore.setUpdateStatus('not-available', result.currentVersion);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('检查更新时出错:', error)
|
||||
console.error('检查更新时出错:', error);
|
||||
appStore.setUpdateStatus(
|
||||
'error',
|
||||
error instanceof Error ? error.message : String(error)
|
||||
)
|
||||
error instanceof Error ? error.message : String(error),
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,3 +1,56 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
// 定义 props
|
||||
interface Props {
|
||||
modelValue: boolean;
|
||||
title?: string;
|
||||
message: string;
|
||||
confirmText?: string;
|
||||
cancelText?: string;
|
||||
confirmColor?: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
title: '确认',
|
||||
confirmText: '确认',
|
||||
cancelText: '取消',
|
||||
confirmColor: 'primary',
|
||||
});
|
||||
|
||||
// 定义 emits
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: boolean];
|
||||
'confirm': [];
|
||||
'cancel': [];
|
||||
}>();
|
||||
|
||||
// 本地状态
|
||||
const dialogVisible = ref(props.modelValue);
|
||||
|
||||
// 监听 modelValue 的变化
|
||||
watch(() => props.modelValue, (newValue) => {
|
||||
dialogVisible.value = newValue;
|
||||
});
|
||||
|
||||
// 监听本地状态变化并同步到父组件
|
||||
watch(dialogVisible, (newValue) => {
|
||||
emit('update:modelValue', newValue);
|
||||
});
|
||||
|
||||
// 处理确认事件
|
||||
const handleConfirm = () => {
|
||||
emit('confirm');
|
||||
dialogVisible.value = false;
|
||||
};
|
||||
|
||||
// 处理取消事件
|
||||
const handleCancel = () => {
|
||||
emit('cancel');
|
||||
dialogVisible.value = false;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-dialog
|
||||
v-model="dialogVisible"
|
||||
@@ -12,17 +65,17 @@
|
||||
{{ message }}
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
@click="handleCancel"
|
||||
variant="text"
|
||||
@click="handleCancel"
|
||||
>
|
||||
{{ cancelText }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
@click="handleConfirm"
|
||||
:color="confirmColor"
|
||||
variant="flat"
|
||||
@click="handleConfirm"
|
||||
>
|
||||
{{ confirmText }}
|
||||
</v-btn>
|
||||
@@ -30,56 +83,3 @@
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
// 定义 props
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
title?: string
|
||||
message: string
|
||||
confirmText?: string
|
||||
cancelText?: string
|
||||
confirmColor?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
title: '确认',
|
||||
confirmText: '确认',
|
||||
cancelText: '取消',
|
||||
confirmColor: 'primary'
|
||||
})
|
||||
|
||||
// 定义 emits
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: boolean]
|
||||
confirm: []
|
||||
cancel: []
|
||||
}>()
|
||||
|
||||
// 本地状态
|
||||
const dialogVisible = ref(props.modelValue)
|
||||
|
||||
// 监听 modelValue 的变化
|
||||
watch(() => props.modelValue, (newValue) => {
|
||||
dialogVisible.value = newValue
|
||||
})
|
||||
|
||||
// 监听本地状态变化并同步到父组件
|
||||
watch(dialogVisible, (newValue) => {
|
||||
emit('update:modelValue', newValue)
|
||||
})
|
||||
|
||||
// 处理确认事件
|
||||
const handleConfirm = () => {
|
||||
emit('confirm')
|
||||
dialogVisible.value = false
|
||||
}
|
||||
|
||||
// 处理取消事件
|
||||
const handleCancel = () => {
|
||||
emit('cancel')
|
||||
dialogVisible.value = false
|
||||
}
|
||||
</script>
|
||||
@@ -1,3 +1,86 @@
|
||||
<script setup lang="ts">
|
||||
import type { TreeNode } from '@/types';
|
||||
import { nextTick, ref, watch } from 'vue';
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean;
|
||||
isEditMode?: boolean;
|
||||
parentDirectory?: TreeNode | null;
|
||||
editingItem?: TreeNode | null;
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: boolean): void;
|
||||
(e: 'confirm', data: { path: string }): void;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
isEditMode: false,
|
||||
parentDirectory: null,
|
||||
editingItem: null,
|
||||
});
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
// Internal state
|
||||
const isVisible = ref(false);
|
||||
const formValid = ref(false);
|
||||
const directoryPath = ref('');
|
||||
const inputRef = ref<HTMLInputElement | null>(null);
|
||||
|
||||
// Computed
|
||||
const isEditMode = computed(() => props.isEditMode);
|
||||
|
||||
// Watch for visibility changes
|
||||
watch(() => props.modelValue, (newValue) => {
|
||||
isVisible.value = newValue;
|
||||
if (newValue) {
|
||||
// Reset form when dialog opens
|
||||
directoryPath.value = props.editingItem?.path || '';
|
||||
autoFocusInput();
|
||||
}
|
||||
});
|
||||
|
||||
// Watch for internal visibility changes and emit update
|
||||
watch(isVisible, (newValue) => {
|
||||
emit('update:modelValue', newValue);
|
||||
});
|
||||
|
||||
// Validation rules
|
||||
const requiredRule = (fieldName: string) => (v: string | null | undefined) =>
|
||||
!!v || `${fieldName}为必填项`;
|
||||
|
||||
// Methods
|
||||
const autoFocusInput = async () => {
|
||||
await nextTick();
|
||||
if (inputRef.value) {
|
||||
;(inputRef.value as HTMLInputElement).focus();
|
||||
}
|
||||
};
|
||||
|
||||
const confirm = () => {
|
||||
if (!directoryPath.value.trim()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Construct the full path if there's a parent directory
|
||||
|
||||
emit('confirm', {
|
||||
path: directoryPath.value,
|
||||
});
|
||||
|
||||
close();
|
||||
};
|
||||
const reset = () => {
|
||||
directoryPath.value = '';
|
||||
formValid.value = false;
|
||||
};
|
||||
|
||||
const close = () => {
|
||||
isVisible.value = false;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-dialog
|
||||
v-model="isVisible"
|
||||
@@ -11,24 +94,26 @@
|
||||
<v-card-text>
|
||||
<v-form v-model="formValid">
|
||||
<v-text-field
|
||||
ref="inputRef"
|
||||
v-model="directoryPath"
|
||||
label="目录名称"
|
||||
placeholder="请输入目录名称"
|
||||
:rules="[requiredRule('目录名称')]"
|
||||
required
|
||||
density="comfortable"
|
||||
ref="inputRef"
|
||||
@keydown.enter="confirm"
|
||||
/>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn @click="close">取消</v-btn>
|
||||
<v-btn @click="close">
|
||||
取消
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="confirm"
|
||||
:disabled="!formValid"
|
||||
@click="confirm"
|
||||
>
|
||||
{{ isEditMode ? '保存' : '添加' }}
|
||||
</v-btn>
|
||||
@@ -36,84 +121,3 @@
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, nextTick } from 'vue'
|
||||
import { TreeNode } from '@/types'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
isEditMode?: boolean
|
||||
parentDirectory?: TreeNode | null
|
||||
editingItem?: TreeNode | null
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
(e: 'confirm', data: { path: string; }): void
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
isEditMode: false,
|
||||
parentDirectory: null,
|
||||
editingItem: null
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
// Internal state
|
||||
const isVisible = ref(false)
|
||||
const formValid = ref(false)
|
||||
const directoryPath = ref('')
|
||||
const inputRef = ref<HTMLInputElement | null>(null)
|
||||
|
||||
// Computed
|
||||
const isEditMode = computed(() => props.isEditMode)
|
||||
|
||||
// Watch for visibility changes
|
||||
watch(() => props.modelValue, (newValue) => {
|
||||
isVisible.value = newValue
|
||||
if (newValue) {
|
||||
// Reset form when dialog opens
|
||||
directoryPath.value = props.editingItem?.path || ''
|
||||
autoFocusInput()
|
||||
}
|
||||
})
|
||||
|
||||
// Watch for internal visibility changes and emit update
|
||||
watch(isVisible, (newValue) => {
|
||||
emit('update:modelValue', newValue)
|
||||
})
|
||||
|
||||
// Validation rules
|
||||
const requiredRule = (fieldName: string) => (v: string | null | undefined) =>
|
||||
!!v || `${fieldName}为必填项`
|
||||
|
||||
// Methods
|
||||
const autoFocusInput = async () => {
|
||||
await nextTick()
|
||||
if (inputRef.value) {
|
||||
;(inputRef.value as HTMLInputElement).focus()
|
||||
}
|
||||
}
|
||||
|
||||
const confirm = () => {
|
||||
if (!directoryPath.value.trim()) return
|
||||
|
||||
// Construct the full path if there's a parent directory
|
||||
|
||||
emit('confirm', {
|
||||
path: directoryPath.value,
|
||||
})
|
||||
|
||||
close()
|
||||
}
|
||||
const reset = () => {
|
||||
directoryPath.value = ''
|
||||
formValid.value = false
|
||||
}
|
||||
|
||||
const close = () => {
|
||||
isVisible.value = false
|
||||
}
|
||||
</script>
|
||||
@@ -1,3 +1,125 @@
|
||||
<script setup lang="ts">
|
||||
import type { TreeNode } from '@/types';
|
||||
import { nextTick, ref, watch } from 'vue';
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean;
|
||||
isEditMode?: boolean;
|
||||
parentDirectory?: TreeNode | null;
|
||||
editingItem?: TreeNode | null;
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: boolean): void;
|
||||
(e: 'confirm', data: { path: string; sourcePath?: string; sourceType?: 'file' | 'folder' }): void;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
isEditMode: false,
|
||||
parentDirectory: null,
|
||||
editingItem: null,
|
||||
});
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
// Internal state
|
||||
const isVisible = ref(false);
|
||||
const formValid = ref(false);
|
||||
const filePath = ref('');
|
||||
const selectedFilePath = ref<string>('');
|
||||
const selectedFolderPath = ref<string>('');
|
||||
const inputRef = ref<HTMLInputElement | null>(null);
|
||||
|
||||
// Computed
|
||||
const isEditMode = computed(() => props.isEditMode);
|
||||
|
||||
// Watch for internal visibility changes and emit update
|
||||
watch(isVisible, (newValue) => {
|
||||
emit('update:modelValue', newValue);
|
||||
});
|
||||
|
||||
// Validation rules
|
||||
const requiredRule = (fieldName: string) => (v: string | null | undefined) =>
|
||||
!!v || `${fieldName}为必填项`;
|
||||
|
||||
// Methods
|
||||
const autoFocusInput = async () => {
|
||||
await nextTick();
|
||||
if (inputRef.value) {
|
||||
;(inputRef.value as HTMLInputElement).focus();
|
||||
}
|
||||
};
|
||||
|
||||
const selectSourceFile = async () => {
|
||||
// 使用主进程API来选择文件
|
||||
const result = await window.mainApi.invoke('msgOpenFile', 'any');
|
||||
|
||||
if (result && !result.canceled && result.filePaths.length > 0) {
|
||||
selectedFilePath.value = result.filePaths[0];
|
||||
selectedFolderPath.value = ''; // 清空文件夹选择
|
||||
}
|
||||
};
|
||||
|
||||
const selectSourceFolder = async () => {
|
||||
// 使用主进程API来选择文件夹
|
||||
const result = await window.mainApi.invoke('msgSelectDirectory');
|
||||
|
||||
if (result && !result.canceled && result.filePath) {
|
||||
selectedFolderPath.value = result.filePath;
|
||||
selectedFilePath.value = ''; // 清空文件选择
|
||||
}
|
||||
};
|
||||
|
||||
const confirm = () => {
|
||||
if (!filePath.value.trim()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 确定选择的源类型和路径
|
||||
let sourcePathValue: string | undefined;
|
||||
let sourceType: 'file' | 'folder' | undefined;
|
||||
|
||||
if (selectedFilePath.value) {
|
||||
sourcePathValue = selectedFilePath.value;
|
||||
sourceType = 'file';
|
||||
} else if (selectedFolderPath.value) {
|
||||
sourcePathValue = selectedFolderPath.value;
|
||||
sourceType = 'folder';
|
||||
}
|
||||
|
||||
emit('confirm', {
|
||||
path: filePath.value,
|
||||
sourcePath: sourcePathValue,
|
||||
sourceType,
|
||||
});
|
||||
|
||||
close();
|
||||
};
|
||||
|
||||
const reset = () => {
|
||||
filePath.value = '';
|
||||
selectedFilePath.value = '';
|
||||
selectedFolderPath.value = '';
|
||||
formValid.value = false;
|
||||
};
|
||||
|
||||
const close = () => {
|
||||
isVisible.value = false;
|
||||
};
|
||||
|
||||
// Watch for visibility changes
|
||||
watch(() => props.modelValue, (newValue) => {
|
||||
isVisible.value = newValue;
|
||||
if (newValue) {
|
||||
// Reset form when dialog opens
|
||||
filePath.value = props.editingItem?.path || '';
|
||||
selectedFilePath.value = props.editingItem?.sourceType === 'file' ? props.editingItem?.sourcePath || '' : '';
|
||||
selectedFolderPath.value = props.editingItem?.sourceType === 'folder' ? props.editingItem?.sourcePath || '' : '';
|
||||
autoFocusInput();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-dialog
|
||||
v-model="isVisible"
|
||||
@@ -11,13 +133,13 @@
|
||||
<v-card-text>
|
||||
<v-form v-model="formValid">
|
||||
<v-text-field
|
||||
ref="inputRef"
|
||||
v-model="filePath"
|
||||
label="文件名"
|
||||
placeholder="文件名需要带上后缀,如:style skech.doc"
|
||||
:rules="[requiredRule('文件名')]"
|
||||
required
|
||||
density="comfortable"
|
||||
ref="inputRef"
|
||||
/>
|
||||
|
||||
<!-- 文件选择输入框 -->
|
||||
@@ -26,7 +148,7 @@
|
||||
label="选择文件"
|
||||
placeholder="点击输入框选择文件"
|
||||
readonly
|
||||
:append-inner-icon="'mdi-file'"
|
||||
append-inner-icon="mdi-file"
|
||||
density="comfortable"
|
||||
class="mt-4"
|
||||
@click="selectSourceFile"
|
||||
@@ -38,7 +160,7 @@
|
||||
label="选择文件夹"
|
||||
placeholder="点击输入框选择文件夹"
|
||||
readonly
|
||||
:append-inner-icon="'mdi-folder'"
|
||||
append-inner-icon="mdi-folder"
|
||||
density="comfortable"
|
||||
class="mt-4"
|
||||
@click="selectSourceFolder"
|
||||
@@ -64,11 +186,13 @@
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn @click="close">取消</v-btn>
|
||||
<v-btn @click="close">
|
||||
取消
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="confirm"
|
||||
:disabled="!formValid || (!selectedFilePath && !selectedFolderPath && !isEditMode)"
|
||||
@click="confirm"
|
||||
>
|
||||
{{ isEditMode ? '保存' : '添加' }}
|
||||
</v-btn>
|
||||
@@ -76,125 +200,3 @@
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, nextTick } from 'vue'
|
||||
import { TreeNode } from '@/types'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
isEditMode?: boolean
|
||||
parentDirectory?: TreeNode | null
|
||||
editingItem?: TreeNode | null
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
(e: 'confirm', data: { path: string; sourcePath?: string; sourceType?: 'file' | 'folder' }): void
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
isEditMode: false,
|
||||
parentDirectory: null,
|
||||
editingItem: null
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
// Internal state
|
||||
const isVisible = ref(false)
|
||||
const formValid = ref(false)
|
||||
const filePath = ref('')
|
||||
const selectedFilePath = ref<string>('')
|
||||
const selectedFolderPath = ref<string>('')
|
||||
const inputRef = ref<HTMLInputElement | null>(null)
|
||||
|
||||
// Computed
|
||||
const isEditMode = computed(() => props.isEditMode)
|
||||
|
||||
// Watch for visibility changes
|
||||
watch(() => props.modelValue, (newValue) => {
|
||||
isVisible.value = newValue
|
||||
if (newValue) {
|
||||
// Reset form when dialog opens
|
||||
filePath.value = props.editingItem?.path || ''
|
||||
selectedFilePath.value = props.editingItem?.sourceType === 'file' ? props.editingItem?.sourcePath || '' : ''
|
||||
selectedFolderPath.value = props.editingItem?.sourceType === 'folder' ? props.editingItem?.sourcePath || '' : ''
|
||||
autoFocusInput()
|
||||
}
|
||||
})
|
||||
|
||||
// Watch for internal visibility changes and emit update
|
||||
watch(isVisible, (newValue) => {
|
||||
emit('update:modelValue', newValue)
|
||||
})
|
||||
|
||||
// Validation rules
|
||||
const requiredRule = (fieldName: string) => (v: string | null | undefined) =>
|
||||
!!v || `${fieldName}为必填项`
|
||||
|
||||
|
||||
|
||||
// Methods
|
||||
const autoFocusInput = async () => {
|
||||
await nextTick()
|
||||
if (inputRef.value) {
|
||||
;(inputRef.value as HTMLInputElement).focus()
|
||||
}
|
||||
}
|
||||
|
||||
const selectSourceFile = async () => {
|
||||
// 使用主进程API来选择文件
|
||||
const result = await window.mainApi.invoke('msgOpenFile', 'any')
|
||||
|
||||
if (result && !result.canceled && result.filePaths.length > 0) {
|
||||
selectedFilePath.value = result.filePaths[0]
|
||||
selectedFolderPath.value = '' // 清空文件夹选择
|
||||
}
|
||||
}
|
||||
|
||||
const selectSourceFolder = async () => {
|
||||
// 使用主进程API来选择文件夹
|
||||
const result = await window.mainApi.invoke('msgSelectDirectory')
|
||||
|
||||
if (result && !result.canceled && result.filePath) {
|
||||
selectedFolderPath.value = result.filePath
|
||||
selectedFilePath.value = '' // 清空文件选择
|
||||
}
|
||||
}
|
||||
|
||||
const confirm = () => {
|
||||
if (!filePath.value.trim()) return
|
||||
|
||||
// 确定选择的源类型和路径
|
||||
let sourcePathValue: string | undefined
|
||||
let sourceType: 'file' | 'folder' | undefined
|
||||
|
||||
if (selectedFilePath.value) {
|
||||
sourcePathValue = selectedFilePath.value
|
||||
sourceType = 'file'
|
||||
} else if (selectedFolderPath.value) {
|
||||
sourcePathValue = selectedFolderPath.value
|
||||
sourceType = 'folder'
|
||||
}
|
||||
|
||||
emit('confirm', {
|
||||
path: filePath.value,
|
||||
sourcePath: sourcePathValue,
|
||||
sourceType
|
||||
})
|
||||
|
||||
close()
|
||||
}
|
||||
|
||||
const reset = () => {
|
||||
filePath.value = ''
|
||||
selectedFilePath.value = ''
|
||||
selectedFolderPath.value = ''
|
||||
formValid.value = false
|
||||
}
|
||||
|
||||
const close = () => {
|
||||
isVisible.value = false
|
||||
}
|
||||
</script>
|
||||
@@ -1,3 +1,111 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted, ref } from 'vue';
|
||||
|
||||
// 定义对话框选项接口
|
||||
interface BaseDialogOptions {
|
||||
title: string;
|
||||
message: string;
|
||||
confirmText?: string;
|
||||
cancelText?: string;
|
||||
confirmColor?: string;
|
||||
}
|
||||
|
||||
interface ConfirmDialogOptions extends BaseDialogOptions {}
|
||||
|
||||
interface MessageDialogOptions extends BaseDialogOptions {
|
||||
dialogColor?: string;
|
||||
}
|
||||
|
||||
// 全局状态
|
||||
const confirmDialogVisible = ref(false);
|
||||
const messageDialogVisible = ref(false);
|
||||
const currentConfirmOptions = ref<ConfirmDialogOptions>({} as ConfirmDialogOptions);
|
||||
const currentMessageOptions = ref<MessageDialogOptions>({} as MessageDialogOptions);
|
||||
|
||||
// Promise 解析器
|
||||
let confirmResolver: ((value: boolean) => void) | null = null;
|
||||
let messageResolver: (() => void) | null = null;
|
||||
|
||||
// 显示确认对话框
|
||||
const showConfirm = (options: ConfirmDialogOptions): Promise<boolean> => {
|
||||
currentConfirmOptions.value = {
|
||||
title: options.title || '确认',
|
||||
message: options.message,
|
||||
confirmText: options.confirmText || '确认',
|
||||
cancelText: options.cancelText || '取消',
|
||||
confirmColor: options.confirmColor || 'primary',
|
||||
};
|
||||
|
||||
confirmDialogVisible.value = true;
|
||||
return new Promise((resolve) => {
|
||||
confirmResolver = resolve;
|
||||
});
|
||||
};
|
||||
|
||||
// 显示消息对话框
|
||||
const showMessage = (options: MessageDialogOptions): Promise<void> => {
|
||||
currentMessageOptions.value = {
|
||||
title: options.title,
|
||||
message: options.message,
|
||||
confirmText: options.confirmText || '确定',
|
||||
confirmColor: options.confirmColor || 'primary',
|
||||
dialogColor: options.dialogColor || 'white',
|
||||
};
|
||||
|
||||
messageDialogVisible.value = true;
|
||||
return new Promise((resolve) => {
|
||||
messageResolver = resolve;
|
||||
});
|
||||
};
|
||||
|
||||
// 处理确认对话框确认操作
|
||||
const handleConfirmAction = () => {
|
||||
confirmDialogVisible.value = false;
|
||||
if (confirmResolver) {
|
||||
confirmResolver(true);
|
||||
confirmResolver = null;
|
||||
}
|
||||
};
|
||||
|
||||
// 处理确认对话框取消操作
|
||||
const handleConfirmCancel = () => {
|
||||
confirmDialogVisible.value = false;
|
||||
if (confirmResolver) {
|
||||
confirmResolver(false);
|
||||
confirmResolver = null;
|
||||
}
|
||||
};
|
||||
|
||||
// 处理消息对话框操作
|
||||
const handleMessageAction = () => {
|
||||
messageDialogVisible.value = false;
|
||||
if (messageResolver) {
|
||||
messageResolver();
|
||||
messageResolver = null;
|
||||
}
|
||||
};
|
||||
|
||||
// 暴露方法给全局
|
||||
onMounted(() => {
|
||||
window.ConfirmDialogManager = {
|
||||
showConfirm,
|
||||
showMessage,
|
||||
};
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (window.ConfirmDialogManager) {
|
||||
delete window.ConfirmDialogManager;
|
||||
}
|
||||
});
|
||||
|
||||
// 导出方法
|
||||
defineExpose({
|
||||
showConfirm,
|
||||
showMessage,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<!-- Confirm Dialog -->
|
||||
@@ -14,17 +122,17 @@
|
||||
{{ currentConfirmOptions.message }}
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
@click="handleConfirmCancel"
|
||||
variant="text"
|
||||
@click="handleConfirmCancel"
|
||||
>
|
||||
{{ currentConfirmOptions.cancelText }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
@click="handleConfirmAction"
|
||||
:color="currentConfirmOptions.confirmColor"
|
||||
variant="flat"
|
||||
@click="handleConfirmAction"
|
||||
>
|
||||
{{ currentConfirmOptions.confirmText }}
|
||||
</v-btn>
|
||||
@@ -46,11 +154,11 @@
|
||||
{{ currentMessageOptions.message }}
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
@click="handleMessageAction"
|
||||
:color="currentMessageOptions.confirmColor"
|
||||
variant="flat"
|
||||
@click="handleMessageAction"
|
||||
>
|
||||
{{ currentMessageOptions.confirmText }}
|
||||
</v-btn>
|
||||
@@ -60,117 +168,6 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
|
||||
// 定义对话框选项接口
|
||||
interface BaseDialogOptions {
|
||||
title: string
|
||||
message: string
|
||||
confirmText?: string
|
||||
cancelText?: string
|
||||
confirmColor?: string
|
||||
}
|
||||
|
||||
interface ConfirmDialogOptions extends BaseDialogOptions {}
|
||||
|
||||
interface MessageDialogOptions extends BaseDialogOptions {
|
||||
dialogColor?: string
|
||||
}
|
||||
|
||||
// 全局状态
|
||||
const confirmDialogVisible = ref(false)
|
||||
const messageDialogVisible = ref(false)
|
||||
const currentConfirmOptions = ref<ConfirmDialogOptions>({} as ConfirmDialogOptions)
|
||||
const currentMessageOptions = ref<MessageDialogOptions>({} as MessageDialogOptions)
|
||||
|
||||
// Promise 解析器
|
||||
let confirmResolver: ((value: boolean) => void) | null = null
|
||||
let messageResolver: (() => void) | null = null
|
||||
|
||||
// 显示确认对话框
|
||||
const showConfirm = (options: ConfirmDialogOptions): Promise<boolean> => {
|
||||
currentConfirmOptions.value = {
|
||||
title: options.title || '确认',
|
||||
message: options.message,
|
||||
confirmText: options.confirmText || '确认',
|
||||
cancelText: options.cancelText || '取消',
|
||||
confirmColor: options.confirmColor || 'primary'
|
||||
}
|
||||
|
||||
confirmDialogVisible.value = true
|
||||
return new Promise((resolve) => {
|
||||
confirmResolver = resolve
|
||||
})
|
||||
}
|
||||
|
||||
// 显示消息对话框
|
||||
const showMessage = (options: MessageDialogOptions): Promise<void> => {
|
||||
currentMessageOptions.value = {
|
||||
title: options.title,
|
||||
message: options.message,
|
||||
confirmText: options.confirmText || '确定',
|
||||
confirmColor: options.confirmColor || 'primary',
|
||||
dialogColor: options.dialogColor || 'white'
|
||||
}
|
||||
|
||||
messageDialogVisible.value = true
|
||||
return new Promise((resolve) => {
|
||||
messageResolver = resolve
|
||||
})
|
||||
}
|
||||
|
||||
// 处理确认对话框确认操作
|
||||
const handleConfirmAction = () => {
|
||||
confirmDialogVisible.value = false
|
||||
if (confirmResolver) {
|
||||
confirmResolver(true)
|
||||
confirmResolver = null
|
||||
}
|
||||
}
|
||||
|
||||
// 处理确认对话框取消操作
|
||||
const handleConfirmCancel = () => {
|
||||
confirmDialogVisible.value = false
|
||||
if (confirmResolver) {
|
||||
confirmResolver(false)
|
||||
confirmResolver = null
|
||||
}
|
||||
}
|
||||
|
||||
// 处理消息对话框操作
|
||||
const handleMessageAction = () => {
|
||||
messageDialogVisible.value = false
|
||||
if (messageResolver) {
|
||||
messageResolver()
|
||||
messageResolver = null
|
||||
}
|
||||
}
|
||||
|
||||
// 暴露方法给全局
|
||||
onMounted(() => {
|
||||
window.ConfirmDialogManager = {
|
||||
showConfirm,
|
||||
showMessage
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (window.ConfirmDialogManager) {
|
||||
delete window.ConfirmDialogManager
|
||||
}
|
||||
})
|
||||
|
||||
// 对话框类型
|
||||
type DialogType = 'confirm' | 'info' | 'success' | 'warning' | 'error'
|
||||
|
||||
// 导出方法
|
||||
defineExpose({
|
||||
showConfirm,
|
||||
showMessage
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 样式在此可以添加额外的自定义样式 */
|
||||
</style>
|
||||
</style>
|
||||
@@ -1,3 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { useSnackbarStore } from '@/renderer/store/snackbar';
|
||||
|
||||
const snackbar = useSnackbarStore();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-snackbar
|
||||
v-model="snackbar.isVisible"
|
||||
@@ -26,9 +32,3 @@
|
||||
</template>
|
||||
</v-snackbar>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useSnackbarStore } from '@/renderer/store/snackbar'
|
||||
|
||||
const snackbar = useSnackbarStore()
|
||||
</script>
|
||||
@@ -1,3 +1,59 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
// 定义 props
|
||||
interface Props {
|
||||
modelValue: boolean;
|
||||
title: string;
|
||||
message: string;
|
||||
confirmText?: string;
|
||||
cancelText?: string;
|
||||
confirmColor?: string;
|
||||
dialogColor?: string;
|
||||
showActions?: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
confirmText: '确认',
|
||||
cancelText: '取消',
|
||||
confirmColor: 'primary',
|
||||
dialogColor: 'white',
|
||||
showActions: true,
|
||||
});
|
||||
|
||||
// 定义 emits
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: boolean];
|
||||
'confirm': [];
|
||||
'cancel': [];
|
||||
}>();
|
||||
|
||||
// 本地状态
|
||||
const dialogVisible = ref(props.modelValue);
|
||||
|
||||
// 监听 modelValue 的变化
|
||||
watch(() => props.modelValue, (newValue) => {
|
||||
dialogVisible.value = newValue;
|
||||
});
|
||||
|
||||
// 监听本地状态变化并同步到父组件
|
||||
watch(dialogVisible, (newValue) => {
|
||||
emit('update:modelValue', newValue);
|
||||
});
|
||||
|
||||
// 处理确认事件
|
||||
const handleConfirm = () => {
|
||||
emit('confirm');
|
||||
dialogVisible.value = false;
|
||||
};
|
||||
|
||||
// 处理取消事件
|
||||
const handleCancel = () => {
|
||||
emit('cancel');
|
||||
dialogVisible.value = false;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-dialog
|
||||
v-model="dialogVisible"
|
||||
@@ -12,28 +68,28 @@
|
||||
{{ message }}
|
||||
</v-card-text>
|
||||
<v-card-actions v-if="showActions">
|
||||
<v-spacer></v-spacer>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
v-if="cancelText"
|
||||
@click="handleCancel"
|
||||
variant="text"
|
||||
@click="handleCancel"
|
||||
>
|
||||
{{ cancelText }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
@click="handleConfirm"
|
||||
:color="confirmColor"
|
||||
variant="flat"
|
||||
@click="handleConfirm"
|
||||
>
|
||||
{{ confirmText }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
<v-card-actions v-else>
|
||||
<v-spacer></v-spacer>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
@click="handleConfirm"
|
||||
:color="confirmColor"
|
||||
variant="flat"
|
||||
@click="handleConfirm"
|
||||
>
|
||||
{{ confirmText }}
|
||||
</v-btn>
|
||||
@@ -41,59 +97,3 @@
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
// 定义 props
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
title: string
|
||||
message: string
|
||||
confirmText?: string
|
||||
cancelText?: string
|
||||
confirmColor?: string
|
||||
dialogColor?: string
|
||||
showActions?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
confirmText: '确认',
|
||||
cancelText: '取消',
|
||||
confirmColor: 'primary',
|
||||
dialogColor: 'white',
|
||||
showActions: true
|
||||
})
|
||||
|
||||
// 定义 emits
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: boolean]
|
||||
confirm: []
|
||||
cancel: []
|
||||
}>()
|
||||
|
||||
// 本地状态
|
||||
const dialogVisible = ref(props.modelValue)
|
||||
|
||||
// 监听 modelValue 的变化
|
||||
watch(() => props.modelValue, (newValue) => {
|
||||
dialogVisible.value = newValue
|
||||
})
|
||||
|
||||
// 监听本地状态变化并同步到父组件
|
||||
watch(dialogVisible, (newValue) => {
|
||||
emit('update:modelValue', newValue)
|
||||
})
|
||||
|
||||
// 处理确认事件
|
||||
const handleConfirm = () => {
|
||||
emit('confirm')
|
||||
dialogVisible.value = false
|
||||
}
|
||||
|
||||
// 处理取消事件
|
||||
const handleCancel = () => {
|
||||
emit('cancel')
|
||||
dialogVisible.value = false
|
||||
}
|
||||
</script>
|
||||
@@ -1,3 +1,46 @@
|
||||
<script setup lang="ts">
|
||||
import type { Template, TreeNode } from '@/types';
|
||||
import { computed } from 'vue';
|
||||
|
||||
interface Props {
|
||||
template: Template;
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'view', template: Template): void;
|
||||
(e: 'execute', template: Template): void;
|
||||
(e: 'edit', template: Template): void;
|
||||
(e: 'delete', template: Template): void;
|
||||
(e: 'copy', template: Template): void;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
defineEmits<Emits>();
|
||||
|
||||
// Count files and directories in tree structure
|
||||
const nodeCount = computed(() => {
|
||||
let files = 0;
|
||||
let directories = 0;
|
||||
|
||||
const count = (nodeList: TreeNode[]) => {
|
||||
nodeList.forEach((node) => {
|
||||
if (node.type === 'file') {
|
||||
files++;
|
||||
} else if (node.type === 'directory') {
|
||||
directories++;
|
||||
if (node.children) {
|
||||
count(node.children);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
count(props.template.treeStructure || []);
|
||||
|
||||
return { files, directories };
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-card class="d-flex flex-column" height="100%" variant="outlined">
|
||||
<v-card-item>
|
||||
@@ -16,7 +59,7 @@
|
||||
</v-chip-group>
|
||||
</v-card-text>
|
||||
|
||||
<v-divider class="mt-auto"></v-divider>
|
||||
<v-divider class="mt-auto" />
|
||||
|
||||
<v-card-actions class="pa-2 justify-space-between">
|
||||
<div>
|
||||
@@ -29,7 +72,7 @@
|
||||
</div>
|
||||
|
||||
<v-menu location="bottom end">
|
||||
<template v-slot:activator="{ props }">
|
||||
<template #activator="{ props }">
|
||||
<v-btn v-bind="props" icon variant="text" size="small" density="comfortable">
|
||||
<v-icon>mdi-dots-vertical</v-icon>
|
||||
</v-btn>
|
||||
@@ -37,72 +80,35 @@
|
||||
|
||||
<v-list density="compact">
|
||||
<v-list-item @click="$emit('edit', template)">
|
||||
<template v-slot:prepend>
|
||||
<template #prepend>
|
||||
<v-icon>mdi-pencil</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>修改</v-list-item-title>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item @click="$emit('copy', template)">
|
||||
<template v-slot:prepend>
|
||||
<v-icon color="primary">mdi-content-copy</v-icon>
|
||||
<template #prepend>
|
||||
<v-icon color="primary">
|
||||
mdi-content-copy
|
||||
</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>复制</v-list-item-title>
|
||||
</v-list-item>
|
||||
|
||||
<v-divider></v-divider>
|
||||
<v-divider />
|
||||
|
||||
<v-list-item @click="$emit('delete', template)">
|
||||
<template v-slot:prepend>
|
||||
<v-icon color="error">mdi-delete</v-icon>
|
||||
<template #prepend>
|
||||
<v-icon color="error">
|
||||
mdi-delete
|
||||
</v-icon>
|
||||
</template>
|
||||
<v-list-item-title class="text-error">删除</v-list-item-title>
|
||||
<v-list-item-title class="text-error">
|
||||
删除
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { Template, TreeNode } from '@/types'
|
||||
|
||||
interface Props {
|
||||
template: Template
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'view', template: Template): void
|
||||
(e: 'execute', template: Template): void
|
||||
(e: 'edit', template: Template): void
|
||||
(e: 'delete', template: Template): void
|
||||
(e: 'copy', template: Template): void
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
// Count files and directories in tree structure
|
||||
const nodeCount = computed(() => {
|
||||
let files = 0
|
||||
let directories = 0
|
||||
|
||||
const count = (nodeList: TreeNode[]) => {
|
||||
nodeList.forEach(node => {
|
||||
if (node.type === 'file') {
|
||||
files++
|
||||
} else if (node.type === 'directory') {
|
||||
directories++
|
||||
if (node.children) {
|
||||
count(node.children)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
count(props.template.treeStructure || [])
|
||||
|
||||
return { files, directories }
|
||||
})
|
||||
</script>
|
||||
@@ -1,3 +1,34 @@
|
||||
<script setup lang="ts">
|
||||
interface TreeItem {
|
||||
id: string;
|
||||
name: string;
|
||||
type: 'directory' | 'file';
|
||||
children?: TreeItem[];
|
||||
open?: boolean;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
items: TreeItem[];
|
||||
height?: number;
|
||||
readOnly?: boolean;
|
||||
emptyText?: string;
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
height: 600,
|
||||
readOnly: false,
|
||||
emptyText: '暂无文件或目录',
|
||||
});
|
||||
|
||||
const emit = defineEmits([
|
||||
'addNewItemToCurrentDirectory',
|
||||
'editFile',
|
||||
'editItem',
|
||||
'deleteItem',
|
||||
'addFileToDirectory',
|
||||
]);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-card
|
||||
variant="outlined"
|
||||
@@ -12,7 +43,7 @@
|
||||
item-value="id"
|
||||
open-on-click
|
||||
:hoverable="true"
|
||||
:indent-lines="'default'"
|
||||
indent-lines="default"
|
||||
separate-roots
|
||||
density="compact"
|
||||
>
|
||||
@@ -50,7 +81,9 @@
|
||||
class="ml-2"
|
||||
@click.stop="emit('addNewItemToCurrentDirectory', item)"
|
||||
>
|
||||
<v-icon size="small">mdi-folder-plus</v-icon>
|
||||
<v-icon size="small">
|
||||
mdi-folder-plus
|
||||
</v-icon>
|
||||
<v-tooltip
|
||||
activator="parent"
|
||||
location="top"
|
||||
@@ -66,7 +99,9 @@
|
||||
size="small"
|
||||
@click.stop="emit('addFileToDirectory', item)"
|
||||
>
|
||||
<v-icon size="small"> mdi-file-plus </v-icon>
|
||||
<v-icon size="small">
|
||||
mdi-file-plus
|
||||
</v-icon>
|
||||
<v-tooltip
|
||||
activator="parent"
|
||||
location="top"
|
||||
@@ -81,7 +116,9 @@
|
||||
size="small"
|
||||
@click.stop="emit('editItem', item)"
|
||||
>
|
||||
<v-icon size="small"> mdi-pencil </v-icon>
|
||||
<v-icon size="small">
|
||||
mdi-pencil
|
||||
</v-icon>
|
||||
<v-tooltip
|
||||
activator="parent"
|
||||
location="top"
|
||||
@@ -134,49 +171,10 @@
|
||||
</p>
|
||||
</div>
|
||||
</v-card>
|
||||
|
||||
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
interface TreeItem {
|
||||
id: string
|
||||
name: string
|
||||
type: 'directory' | 'file'
|
||||
children?: TreeItem[]
|
||||
open?: boolean
|
||||
}
|
||||
|
||||
interface Props {
|
||||
items: TreeItem[]
|
||||
height?: number
|
||||
readOnly?: boolean
|
||||
emptyText?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
height: 600,
|
||||
readOnly: false,
|
||||
emptyText: '暂无文件或目录'
|
||||
})
|
||||
|
||||
const localExpanded = defineModel<string[]>('expanded', {
|
||||
default: () => []
|
||||
})
|
||||
|
||||
const emit = defineEmits([
|
||||
'addNewItemToCurrentDirectory',
|
||||
'editFile',
|
||||
'editItem',
|
||||
'deleteItem',
|
||||
'addFileToDirectory'
|
||||
])
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.cursor-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -1,3 +1,88 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, onUnmounted } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useAppStore } from '@/renderer/store/app';
|
||||
|
||||
const { t } = useI18n();
|
||||
const appStore = useAppStore();
|
||||
|
||||
// 从store获取状态
|
||||
const updateStatus = computed(() => appStore.updateStatus);
|
||||
const downloadProgress = computed(() => appStore.downloadProgress);
|
||||
|
||||
// 对话框显示状态
|
||||
const dialogVisible = computed({
|
||||
get: () => {
|
||||
return appStore.updateDialogVisible
|
||||
|| updateStatus.value.status === 'available'
|
||||
|| updateStatus.value.status === 'downloading'
|
||||
|| updateStatus.value.status === 'downloaded'
|
||||
|| (updateStatus.value.status === 'error' && !!updateStatus.value.error);
|
||||
},
|
||||
set: (value: boolean) => {
|
||||
if (value) {
|
||||
appStore.showUpdateDialog();
|
||||
} else {
|
||||
appStore.hideUpdateDialog();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const latestVersion = computed(() => {
|
||||
// 这里应该从API获取最新版本号,暂时使用固定值
|
||||
return `v${(updateStatus.value.currentVersion || '1.0.0').replace(/^v?/, '').split('.').map((n, i) =>
|
||||
i === 2 ? (Number.parseInt(n) + 1).toString() : n,
|
||||
).join('.')}`;
|
||||
});
|
||||
|
||||
const isDownloading = computed(() => updateStatus.value.status === 'downloading');
|
||||
const isDownloadComplete = computed(() => downloadProgress.value >= 100);
|
||||
|
||||
// 方法
|
||||
const downloadUpdate = async () => {
|
||||
await appStore.startUpdateDownload();
|
||||
};
|
||||
|
||||
const skipUpdate = () => {
|
||||
appStore.setUpdateStatus('not-available', updateStatus.value.currentVersion);
|
||||
dialogVisible.value = false;
|
||||
};
|
||||
|
||||
const cancelDownload = () => {
|
||||
// 取消下载逻辑
|
||||
appStore.setUpdateStatus('not-available', updateStatus.value.currentVersion);
|
||||
dialogVisible.value = false;
|
||||
};
|
||||
|
||||
const laterInstall = () => {
|
||||
dialogVisible.value = false;
|
||||
};
|
||||
|
||||
const installUpdate = () => {
|
||||
// 安装更新,这需要触发quitAndInstall
|
||||
window.mainApi.invoke('msgInstallUpdate');
|
||||
dialogVisible.value = false;
|
||||
};
|
||||
|
||||
const closeDialog = () => {
|
||||
appStore.setUpdateStatus('idle');
|
||||
dialogVisible.value = false;
|
||||
};
|
||||
|
||||
// 监听更新下载进度
|
||||
onMounted(() => {
|
||||
// 监听主进程发来的更新相关事件
|
||||
window.mainApi.on('update-download-progress', (_, progress) => {
|
||||
appStore.setDownloadProgress(progress.percent);
|
||||
});
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
// 移除事件监听器
|
||||
window.mainApi.off('update-download-progress', () => {});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-dialog
|
||||
v-model="dialogVisible"
|
||||
@@ -12,10 +97,12 @@
|
||||
<v-card-text>
|
||||
<div>{{ t('update.currentVersion') }}: {{ updateStatus.currentVersion }}</div>
|
||||
<div>{{ t('update.latestVersion') }}: {{ latestVersion }}</div>
|
||||
<div class="mt-3">{{ t('update.updateDescription') }}</div>
|
||||
<div class="mt-3">
|
||||
{{ t('update.updateDescription') }}
|
||||
</div>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="text"
|
||||
@@ -26,8 +113,8 @@
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="flat"
|
||||
@click="downloadUpdate"
|
||||
:loading="isDownloading"
|
||||
@click="downloadUpdate"
|
||||
>
|
||||
{{ t('update.downloadUpdate') }}
|
||||
</v-btn>
|
||||
@@ -47,15 +134,17 @@
|
||||
>
|
||||
<strong>{{ Math.round(downloadProgress) }}%</strong>
|
||||
</v-progress-linear>
|
||||
<div class="mt-3">{{ t('update.downloadProgressText') }}</div>
|
||||
<div class="mt-3">
|
||||
{{ t('update.downloadProgressText') }}
|
||||
</div>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="text"
|
||||
@click="cancelDownload"
|
||||
:disabled="isDownloadComplete"
|
||||
@click="cancelDownload"
|
||||
>
|
||||
{{ t('common.cancel') }}
|
||||
</v-btn>
|
||||
@@ -70,7 +159,7 @@
|
||||
{{ t('update.updateReadyToInstall') }}
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="text"
|
||||
@@ -96,7 +185,7 @@
|
||||
{{ updateStatus.error }}
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="flat"
|
||||
@@ -108,88 +197,3 @@
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, onUnmounted } from 'vue'
|
||||
import { useAppStore, UpdateStatus } from '@/renderer/store/app'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
const appStore = useAppStore()
|
||||
|
||||
// 从store获取状态
|
||||
const updateStatus = computed(() => appStore.updateStatus)
|
||||
const downloadProgress = computed(() => appStore.downloadProgress)
|
||||
|
||||
// 对话框显示状态
|
||||
const dialogVisible = computed({
|
||||
get: () => {
|
||||
return appStore.updateDialogVisible ||
|
||||
updateStatus.value.status === 'available' ||
|
||||
updateStatus.value.status === 'downloading' ||
|
||||
updateStatus.value.status === 'downloaded' ||
|
||||
(updateStatus.value.status === 'error' && !!updateStatus.value.error)
|
||||
},
|
||||
set: (value: boolean) => {
|
||||
if (value) {
|
||||
appStore.showUpdateDialog()
|
||||
} else {
|
||||
appStore.hideUpdateDialog()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const latestVersion = computed(() => {
|
||||
// 这里应该从API获取最新版本号,暂时使用固定值
|
||||
return 'v' + (updateStatus.value.currentVersion || '1.0.0').replace(/^v?/, '').split('.').map((n, i) =>
|
||||
i === 2 ? (parseInt(n) + 1).toString() : n
|
||||
).join('.')
|
||||
})
|
||||
|
||||
const isDownloading = computed(() => updateStatus.value.status === 'downloading')
|
||||
const isDownloadComplete = computed(() => downloadProgress.value >= 100)
|
||||
|
||||
// 方法
|
||||
const downloadUpdate = async () => {
|
||||
await appStore.startUpdateDownload()
|
||||
}
|
||||
|
||||
const skipUpdate = () => {
|
||||
appStore.setUpdateStatus('not-available', updateStatus.value.currentVersion)
|
||||
dialogVisible.value = false
|
||||
}
|
||||
|
||||
const cancelDownload = () => {
|
||||
// 取消下载逻辑
|
||||
appStore.setUpdateStatus('not-available', updateStatus.value.currentVersion)
|
||||
dialogVisible.value = false
|
||||
}
|
||||
|
||||
const laterInstall = () => {
|
||||
dialogVisible.value = false
|
||||
}
|
||||
|
||||
const installUpdate = () => {
|
||||
// 安装更新,这需要触发quitAndInstall
|
||||
window.mainApi.invoke('msgInstallUpdate')
|
||||
dialogVisible.value = false
|
||||
}
|
||||
|
||||
const closeDialog = () => {
|
||||
appStore.setUpdateStatus('idle')
|
||||
dialogVisible.value = false
|
||||
}
|
||||
|
||||
// 监听更新下载进度
|
||||
onMounted(() => {
|
||||
// 监听主进程发来的更新相关事件
|
||||
window.mainApi.on('update-download-progress', (_, progress) => {
|
||||
appStore.setDownloadProgress(progress.percent)
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
// 移除事件监听器
|
||||
window.mainApi.off('update-download-progress', () => {})
|
||||
})
|
||||
</script>
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="tsx">
|
||||
import HeaderLayout from '@/renderer/components/layout/HeaderLayout.vue'
|
||||
import HeaderLayout from '@/renderer/components/layout/HeaderLayout.vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,51 +1,25 @@
|
||||
<script setup lang="tsx">
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { mdiFitToScreenOutline, mdiHome } from '@mdi/js'
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
const router = useRouter()
|
||||
const route: any = useRoute()
|
||||
const titleKey: string = (route?.meta?.titleKey || 'title.main') as string
|
||||
|
||||
const { t } = useI18n()
|
||||
const router = useRouter();
|
||||
|
||||
const handleRoute = (path: string): void => {
|
||||
router.push(path)
|
||||
}
|
||||
|
||||
const isCurrentRoute = (path: string): boolean => {
|
||||
return path === route.path
|
||||
}
|
||||
|
||||
const headerMenus: {
|
||||
icon: string
|
||||
text: string
|
||||
path: string
|
||||
}[] = [
|
||||
{
|
||||
icon: mdiHome,
|
||||
text: 'title.main',
|
||||
path: '/'
|
||||
},
|
||||
{
|
||||
icon: mdiFitToScreenOutline,
|
||||
text: 'title.second',
|
||||
path: '/second'
|
||||
}
|
||||
]
|
||||
router.push(path);
|
||||
};
|
||||
|
||||
// Get app version
|
||||
const appVersion = ref('')
|
||||
const appVersion = ref('');
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
appVersion.value = await window.mainApi.invoke('msgRequestGetVersion')
|
||||
appVersion.value = await window.mainApi.invoke('msgRequestGetVersion');
|
||||
} catch (error) {
|
||||
console.error('Failed to get app version:', error)
|
||||
appVersion.value = 'unknown'
|
||||
console.error('Failed to get app version:', error);
|
||||
appVersion.value = 'unknown';
|
||||
}
|
||||
})
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-app-bar color="primary" density="compact">
|
||||
<v-app-bar-title @click="handleRoute('/')">
|
||||
@@ -63,6 +37,7 @@ onMounted(async () => {
|
||||
</template>
|
||||
</v-app-bar>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.v-btn {
|
||||
opacity: 0.4;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import DefaultLayout from '@/renderer/components/layout/DefaultLayout.vue'
|
||||
import HeaderLayout from '@/renderer/components/layout/HeaderLayout.vue'
|
||||
import DefaultLayout from '@/renderer/components/layout/DefaultLayout.vue';
|
||||
import HeaderLayout from '@/renderer/components/layout/HeaderLayout.vue';
|
||||
|
||||
export { DefaultLayout, HeaderLayout }
|
||||
export { DefaultLayout, HeaderLayout };
|
||||
@@ -6,8 +6,5 @@
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
<script
|
||||
type="module"
|
||||
src="./main"
|
||||
></script>
|
||||
<script type="module" src="./main"></script>
|
||||
</html>
|
||||
@@ -1,13 +1,13 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createApp } from 'vue';
|
||||
|
||||
import App from '@/renderer/App.vue'
|
||||
import router from '@/renderer/router'
|
||||
import vuetify from '@/renderer/plugins/vuetify'
|
||||
import i18n from '@/renderer/plugins/i18n'
|
||||
import pinia from '@/renderer/plugins/pinia'
|
||||
import App from '@/renderer/App.vue';
|
||||
import i18n from '@/renderer/plugins/i18n';
|
||||
import pinia from '@/renderer/plugins/pinia';
|
||||
import vuetify from '@/renderer/plugins/vuetify';
|
||||
import router from '@/renderer/router';
|
||||
|
||||
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 en from '@/renderer/locales/en.json'
|
||||
import ko from '@/renderer/locales/ko.json'
|
||||
import zhHans from '@/renderer/locales/zh-hans.json'
|
||||
import zhHant from '@/renderer/locales/zh-hant.json'
|
||||
import de from '@/renderer/locales/de.json'
|
||||
import es from '@/renderer/locales/es.json'
|
||||
import ja from '@/renderer/locales/ja.json'
|
||||
import fr from '@/renderer/locales/fr.json'
|
||||
import ru from '@/renderer/locales/ru.json'
|
||||
import pt from '@/renderer/locales/pt.json'
|
||||
import nl from '@/renderer/locales/nl.json'
|
||||
import { getCurrentLocale } from '@/renderer/utils'
|
||||
import { createI18n } from 'vue-i18n';
|
||||
import de from '@/renderer/locales/de.json';
|
||||
import en from '@/renderer/locales/en.json';
|
||||
import es from '@/renderer/locales/es.json';
|
||||
import fr from '@/renderer/locales/fr.json';
|
||||
import ja from '@/renderer/locales/ja.json';
|
||||
import ko from '@/renderer/locales/ko.json';
|
||||
import nl from '@/renderer/locales/nl.json';
|
||||
import pt from '@/renderer/locales/pt.json';
|
||||
import ru from '@/renderer/locales/ru.json';
|
||||
import zhHans from '@/renderer/locales/zh-hans.json';
|
||||
import zhHant from '@/renderer/locales/zh-hant.json';
|
||||
import { getCurrentLocale } from '@/renderer/utils';
|
||||
|
||||
export default createI18n({
|
||||
locale: getCurrentLocale(),
|
||||
@@ -27,6 +27,6 @@ export default createI18n({
|
||||
fr,
|
||||
ru,
|
||||
pt,
|
||||
nl
|
||||
}
|
||||
})
|
||||
nl,
|
||||
},
|
||||
});
|
||||
@@ -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 {
|
||||
ko,
|
||||
de,
|
||||
en,
|
||||
es,
|
||||
fr,
|
||||
ja,
|
||||
ko,
|
||||
nl,
|
||||
pt,
|
||||
ru,
|
||||
zhHans,
|
||||
zhHant,
|
||||
de,
|
||||
es,
|
||||
ja,
|
||||
fr,
|
||||
ru,
|
||||
pt,
|
||||
nl
|
||||
} from 'vuetify/locale'
|
||||
} from 'vuetify/locale';
|
||||
import colors from 'vuetify/util/colors';
|
||||
// import { aliases, mdi } from 'vuetify/iconsets/mdi-svg'
|
||||
import 'vuetify/styles'
|
||||
import '@mdi/font/css/materialdesignicons.css'
|
||||
import 'vuetify/styles';
|
||||
|
||||
import colors from 'vuetify/util/colors'
|
||||
import '@mdi/font/css/materialdesignicons.css';
|
||||
|
||||
export default createVuetify({
|
||||
locale: {
|
||||
messages: { ko, en, zhHans, zhHant, de, es, ja, fr, ru, pt, nl },
|
||||
locale: 'zhHans',
|
||||
fallback: 'zhHans'
|
||||
fallback: 'zhHans',
|
||||
},
|
||||
defaults: {
|
||||
VBtn: {
|
||||
style: [
|
||||
{
|
||||
// Do not force capitalization of a button text
|
||||
textTransform: 'none'
|
||||
}
|
||||
]
|
||||
}
|
||||
textTransform: 'none',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
// icons: {
|
||||
// defaultSet: 'mdi',
|
||||
@@ -46,15 +46,15 @@ export default createVuetify({
|
||||
light: {
|
||||
dark: false,
|
||||
colors: {
|
||||
primary: colors.green.darken2
|
||||
}
|
||||
primary: colors.green.darken2,
|
||||
},
|
||||
},
|
||||
dark: {
|
||||
dark: true,
|
||||
colors: {
|
||||
primary: colors.green.darken4
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
primary: colors.green.darken4,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -1,52 +1,51 @@
|
||||
import { MainScreen } from '@/renderer/screens'
|
||||
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||
import { createRouter, createWebHashHistory } from 'vue-router';
|
||||
|
||||
export default createRouter({
|
||||
history: createWebHashHistory(),
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
redirect: '/templates'
|
||||
redirect: '/templates',
|
||||
},
|
||||
{
|
||||
path: '/templates',
|
||||
component: () => import('@/renderer/screens/TemplateListScreen.vue'),
|
||||
meta: {
|
||||
titleKey: 'title.templates'
|
||||
}
|
||||
titleKey: 'title.templates',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/create-template',
|
||||
component: () => import('@/renderer/screens/CreateTemplateScreen.vue'),
|
||||
meta: {
|
||||
titleKey: 'title.create-template'
|
||||
}
|
||||
titleKey: 'title.create-template',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/create-template/:id',
|
||||
component: () => import('@/renderer/screens/CreateTemplateScreen.vue'),
|
||||
meta: {
|
||||
titleKey: 'title.edit-template'
|
||||
titleKey: 'title.edit-template',
|
||||
},
|
||||
props: true
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: '/second',
|
||||
component: () => import('@/renderer/screens/SecondScreen.vue'),
|
||||
meta: {
|
||||
titleKey: 'title.second'
|
||||
}
|
||||
titleKey: 'title.second',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/error',
|
||||
component: () => import('@/renderer/screens/ErrorScreen.vue'),
|
||||
meta: {
|
||||
titleKey: 'title.error'
|
||||
}
|
||||
titleKey: 'title.error',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/:pathMatch(.*)*',
|
||||
redirect: '/'
|
||||
}
|
||||
]
|
||||
})
|
||||
redirect: '/',
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -1,3 +1,367 @@
|
||||
<script setup lang="ts">
|
||||
import type { TreeNode } from '@/types';
|
||||
import DirectoryDialog from '@/renderer/components/DirectoryDialog.vue';
|
||||
import FileDialog from '@/renderer/components/FileDialog.vue';
|
||||
import TemplateTree from '@/renderer/components/TemplateTree.vue';
|
||||
import { useSnackbarStore } from '@/renderer/store/snackbar';
|
||||
import { useTemplateStore } from '@/renderer/store/template';
|
||||
import { generateUniqueId } from '@/renderer/utils';
|
||||
import { ConfirmDialogService } from '@/renderer/utils/confirm-dialog-service';
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const templateStore = useTemplateStore();
|
||||
const snackbar = useSnackbarStore();
|
||||
|
||||
// Extract methods from store
|
||||
const { getTemplateById } = templateStore;
|
||||
|
||||
// Check if we're copying a template
|
||||
const isCopyingTemplate = computed(() => !route.params.id && !!route.query.copyId);
|
||||
|
||||
// Form state
|
||||
const formValid = ref(false);
|
||||
const templateName = ref('');
|
||||
const templateDescription = ref('');
|
||||
|
||||
// Dialogs
|
||||
const showFileDialog = ref(false);
|
||||
|
||||
// Directory dialog state
|
||||
const showDirectoryDialog = ref(false);
|
||||
const parentDirectoryForNewItems = ref<TreeNode | null>(null);
|
||||
const currentEditingItem = ref<TreeNode | null>(null);
|
||||
|
||||
// Tree state
|
||||
const expandedNodes = ref<string[]>([]);
|
||||
const treeHeight = ref(600);
|
||||
|
||||
// Item state - 使用树状结构
|
||||
const treeStructure = ref<TreeNode[]>([]);
|
||||
|
||||
// Computed
|
||||
const isEditMode = computed(() => !!route.params.id);
|
||||
|
||||
// Validation rules
|
||||
const requiredRule = (fieldName: string) => (v: string | null | undefined) =>
|
||||
!!v || `${fieldName}为必填项`;
|
||||
|
||||
// Lifecycle
|
||||
onMounted(async () => {
|
||||
await templateStore.loadTemplates();
|
||||
|
||||
// Set tree height based on window height
|
||||
updateTreeHeight();
|
||||
|
||||
// Add resize listener
|
||||
window.addEventListener('resize', updateTreeHeight);
|
||||
|
||||
// Handle edit mode
|
||||
if (isEditMode.value) {
|
||||
const templateId = route.params.id as string;
|
||||
const template = getTemplateById(templateId);
|
||||
|
||||
if (template) {
|
||||
templateName.value = template.name;
|
||||
templateDescription.value = template.description;
|
||||
treeStructure.value = [...template.treeStructure];
|
||||
} else {
|
||||
router.push('/templates');
|
||||
}
|
||||
} else if (isCopyingTemplate.value) {
|
||||
// Handle copy mode
|
||||
const copyId = route.query.copyId as string;
|
||||
if (copyId) {
|
||||
const template = getTemplateById(copyId);
|
||||
if (template) {
|
||||
templateName.value = `${template.name} - 副本`;
|
||||
templateDescription.value = template.description;
|
||||
treeStructure.value = [...template.treeStructure];
|
||||
} else {
|
||||
snackbar.error('找不到要复制的模板');
|
||||
router.push('/templates');
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', updateTreeHeight);
|
||||
});
|
||||
|
||||
// Methods
|
||||
const updateTreeHeight = () => {
|
||||
treeHeight.value = window.innerHeight - 200;
|
||||
};
|
||||
|
||||
const addRootDirectory = () => {
|
||||
parentDirectoryForNewItems.value = null;
|
||||
currentEditingItem.value = null;
|
||||
showDirectoryDialog.value = true;
|
||||
};
|
||||
|
||||
const addRootFile = () => {
|
||||
parentDirectoryForNewItems.value = null;
|
||||
currentEditingItem.value = null;
|
||||
showFileDialog.value = true;
|
||||
};
|
||||
|
||||
// Add a file directly to a specific directory
|
||||
const addFileToDirectory = (directoryItem: TreeNode) => {
|
||||
parentDirectoryForNewItems.value = directoryItem; // Store the directory node as parent
|
||||
currentEditingItem.value = null;
|
||||
showFileDialog.value = true;
|
||||
};
|
||||
|
||||
const addNewItemToCurrentDirectory = (directoryItem: TreeNode) => {
|
||||
parentDirectoryForNewItems.value = directoryItem; // Store the directory node as parent
|
||||
currentEditingItem.value = null;
|
||||
showDirectoryDialog.value = true;
|
||||
};
|
||||
|
||||
const handleDirectoryDialogConfirm = (data: { path: string }) => {
|
||||
const { path } = data;
|
||||
const newId = generateUniqueId('dir');
|
||||
|
||||
// Check if we're editing an existing directory
|
||||
if (
|
||||
currentEditingItem.value
|
||||
&& currentEditingItem.value.type === 'directory'
|
||||
) {
|
||||
// Find and update the existing directory in the tree
|
||||
const updatedNode: TreeNode = {
|
||||
...currentEditingItem.value,
|
||||
name: path,
|
||||
path,
|
||||
type: 'directory',
|
||||
};
|
||||
treeStructure.value = updateNodeInTree(
|
||||
treeStructure.value,
|
||||
currentEditingItem.value.id,
|
||||
updatedNode,
|
||||
);
|
||||
} else {
|
||||
// Create new directory node
|
||||
const newNode: TreeNode = {
|
||||
id: newId,
|
||||
name: path,
|
||||
path,
|
||||
type: 'directory',
|
||||
};
|
||||
|
||||
// Add to tree structure
|
||||
if (parentDirectoryForNewItems.value) {
|
||||
// In the parent directory, check if a node with the same name already exists
|
||||
const parentNode = parentDirectoryForNewItems.value;
|
||||
if (parentNode.children) {
|
||||
const duplicateExists = parentNode.children.some(
|
||||
child => child.name === path,
|
||||
);
|
||||
if (duplicateExists) {
|
||||
snackbar.error('当前目录下已存在同名节点!');
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Add as child to the parent directory
|
||||
parentNode.children = parentNode.children || [];
|
||||
parentNode.children.push(newNode);
|
||||
} else {
|
||||
// Check at root level for duplicates
|
||||
const duplicateExists = treeStructure.value.some(
|
||||
node => node.name === path,
|
||||
);
|
||||
if (duplicateExists) {
|
||||
snackbar.error('根目录下已存在同名节点!');
|
||||
return;
|
||||
}
|
||||
// Add as root node
|
||||
treeStructure.value.push(newNode);
|
||||
}
|
||||
}
|
||||
|
||||
showDirectoryDialog.value = false;
|
||||
parentDirectoryForNewItems.value = null; // Clear parent context after creation
|
||||
currentEditingItem.value = null;
|
||||
};
|
||||
|
||||
// 在树状结构中更新节点的辅助函数
|
||||
const updateNodeInTree = (
|
||||
nodes: TreeNode[],
|
||||
oldId: string,
|
||||
updatedNode: TreeNode,
|
||||
): TreeNode[] => {
|
||||
return nodes.map((node) => {
|
||||
if (node.id === oldId) {
|
||||
return updatedNode;
|
||||
}
|
||||
if (node.children) {
|
||||
return {
|
||||
...node,
|
||||
children: updateNodeInTree(node.children, oldId, updatedNode),
|
||||
};
|
||||
}
|
||||
return node;
|
||||
});
|
||||
};
|
||||
|
||||
const handleFileDialogConfirm = (data: { path: string; sourcePath?: string; sourceType?: 'file' | 'folder' }) => {
|
||||
const { path, sourcePath, sourceType } = data;
|
||||
|
||||
const newId = generateUniqueId('file');
|
||||
|
||||
// Check if we're editing an existing file
|
||||
if (currentEditingItem.value && currentEditingItem.value.type === 'file') {
|
||||
// Find and update the existing file in the tree
|
||||
const updatedNode: TreeNode = {
|
||||
id: currentEditingItem.value.id,
|
||||
name: path,
|
||||
path,
|
||||
type: 'file',
|
||||
sourcePath,
|
||||
sourceType,
|
||||
};
|
||||
treeStructure.value = updateNodeInTree(
|
||||
treeStructure.value,
|
||||
currentEditingItem.value.id,
|
||||
updatedNode,
|
||||
);
|
||||
} else {
|
||||
// Check for duplicate names in the same parent directory
|
||||
if (parentDirectoryForNewItems.value) {
|
||||
// In the parent directory, check if a node with the same name already exists
|
||||
const parentNode = parentDirectoryForNewItems.value;
|
||||
if (parentNode.children) {
|
||||
const duplicateExists = parentNode.children.some(
|
||||
child => child.name === path,
|
||||
);
|
||||
if (duplicateExists) {
|
||||
snackbar.error('当前目录下已存在同名节点!');
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Check at root level for duplicates
|
||||
const duplicateExists = treeStructure.value.some(
|
||||
node => node.name === path,
|
||||
);
|
||||
if (duplicateExists) {
|
||||
snackbar.error('根目录下已存在同名节点!');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const newFileNode: TreeNode = {
|
||||
id: newId,
|
||||
name: path,
|
||||
path,
|
||||
type: 'file',
|
||||
sourcePath,
|
||||
sourceType,
|
||||
};
|
||||
|
||||
// Add to tree structure
|
||||
if (parentDirectoryForNewItems.value) {
|
||||
// Add as child to the parent directory
|
||||
const parentNode = parentDirectoryForNewItems.value;
|
||||
parentNode.children = parentNode.children || [];
|
||||
parentNode.children.push(newFileNode);
|
||||
} else {
|
||||
// Add as root node
|
||||
treeStructure.value.push(newFileNode);
|
||||
}
|
||||
}
|
||||
|
||||
showFileDialog.value = false;
|
||||
parentDirectoryForNewItems.value = null; // Clear parent context after creation
|
||||
currentEditingItem.value = null;
|
||||
};
|
||||
|
||||
const goBack = async () => {
|
||||
router.push('/templates');
|
||||
};
|
||||
|
||||
// Unified method to edit items (both files and directories)
|
||||
const editItem = (item: TreeNode) => {
|
||||
if (item.type === 'directory') {
|
||||
showEditDirectoryDialog(item);
|
||||
} else {
|
||||
showEditFileDialog(item);
|
||||
}
|
||||
};
|
||||
|
||||
// Show dialog to edit directory
|
||||
const showEditDirectoryDialog = (directory: TreeNode) => {
|
||||
// Set up for editing an existing directory
|
||||
currentEditingItem.value = directory;
|
||||
parentDirectoryForNewItems.value = null;
|
||||
showDirectoryDialog.value = true;
|
||||
};
|
||||
|
||||
// Show dialog to edit file
|
||||
const showEditFileDialog = (file: TreeNode) => {
|
||||
// Set up for editing an existing file
|
||||
currentEditingItem.value = file;
|
||||
parentDirectoryForNewItems.value = null;
|
||||
showFileDialog.value = true;
|
||||
};
|
||||
|
||||
const deleteItem = async (item: TreeNode) => {
|
||||
const confirmed = await ConfirmDialogService.confirm({
|
||||
title: '删除确认',
|
||||
message: '确定要删除此项吗?',
|
||||
confirmText: '删除',
|
||||
cancelText: '取消',
|
||||
confirmColor: 'error',
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 从树状结构中删除项目
|
||||
treeStructure.value = removeItemFromTree(treeStructure.value, item.id);
|
||||
};
|
||||
|
||||
// 从树状结构中删除项目的辅助函数
|
||||
const removeItemFromTree = (nodes: TreeNode[], itemId: string): TreeNode[] => {
|
||||
return nodes
|
||||
.filter(node => node.id !== itemId) // 过滤掉要删除的节点
|
||||
.map((node) => {
|
||||
if (node.children) {
|
||||
return {
|
||||
...node,
|
||||
children: removeItemFromTree(node.children, itemId),
|
||||
};
|
||||
}
|
||||
return node;
|
||||
});
|
||||
};
|
||||
|
||||
const saveTemplate = () => {
|
||||
if (!formValid.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const templateData = {
|
||||
name: templateName.value,
|
||||
description: templateDescription.value,
|
||||
treeStructure: treeStructure.value,
|
||||
};
|
||||
|
||||
if (isEditMode.value) {
|
||||
const templateId = route.params.id as string;
|
||||
templateStore.updateTemplate({
|
||||
id: templateId,
|
||||
...templateData,
|
||||
});
|
||||
} else {
|
||||
templateStore.addTemplate(templateData);
|
||||
}
|
||||
|
||||
router.push('/templates');
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-container
|
||||
fluid
|
||||
@@ -61,10 +425,10 @@
|
||||
<div class="d-flex ga-2">
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="saveTemplate"
|
||||
:disabled="!formValid"
|
||||
prepend-icon="mdi-content-save"
|
||||
block
|
||||
@click="saveTemplate"
|
||||
>
|
||||
保存模板
|
||||
</v-btn>
|
||||
@@ -77,13 +441,15 @@
|
||||
class="pa-4"
|
||||
>
|
||||
<div class="d-flex align-center mb-4">
|
||||
<h3 class="text-h6 mr-2">模板结构</h3>
|
||||
<h3 class="text-h6 mr-2">
|
||||
模板结构
|
||||
</h3>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
icon
|
||||
variant="text"
|
||||
@click="addRootDirectory"
|
||||
size="small"
|
||||
@click="addRootDirectory"
|
||||
>
|
||||
<v-icon>mdi-folder-plus</v-icon>
|
||||
<v-tooltip
|
||||
@@ -96,8 +462,8 @@
|
||||
<v-btn
|
||||
icon
|
||||
variant="text"
|
||||
@click="addRootFile"
|
||||
size="small"
|
||||
@click="addRootFile"
|
||||
>
|
||||
<v-icon>mdi-file-plus</v-icon>
|
||||
<v-tooltip
|
||||
@@ -107,14 +473,13 @@
|
||||
添加根文件
|
||||
</v-tooltip>
|
||||
</v-btn>
|
||||
|
||||
</div>
|
||||
|
||||
<TemplateTree
|
||||
v-model:expanded="expandedNodes"
|
||||
:items="treeStructure"
|
||||
:height="treeHeight"
|
||||
v-model:expanded="expandedNodes"
|
||||
:empty-text="'暂无文件或目录'"
|
||||
empty-text="暂无文件或目录"
|
||||
:read-only="false"
|
||||
@add-new-item-to-current-directory="addNewItemToCurrentDirectory"
|
||||
@edit-item="editItem"
|
||||
@@ -141,387 +506,9 @@
|
||||
:editing-item="currentEditingItem"
|
||||
@confirm="handleFileDialogConfirm"
|
||||
/>
|
||||
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useTemplateStore } from '@/renderer/store/template'
|
||||
import { TreeNode } from '@/types'
|
||||
import { useSnackbarStore } from '@/renderer/store/snackbar'
|
||||
import TemplateTree from '@/renderer/components/TemplateTree.vue'
|
||||
import DirectoryDialog from '@/renderer/components/DirectoryDialog.vue'
|
||||
import FileDialog from '@/renderer/components/FileDialog.vue'
|
||||
import { ConfirmDialogService } from '@/renderer/utils/confirm-dialog-service'
|
||||
import { generateUniqueId } from '@/renderer/utils'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const templateStore = useTemplateStore()
|
||||
const snackbar = useSnackbarStore()
|
||||
|
||||
// Extract methods from store
|
||||
const { getTemplateById } = templateStore
|
||||
|
||||
// Check if we're copying a template
|
||||
const isCopyingTemplate = computed(() => !route.params.id && !!route.query.copyId)
|
||||
|
||||
// Form state
|
||||
const formValid = ref(false)
|
||||
const templateName = ref('')
|
||||
const templateDescription = ref('')
|
||||
|
||||
// Dialogs
|
||||
const showFileDialog = ref(false)
|
||||
|
||||
// Directory dialog state
|
||||
const showDirectoryDialog = ref(false)
|
||||
const parentDirectoryForNewItems = ref<TreeNode | null>(null)
|
||||
const currentEditingItem = ref<TreeNode | null>(null)
|
||||
|
||||
|
||||
// Tree state
|
||||
const expandedNodes = ref<string[]>([])
|
||||
const treeHeight = ref(600)
|
||||
|
||||
|
||||
|
||||
// Item state - 使用树状结构
|
||||
const treeStructure = ref<TreeNode[]>([])
|
||||
|
||||
// Computed
|
||||
const isEditMode = computed(() => !!route.params.id)
|
||||
|
||||
|
||||
|
||||
// Validation rules
|
||||
const requiredRule = (fieldName: string) => (v: string | null | undefined) =>
|
||||
!!v || `${fieldName}为必填项`
|
||||
|
||||
|
||||
// Lifecycle
|
||||
onMounted(async () => {
|
||||
await templateStore.loadTemplates()
|
||||
|
||||
// Set tree height based on window height
|
||||
updateTreeHeight()
|
||||
|
||||
// Add resize listener
|
||||
window.addEventListener('resize', updateTreeHeight)
|
||||
|
||||
// Handle edit mode
|
||||
if (isEditMode.value) {
|
||||
const templateId = route.params.id as string
|
||||
const template = getTemplateById(templateId)
|
||||
|
||||
if (template) {
|
||||
templateName.value = template.name
|
||||
templateDescription.value = template.description
|
||||
treeStructure.value = [...template.treeStructure]
|
||||
} else {
|
||||
router.push('/templates')
|
||||
}
|
||||
} else if (isCopyingTemplate.value) {
|
||||
// Handle copy mode
|
||||
const copyId = route.query.copyId as string
|
||||
if (copyId) {
|
||||
const template = getTemplateById(copyId)
|
||||
if (template) {
|
||||
templateName.value = `${template.name} - 副本`
|
||||
templateDescription.value = template.description
|
||||
treeStructure.value = [...template.treeStructure]
|
||||
} else {
|
||||
snackbar.error('找不到要复制的模板')
|
||||
router.push('/templates')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', updateTreeHeight)
|
||||
})
|
||||
|
||||
// Methods
|
||||
const updateTreeHeight = () => {
|
||||
treeHeight.value = window.innerHeight - 200
|
||||
}
|
||||
|
||||
const addRootDirectory = () => {
|
||||
parentDirectoryForNewItems.value = null
|
||||
currentEditingItem.value = null
|
||||
showDirectoryDialog.value = true
|
||||
}
|
||||
|
||||
const addRootFile = () => {
|
||||
parentDirectoryForNewItems.value = null
|
||||
currentEditingItem.value = null
|
||||
showFileDialog.value = true
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Add a file directly to a specific directory
|
||||
const addFileToDirectory = (directoryItem: TreeNode) => {
|
||||
parentDirectoryForNewItems.value = directoryItem // Store the directory node as parent
|
||||
currentEditingItem.value = null
|
||||
showFileDialog.value = true
|
||||
}
|
||||
|
||||
const addNewItemToCurrentDirectory = (directoryItem: TreeNode) => {
|
||||
parentDirectoryForNewItems.value = directoryItem // Store the directory node as parent
|
||||
currentEditingItem.value = null
|
||||
showDirectoryDialog.value = true
|
||||
}
|
||||
|
||||
const handleDirectoryDialogConfirm = (data: { path: string }) => {
|
||||
const { path } = data
|
||||
const newId = generateUniqueId('dir')
|
||||
|
||||
// Check if we're editing an existing directory
|
||||
if (
|
||||
currentEditingItem.value &&
|
||||
currentEditingItem.value.type === 'directory'
|
||||
) {
|
||||
// Find and update the existing directory in the tree
|
||||
const updatedNode: TreeNode = {
|
||||
...currentEditingItem.value,
|
||||
name: path,
|
||||
path: path,
|
||||
type: 'directory'
|
||||
}
|
||||
treeStructure.value = updateNodeInTree(
|
||||
treeStructure.value,
|
||||
currentEditingItem.value.id,
|
||||
updatedNode
|
||||
)
|
||||
} else {
|
||||
// Create new directory node
|
||||
const newNode: TreeNode = {
|
||||
id: newId,
|
||||
name: path,
|
||||
path: path,
|
||||
type: 'directory'
|
||||
}
|
||||
|
||||
// Add to tree structure
|
||||
if (parentDirectoryForNewItems.value) {
|
||||
// In the parent directory, check if a node with the same name already exists
|
||||
const parentNode = parentDirectoryForNewItems.value
|
||||
if (parentNode.children) {
|
||||
const duplicateExists = parentNode.children.some(
|
||||
(child) => child.name === path
|
||||
)
|
||||
if (duplicateExists) {
|
||||
snackbar.error('当前目录下已存在同名节点!')
|
||||
return
|
||||
}
|
||||
}
|
||||
// Add as child to the parent directory
|
||||
parentNode.children = parentNode.children || []
|
||||
parentNode.children.push(newNode)
|
||||
} else {
|
||||
// Check at root level for duplicates
|
||||
const duplicateExists = treeStructure.value.some(
|
||||
(node) => node.name === path
|
||||
)
|
||||
if (duplicateExists) {
|
||||
snackbar.error('根目录下已存在同名节点!')
|
||||
return
|
||||
}
|
||||
// Add as root node
|
||||
treeStructure.value.push(newNode)
|
||||
}
|
||||
}
|
||||
|
||||
showDirectoryDialog.value = false
|
||||
parentDirectoryForNewItems.value = null // Clear parent context after creation
|
||||
currentEditingItem.value = null
|
||||
|
||||
|
||||
}
|
||||
|
||||
// 在树状结构中更新节点的辅助函数
|
||||
const updateNodeInTree = (
|
||||
nodes: TreeNode[],
|
||||
oldId: string,
|
||||
updatedNode: TreeNode
|
||||
): TreeNode[] => {
|
||||
return nodes.map((node) => {
|
||||
if (node.id === oldId) {
|
||||
return updatedNode
|
||||
}
|
||||
if (node.children) {
|
||||
return {
|
||||
...node,
|
||||
children: updateNodeInTree(node.children, oldId, updatedNode)
|
||||
}
|
||||
}
|
||||
return node
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
const handleFileDialogConfirm = (data: { path: string; sourcePath?: string; sourceType?: 'file' | 'folder' }) => {
|
||||
const { path, sourcePath, sourceType } = data
|
||||
|
||||
const newId = generateUniqueId('file')
|
||||
|
||||
// Check if we're editing an existing file
|
||||
if (currentEditingItem.value && currentEditingItem.value.type === 'file') {
|
||||
// Find and update the existing file in the tree
|
||||
const updatedNode: TreeNode = {
|
||||
id: currentEditingItem.value.id,
|
||||
name: path,
|
||||
path: path,
|
||||
type: 'file',
|
||||
sourcePath: sourcePath,
|
||||
sourceType
|
||||
}
|
||||
treeStructure.value = updateNodeInTree(
|
||||
treeStructure.value,
|
||||
currentEditingItem.value.id,
|
||||
updatedNode
|
||||
)
|
||||
} else {
|
||||
// Check for duplicate names in the same parent directory
|
||||
if (parentDirectoryForNewItems.value) {
|
||||
// In the parent directory, check if a node with the same name already exists
|
||||
const parentNode = parentDirectoryForNewItems.value
|
||||
if (parentNode.children) {
|
||||
const duplicateExists = parentNode.children.some(
|
||||
(child) => child.name === path
|
||||
)
|
||||
if (duplicateExists) {
|
||||
snackbar.error('当前目录下已存在同名节点!')
|
||||
return
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Check at root level for duplicates
|
||||
const duplicateExists = treeStructure.value.some(
|
||||
(node) => node.name === path
|
||||
)
|
||||
if (duplicateExists) {
|
||||
snackbar.error('根目录下已存在同名节点!')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const newFileNode: TreeNode = {
|
||||
id: newId,
|
||||
name: path,
|
||||
path: path,
|
||||
type: 'file',
|
||||
sourcePath,
|
||||
sourceType
|
||||
}
|
||||
|
||||
// Add to tree structure
|
||||
if (parentDirectoryForNewItems.value) {
|
||||
// Add as child to the parent directory
|
||||
const parentNode = parentDirectoryForNewItems.value
|
||||
parentNode.children = parentNode.children || []
|
||||
parentNode.children.push(newFileNode)
|
||||
} else {
|
||||
// Add as root node
|
||||
treeStructure.value.push(newFileNode)
|
||||
}
|
||||
}
|
||||
|
||||
showFileDialog.value = false
|
||||
parentDirectoryForNewItems.value = null // Clear parent context after creation
|
||||
currentEditingItem.value = null
|
||||
}
|
||||
|
||||
const goBack = async () => {
|
||||
router.push('/templates')
|
||||
}
|
||||
|
||||
|
||||
// Unified method to edit items (both files and directories)
|
||||
const editItem = (item: TreeNode) => {
|
||||
if (item.type === 'directory') {
|
||||
showEditDirectoryDialog(item)
|
||||
} else {
|
||||
showEditFileDialog(item)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Show dialog to edit directory
|
||||
const showEditDirectoryDialog = (directory: TreeNode) => {
|
||||
// Set up for editing an existing directory
|
||||
currentEditingItem.value = directory
|
||||
parentDirectoryForNewItems.value = null
|
||||
showDirectoryDialog.value = true
|
||||
}
|
||||
|
||||
// Show dialog to edit file
|
||||
const showEditFileDialog = (file: TreeNode) => {
|
||||
// Set up for editing an existing file
|
||||
currentEditingItem.value = file
|
||||
parentDirectoryForNewItems.value = null
|
||||
showFileDialog.value = true
|
||||
}
|
||||
|
||||
const deleteItem = async (item: TreeNode) => {
|
||||
const confirmed = await ConfirmDialogService.confirm({
|
||||
title: '删除确认',
|
||||
message: '确定要删除此项吗?',
|
||||
confirmText: '删除',
|
||||
cancelText: '取消',
|
||||
confirmColor: 'error'
|
||||
})
|
||||
|
||||
if (!confirmed) return
|
||||
|
||||
// 从树状结构中删除项目
|
||||
treeStructure.value = removeItemFromTree(treeStructure.value, item.id)
|
||||
}
|
||||
|
||||
// 从树状结构中删除项目的辅助函数
|
||||
const removeItemFromTree = (nodes: TreeNode[], itemId: string): TreeNode[] => {
|
||||
return nodes
|
||||
.filter((node) => node.id !== itemId) // 过滤掉要删除的节点
|
||||
.map((node) => {
|
||||
if (node.children) {
|
||||
return {
|
||||
...node,
|
||||
children: removeItemFromTree(node.children, itemId)
|
||||
}
|
||||
}
|
||||
return node
|
||||
})
|
||||
}
|
||||
|
||||
const saveTemplate = () => {
|
||||
if (!formValid.value) return
|
||||
|
||||
const templateData = {
|
||||
name: templateName.value,
|
||||
description: templateDescription.value,
|
||||
treeStructure: treeStructure.value
|
||||
}
|
||||
|
||||
if (isEditMode.value) {
|
||||
const templateId = route.params.id as string
|
||||
templateStore.updateTemplate({
|
||||
id: templateId,
|
||||
...templateData
|
||||
})
|
||||
} else {
|
||||
templateStore.addTemplate(templateData)
|
||||
}
|
||||
|
||||
router.push('/templates')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.border-e {
|
||||
border-right: 1px solid rgba(0, 0, 0, 0.12);
|
||||
|
||||
@@ -1,79 +1,59 @@
|
||||
<script setup lang="tsx">
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useTheme } from 'vuetify'
|
||||
import { openExternal, openFile } from '@/renderer/utils'
|
||||
import { useCounterStore } from '@/renderer/store/counter'
|
||||
import { onMounted, ref, onBeforeMount } from 'vue'
|
||||
import {
|
||||
mdiBrightness6,
|
||||
mdiFileDocument,
|
||||
mdiFolderOpen,
|
||||
mdiGithub,
|
||||
mdiPlusCircle,
|
||||
mdiFolderMultiple,
|
||||
mdiFileDocumentMultiple,
|
||||
mdiArrowRight,
|
||||
mdiHome
|
||||
} from '@mdi/js'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useRouter } from 'vue-router'
|
||||
mdiGithub,
|
||||
} from '@mdi/js';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useTheme } from 'vuetify';
|
||||
import { openExternal } from '@/renderer/utils';
|
||||
|
||||
const router = useRouter()
|
||||
const { t, locale, availableLocales } = useI18n()
|
||||
const { increaseCount } = useCounterStore()
|
||||
const { count } = storeToRefs(useCounterStore())
|
||||
const theme = useTheme()
|
||||
const languages = ref(['en'])
|
||||
const appVersion = ref('Unknown')
|
||||
const selectedFile = ref('')
|
||||
const router = useRouter();
|
||||
const { t, locale, availableLocales } = useI18n();
|
||||
const theme = useTheme();
|
||||
const languages = ref(['en']);
|
||||
const appVersion = ref('Unknown');
|
||||
const selectedFile = ref('');
|
||||
|
||||
onMounted((): void => {
|
||||
languages.value = availableLocales
|
||||
languages.value = availableLocales;
|
||||
|
||||
// Get application version from package.json version string (Using IPC communication)
|
||||
getApplicationVersionFromMainProcess()
|
||||
})
|
||||
getApplicationVersionFromMainProcess();
|
||||
});
|
||||
|
||||
const getApplicationVersionFromMainProcess = (): void => {
|
||||
window.mainApi.invoke('msgRequestGetVersion').then((result: string) => {
|
||||
appVersion.value = result
|
||||
})
|
||||
}
|
||||
appVersion.value = result;
|
||||
});
|
||||
};
|
||||
|
||||
const handleChangeTheme = (): void => {
|
||||
theme.global.name.value = theme.global.current.value.dark ? 'light' : 'dark'
|
||||
}
|
||||
theme.global.name.value = theme.global.current.value.dark ? 'light' : 'dark';
|
||||
};
|
||||
|
||||
const handleChangeLanguage = (val): void => {
|
||||
locale.value = val
|
||||
}
|
||||
locale.value = val;
|
||||
};
|
||||
|
||||
const handleOpenDocument = async (): Promise<void> => {
|
||||
await openExternal('https://vutron.cdget.com')
|
||||
}
|
||||
await openExternal('https://vutron.cdget.com');
|
||||
};
|
||||
|
||||
const handleOpenGitHub = async (): Promise<void> => {
|
||||
await openExternal('https://github.com/jooy2/vutron')
|
||||
}
|
||||
|
||||
const handleCountIncrease = (): void => {
|
||||
increaseCount(1)
|
||||
}
|
||||
|
||||
const handleOpenFile = async () => {
|
||||
const dialogResult = await openFile('text')
|
||||
if (!dialogResult.canceled) {
|
||||
selectedFile.value = dialogResult.filePaths[0]
|
||||
}
|
||||
}
|
||||
await openExternal('https://github.com/jooy2/vutron');
|
||||
};
|
||||
|
||||
const navigateToTemplates = () => {
|
||||
router.push('/templates')
|
||||
}
|
||||
router.push('/templates');
|
||||
};
|
||||
|
||||
const navigateToCreateTemplate = () => {
|
||||
router.push('/create-template')
|
||||
}
|
||||
router.push('/create-template');
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -103,7 +83,7 @@ const navigateToCreateTemplate = () => {
|
||||
<p v-if="selectedFile">
|
||||
{{
|
||||
t('desc.selected-file', {
|
||||
filePath: selectedFile
|
||||
filePath: selectedFile,
|
||||
})
|
||||
}}
|
||||
</p>
|
||||
@@ -112,17 +92,17 @@ const navigateToCreateTemplate = () => {
|
||||
<v-btn
|
||||
color="primary"
|
||||
size="x-large"
|
||||
@click="navigateToCreateTemplate"
|
||||
class="mr-4"
|
||||
prepend-icon="mdi-plus"
|
||||
@click="navigateToCreateTemplate"
|
||||
>
|
||||
创建模板
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="success"
|
||||
size="x-large"
|
||||
@click="navigateToTemplates"
|
||||
prepend-icon="mdi-folder-multiple"
|
||||
@click="navigateToTemplates"
|
||||
>
|
||||
管理模板
|
||||
</v-btn>
|
||||
@@ -136,7 +116,9 @@ const navigateToCreateTemplate = () => {
|
||||
<v-expansion-panels variant="accordion">
|
||||
<v-expansion-panel>
|
||||
<v-expansion-panel-title>
|
||||
<v-icon start>{{ mdiFolderMultiple }}</v-icon>
|
||||
<v-icon start>
|
||||
{{ mdiFolderMultiple }}
|
||||
</v-icon>
|
||||
如何使用模板生成器
|
||||
</v-expansion-panel-title>
|
||||
<v-expansion-panel-text>
|
||||
@@ -172,8 +154,8 @@ const navigateToCreateTemplate = () => {
|
||||
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="handleOpenDocument"
|
||||
class="ml-2"
|
||||
@click="handleOpenDocument"
|
||||
>
|
||||
<v-icon :icon="mdiFileDocument" start />
|
||||
{{ t('menu.documentation') }}
|
||||
@@ -181,8 +163,8 @@ const navigateToCreateTemplate = () => {
|
||||
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="handleOpenGitHub"
|
||||
class="ml-2"
|
||||
@click="handleOpenGitHub"
|
||||
>
|
||||
<v-icon :icon="mdiGithub" start />
|
||||
{{ t('menu.github') }}
|
||||
@@ -199,8 +181,8 @@ const navigateToCreateTemplate = () => {
|
||||
density="compact"
|
||||
:label="t('menu.change-language')"
|
||||
:items="languages"
|
||||
@update:model-value="handleChangeLanguage"
|
||||
class="ml-2"
|
||||
@update:model-value="handleChangeLanguage"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script setup lang="tsx">
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { mdiEmoticonCoolOutline } from '@mdi/js'
|
||||
import { mdiEmoticonCoolOutline } from '@mdi/js';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n()
|
||||
const { t } = useI18n();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,10 +1,139 @@
|
||||
<script setup lang="ts">
|
||||
import type { Template, TreeNode } from '@/types';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import TemplateCard from '@/renderer/components/TemplateCard.vue';
|
||||
import TemplateTree from '@/renderer/components/TemplateTree.vue';
|
||||
import { useTemplateStore } from '@/renderer/store/template';
|
||||
import { ConfirmDialogService } from '@/renderer/utils/confirm-dialog-service';
|
||||
|
||||
const router = useRouter();
|
||||
const templateStore = useTemplateStore();
|
||||
|
||||
// Initialize templates
|
||||
onMounted(async () => {
|
||||
await templateStore.loadTemplates();
|
||||
});
|
||||
|
||||
const { deleteTemplate: storeDeleteTemplate } = templateStore;
|
||||
|
||||
const templates = computed(() => templateStore.templates);
|
||||
|
||||
// Template detail view
|
||||
const showTemplateDetail = ref(false);
|
||||
const selectedTemplate = ref<Template | null>(null);
|
||||
const expandedTreeNodes = ref<string[]>([]);
|
||||
|
||||
// Execute template
|
||||
const showExecuteDialog = ref(false);
|
||||
const executePath = ref('');
|
||||
const baseDirNames = ref('');
|
||||
const executeFormValid = ref(false);
|
||||
|
||||
const templateTree = computed<TreeNode[]>(() => {
|
||||
if (!selectedTemplate.value) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 直接返回模板的树状结构
|
||||
return selectedTemplate.value.treeStructure || [];
|
||||
});
|
||||
|
||||
const navigateToCreateTemplate = () => {
|
||||
router.push('/create-template');
|
||||
};
|
||||
|
||||
const viewTemplate = (template: Template) => {
|
||||
selectedTemplate.value = template;
|
||||
showTemplateDetail.value = true;
|
||||
};
|
||||
|
||||
const editTemplate = (template: Template) => {
|
||||
router.push(`/create-template/${template.id}`);
|
||||
};
|
||||
|
||||
const copyTemplate = (template: Template) => {
|
||||
router.push(`/create-template?copyId=${template.id}`);
|
||||
};
|
||||
|
||||
const deleteTemplate = async (template: Template) => {
|
||||
const confirmed = await ConfirmDialogService.confirm({
|
||||
title: '删除模板',
|
||||
message: `确定要删除模板 "${template.name}" 吗?此操作不可撤销。`,
|
||||
confirmText: '删除',
|
||||
cancelText: '取消',
|
||||
confirmColor: 'error',
|
||||
});
|
||||
|
||||
if (confirmed) {
|
||||
storeDeleteTemplate(template.id);
|
||||
}
|
||||
};
|
||||
|
||||
const executeTemplate = (template: Template) => {
|
||||
selectedTemplate.value = template;
|
||||
executePath.value = '';
|
||||
baseDirNames.value = '';
|
||||
showExecuteDialog.value = true;
|
||||
};
|
||||
|
||||
const selectDirectory = async () => {
|
||||
const result = await window.mainApi.invoke('msgSelectDirectory');
|
||||
if (!result.canceled) {
|
||||
executePath.value = result.filePath;
|
||||
}
|
||||
};
|
||||
|
||||
const confirmExecuteTemplate = () => {
|
||||
if (!executePath.value || !baseDirNames.value.trim() || !selectedTemplate.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dirNames = baseDirNames.value.split('\n').filter(name => name.trim());
|
||||
|
||||
// Call main process to execute template
|
||||
window.mainApi
|
||||
.invoke(
|
||||
'msgExecuteTemplate',
|
||||
selectedTemplate.value.id,
|
||||
executePath.value,
|
||||
dirNames,
|
||||
)
|
||||
.then(async (result: any) => {
|
||||
if (result.success) {
|
||||
await ConfirmDialogService.success({
|
||||
title: '执行成功',
|
||||
message: '模板执行成功!',
|
||||
confirmText: '确定',
|
||||
});
|
||||
showExecuteDialog.value = false;
|
||||
} else {
|
||||
await ConfirmDialogService.error({
|
||||
title: '执行失败',
|
||||
message: `模板执行失败: ${result.error}`,
|
||||
confirmText: '确定',
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(async (error: any) => {
|
||||
await ConfirmDialogService.error({
|
||||
title: '执行出错',
|
||||
message: `模板执行出错: ${error.message}`,
|
||||
confirmText: '确定',
|
||||
});
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-container>
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<div class="d-flex align-center mb-4 justify-space-between flex-wrap">
|
||||
<h2 class="mr-2 mb-2"> 文件创建 </h2>
|
||||
<v-btn @click="navigateToCreateTemplate" color="primary" prepend-icon="mdi-plus" class="mb-2">
|
||||
<h2 class="mr-2 mb-2">
|
||||
文件创建
|
||||
</h2>
|
||||
<v-btn color="primary" prepend-icon="mdi-plus" class="mb-2" @click="navigateToCreateTemplate">
|
||||
创建模板
|
||||
</v-btn>
|
||||
</div>
|
||||
@@ -18,7 +147,7 @@
|
||||
<p>还没有创建任何模板。创建您的第一个模板以开始批量生成文件和目录。</p>
|
||||
</v-card-text>
|
||||
<v-card-actions class="justify-center">
|
||||
<v-btn color="primary" size="default" @click="navigateToCreateTemplate" prepend-icon="mdi-plus">
|
||||
<v-btn color="primary" size="default" prepend-icon="mdi-plus" @click="navigateToCreateTemplate">
|
||||
创建模板
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
@@ -54,7 +183,7 @@
|
||||
:items="templateTree"
|
||||
:height="400"
|
||||
:read-only="true"
|
||||
:empty-text="'模板结构为空'"
|
||||
empty-text="模板结构为空"
|
||||
:expanded="expandedTreeNodes"
|
||||
@update:expanded="expandedTreeNodes = $event"
|
||||
/>
|
||||
@@ -68,147 +197,26 @@
|
||||
<v-card-title>执行模板: {{ selectedTemplate?.name }}</v-card-title>
|
||||
<v-card-text>
|
||||
<v-form v-model="executeFormValid">
|
||||
<v-text-field v-model="executePath" label="选择目录" readonly @click="selectDirectory"
|
||||
prepend-inner-icon="mdi-folder" :rules="[(v) => !!v || '请选择目录']" />
|
||||
<v-textarea v-model="baseDirNames" label="基础目录名 (每行一个,支持批量)" placeholder="输入基础目录名,每行一个"
|
||||
:rules="[(v) => !!v || '请输入基础目录名']" />
|
||||
<v-text-field
|
||||
v-model="executePath" label="选择目录" readonly prepend-inner-icon="mdi-folder"
|
||||
:rules="[(v) => !!v || '请选择目录']" @click="selectDirectory"
|
||||
/>
|
||||
<v-textarea
|
||||
v-model="baseDirNames" label="基础目录名 (每行一个,支持批量)" placeholder="输入基础目录名,每行一个"
|
||||
:rules="[(v) => !!v || '请输入基础目录名']"
|
||||
/>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn @click="showExecuteDialog = false"> 取消 </v-btn>
|
||||
<v-btn color="success" @click="confirmExecuteTemplate" :disabled="!executeFormValid">
|
||||
<v-btn @click="showExecuteDialog = false">
|
||||
取消
|
||||
</v-btn>
|
||||
<v-btn color="success" :disabled="!executeFormValid" @click="confirmExecuteTemplate">
|
||||
确认执行
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useTemplateStore } from '@/renderer/store/template'
|
||||
import { TreeNode, Template } from '@/types'
|
||||
import TemplateTree from '@/renderer/components/TemplateTree.vue'
|
||||
import TemplateCard from '@/renderer/components/TemplateCard.vue'
|
||||
import { ConfirmDialogService } from '@/renderer/utils/confirm-dialog-service'
|
||||
|
||||
const router = useRouter()
|
||||
const templateStore = useTemplateStore()
|
||||
|
||||
// Initialize templates
|
||||
onMounted(async () => {
|
||||
await templateStore.loadTemplates()
|
||||
})
|
||||
|
||||
const { deleteTemplate: storeDeleteTemplate } = templateStore
|
||||
|
||||
const templates = computed(() => templateStore.templates)
|
||||
|
||||
// Template detail view
|
||||
const showTemplateDetail = ref(false)
|
||||
const selectedTemplate = ref<Template | null>(null)
|
||||
const expandedTreeNodes = ref<string[]>([])
|
||||
|
||||
// Execute template
|
||||
const showExecuteDialog = ref(false)
|
||||
const executePath = ref('')
|
||||
const baseDirNames = ref('')
|
||||
const executeFormValid = ref(false)
|
||||
|
||||
|
||||
|
||||
const templateTree = computed<TreeNode[]>(() => {
|
||||
if (!selectedTemplate.value) return []
|
||||
|
||||
// 直接返回模板的树状结构
|
||||
return selectedTemplate.value.treeStructure || []
|
||||
})
|
||||
|
||||
const navigateToCreateTemplate = () => {
|
||||
router.push('/create-template')
|
||||
}
|
||||
|
||||
const viewTemplate = (template: Template) => {
|
||||
selectedTemplate.value = template
|
||||
showTemplateDetail.value = true
|
||||
}
|
||||
|
||||
const editTemplate = (template: Template) => {
|
||||
router.push(`/create-template/${template.id}`)
|
||||
}
|
||||
|
||||
const copyTemplate = (template: Template) => {
|
||||
router.push(`/create-template?copyId=${template.id}`)
|
||||
}
|
||||
|
||||
|
||||
const deleteTemplate = async (template: Template) => {
|
||||
const confirmed = await ConfirmDialogService.confirm({
|
||||
title: '删除模板',
|
||||
message: `确定要删除模板 "${template.name}" 吗?此操作不可撤销。`,
|
||||
confirmText: '删除',
|
||||
cancelText: '取消',
|
||||
confirmColor: 'error'
|
||||
})
|
||||
|
||||
if (confirmed) {
|
||||
storeDeleteTemplate(template.id)
|
||||
}
|
||||
}
|
||||
|
||||
const executeTemplate = (template: Template) => {
|
||||
selectedTemplate.value = template
|
||||
executePath.value = ''
|
||||
baseDirNames.value = ''
|
||||
showExecuteDialog.value = true
|
||||
}
|
||||
|
||||
const selectDirectory = async () => {
|
||||
const result = await window.mainApi.invoke('msgSelectDirectory')
|
||||
if (!result.canceled) {
|
||||
executePath.value = result.filePath
|
||||
}
|
||||
}
|
||||
|
||||
const confirmExecuteTemplate = () => {
|
||||
if (!executePath.value || !baseDirNames.value.trim() || !selectedTemplate.value) return
|
||||
|
||||
const dirNames = baseDirNames.value.split('\n').filter((name) => name.trim())
|
||||
|
||||
// Call main process to execute template
|
||||
window.mainApi
|
||||
.invoke(
|
||||
'msgExecuteTemplate',
|
||||
selectedTemplate.value.id,
|
||||
executePath.value,
|
||||
dirNames
|
||||
)
|
||||
.then(async (result: any) => {
|
||||
if (result.success) {
|
||||
await ConfirmDialogService.success({
|
||||
title: '执行成功',
|
||||
message: '模板执行成功!',
|
||||
confirmText: '确定'
|
||||
})
|
||||
showExecuteDialog.value = false
|
||||
} else {
|
||||
await ConfirmDialogService.error({
|
||||
title: '执行失败',
|
||||
message: `模板执行失败: ${result.error}`,
|
||||
confirmText: '确定'
|
||||
})
|
||||
}
|
||||
})
|
||||
.catch(async (error: any) => {
|
||||
await ConfirmDialogService.error({
|
||||
title: '执行出错',
|
||||
message: `模板执行出错: ${error.message}`,
|
||||
confirmText: '确定'
|
||||
})
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@@ -1,13 +1,13 @@
|
||||
import ErrorScreen from '@/renderer/screens/ErrorScreen.vue'
|
||||
import MainScreen from '@/renderer/screens/MainScreen.vue'
|
||||
import SecondScreen from '@/renderer/screens/SecondScreen.vue'
|
||||
import TemplateListScreen from '@/renderer/screens/TemplateListScreen.vue'
|
||||
import CreateTemplateScreen from '@/renderer/screens/CreateTemplateScreen.vue'
|
||||
import CreateTemplateScreen from '@/renderer/screens/CreateTemplateScreen.vue';
|
||||
import ErrorScreen from '@/renderer/screens/ErrorScreen.vue';
|
||||
import MainScreen from '@/renderer/screens/MainScreen.vue';
|
||||
import SecondScreen from '@/renderer/screens/SecondScreen.vue';
|
||||
import TemplateListScreen from '@/renderer/screens/TemplateListScreen.vue';
|
||||
|
||||
export {
|
||||
CreateTemplateScreen,
|
||||
ErrorScreen,
|
||||
MainScreen,
|
||||
SecondScreen,
|
||||
TemplateListScreen,
|
||||
CreateTemplateScreen
|
||||
}
|
||||
};
|
||||
+26
-26
@@ -1,58 +1,58 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { defineStore } from 'pinia';
|
||||
|
||||
export interface UpdateStatus {
|
||||
status: 'idle' | 'checking' | 'available' | 'not-available' | 'downloading' | 'downloaded' | 'error'
|
||||
currentVersion?: string
|
||||
error?: string
|
||||
progress?: number
|
||||
status: 'idle' | 'checking' | 'available' | 'not-available' | 'downloading' | 'downloaded' | 'error';
|
||||
currentVersion?: string;
|
||||
error?: string;
|
||||
progress?: number;
|
||||
}
|
||||
|
||||
export const useAppStore = defineStore('app', {
|
||||
state: () => ({
|
||||
updateStatus: {
|
||||
status: 'idle' as const
|
||||
status: 'idle' as const,
|
||||
} as UpdateStatus,
|
||||
updateDialogVisible: false,
|
||||
downloadProgress: 0
|
||||
downloadProgress: 0,
|
||||
}),
|
||||
|
||||
getters: {
|
||||
isUpdateAvailable: (state) => state.updateStatus.status === 'available',
|
||||
isCheckingForUpdates: (state) => state.updateStatus.status === 'checking',
|
||||
hasUpdateError: (state) => state.updateStatus.status === 'error'
|
||||
isUpdateAvailable: state => state.updateStatus.status === 'available',
|
||||
isCheckingForUpdates: state => state.updateStatus.status === 'checking',
|
||||
hasUpdateError: state => state.updateStatus.status === 'error',
|
||||
},
|
||||
|
||||
actions: {
|
||||
setUpdateStatus(status: UpdateStatus['status'], currentVersion?: string, error?: string) {
|
||||
this.updateStatus = { status, currentVersion, error }
|
||||
this.updateStatus = { status, currentVersion, error };
|
||||
},
|
||||
|
||||
|
||||
showUpdateDialog() {
|
||||
this.updateDialogVisible = true
|
||||
this.updateDialogVisible = true;
|
||||
},
|
||||
|
||||
|
||||
hideUpdateDialog() {
|
||||
this.updateDialogVisible = false
|
||||
this.updateDialogVisible = false;
|
||||
},
|
||||
|
||||
|
||||
setDownloadProgress(progress: number) {
|
||||
this.downloadProgress = progress
|
||||
this.downloadProgress = progress;
|
||||
},
|
||||
|
||||
// 启动更新下载
|
||||
async startUpdateDownload() {
|
||||
try {
|
||||
this.setUpdateStatus('downloading')
|
||||
const result = await window.mainApi.invoke('msgDownloadUpdate')
|
||||
|
||||
this.setUpdateStatus('downloading');
|
||||
const result = await window.mainApi.invoke('msgDownloadUpdate');
|
||||
|
||||
if (result.success) {
|
||||
this.setUpdateStatus('downloaded')
|
||||
this.setUpdateStatus('downloaded');
|
||||
} else {
|
||||
this.setUpdateStatus('error', undefined, result.error)
|
||||
this.setUpdateStatus('error', undefined, result.error);
|
||||
}
|
||||
} catch (error) {
|
||||
this.setUpdateStatus('error', undefined, error instanceof Error ? error.message : String(error))
|
||||
this.setUpdateStatus('error', undefined, error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -1,15 +1,15 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { defineStore } from 'pinia';
|
||||
import { ref } from 'vue';
|
||||
|
||||
export const useCounterStore = defineStore('counter', () => {
|
||||
const count = ref(0)
|
||||
const count = ref(0);
|
||||
|
||||
function increaseCount(amount: number) {
|
||||
count.value += amount
|
||||
count.value += amount;
|
||||
}
|
||||
|
||||
return {
|
||||
count,
|
||||
increaseCount
|
||||
}
|
||||
})
|
||||
increaseCount,
|
||||
};
|
||||
});
|
||||
@@ -1,36 +1,36 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { defineStore } from 'pinia';
|
||||
import { ref } from 'vue';
|
||||
|
||||
export interface SnackbarAction {
|
||||
text: string
|
||||
color?: string
|
||||
handler?: () => void
|
||||
text: string;
|
||||
color?: string;
|
||||
handler?: () => void;
|
||||
}
|
||||
|
||||
export const useSnackbarStore = defineStore('snackbar', () => {
|
||||
const isVisible = ref(false)
|
||||
const message = ref('')
|
||||
const color = ref('info')
|
||||
const timeout = ref(3000)
|
||||
const actions = ref<SnackbarAction[]>([])
|
||||
const isVisible = ref(false);
|
||||
const message = ref('');
|
||||
const color = ref('info');
|
||||
const timeout = ref(3000);
|
||||
const actions = ref<SnackbarAction[]>([]);
|
||||
|
||||
const show = (msg: string, clr = 'info', tout = 3000, actionList: SnackbarAction[] = []) => {
|
||||
message.value = msg
|
||||
color.value = clr
|
||||
timeout.value = tout
|
||||
actions.value = actionList
|
||||
isVisible.value = true
|
||||
}
|
||||
message.value = msg;
|
||||
color.value = clr;
|
||||
timeout.value = tout;
|
||||
actions.value = actionList;
|
||||
isVisible.value = true;
|
||||
};
|
||||
|
||||
const success = (msg: string, tout = 3000) => show(msg, 'success', tout)
|
||||
const error = (msg: string, tout = 3000) => show(msg, 'error', tout)
|
||||
const warning = (msg: string, tout = 3000) => show(msg, 'warning', tout)
|
||||
const info = (msg: string, tout = 3000) => show(msg, 'info', tout)
|
||||
const success = (msg: string, tout = 3000) => show(msg, 'success', tout);
|
||||
const error = (msg: string, tout = 3000) => show(msg, 'error', tout);
|
||||
const warning = (msg: string, tout = 3000) => show(msg, 'warning', tout);
|
||||
const info = (msg: string, tout = 3000) => show(msg, 'info', tout);
|
||||
|
||||
const hide = () => {
|
||||
isVisible.value = false
|
||||
actions.value = []
|
||||
}
|
||||
isVisible.value = false;
|
||||
actions.value = [];
|
||||
};
|
||||
|
||||
return {
|
||||
isVisible,
|
||||
@@ -43,6 +43,6 @@ export const useSnackbarStore = defineStore('snackbar', () => {
|
||||
error,
|
||||
warning,
|
||||
info,
|
||||
hide
|
||||
}
|
||||
})
|
||||
hide,
|
||||
};
|
||||
});
|
||||
@@ -1,24 +1,24 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { TreeNode, Template } from '@/types'
|
||||
import type { Template } from '@/types';
|
||||
import { defineStore } from 'pinia';
|
||||
import { ref } from 'vue';
|
||||
|
||||
export const useTemplateStore = defineStore('template', () => {
|
||||
const templates = ref<Template[]>([])
|
||||
const dataFile = 'templates.json'
|
||||
const templates = ref<Template[]>([]);
|
||||
const dataFile = 'templates.json';
|
||||
|
||||
// Load templates from local storage or initialize
|
||||
async function loadTemplates() {
|
||||
try {
|
||||
const stored = await window.mainApi.invoke('msgLoadData', dataFile)
|
||||
const stored = await window.mainApi.invoke('msgLoadData', dataFile);
|
||||
if (stored) {
|
||||
templates.value = JSON.parse(stored as string) || []
|
||||
templates.value = JSON.parse(stored as string) || [];
|
||||
} else {
|
||||
// Initialize with sample data if no stored data
|
||||
templates.value = []
|
||||
templates.value = [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load templates:', error)
|
||||
templates.value = []
|
||||
console.error('Failed to load templates:', error);
|
||||
templates.value = [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,54 +28,54 @@ export const useTemplateStore = defineStore('template', () => {
|
||||
window.mainApi.invoke(
|
||||
'msgSaveData',
|
||||
dataFile,
|
||||
JSON.stringify(templates.value)
|
||||
)
|
||||
JSON.stringify(templates.value),
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Failed to save templates:', error)
|
||||
console.error('Failed to save templates:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Get all templates
|
||||
function getTemplates() {
|
||||
return templates.value
|
||||
return templates.value;
|
||||
}
|
||||
|
||||
// Get template by ID
|
||||
function getTemplateById(id: string) {
|
||||
return templates.value.find((template) => template.id === id)
|
||||
return templates.value.find(template => template.id === id);
|
||||
}
|
||||
|
||||
// Add new template
|
||||
function addTemplate(template: Omit<Template, 'id'>) {
|
||||
const newTemplate: Template = {
|
||||
id: `template_${Date.now()}`,
|
||||
...template
|
||||
}
|
||||
templates.value.push(newTemplate)
|
||||
saveTemplates()
|
||||
...template,
|
||||
};
|
||||
templates.value.push(newTemplate);
|
||||
saveTemplates();
|
||||
}
|
||||
|
||||
// Update existing template
|
||||
function updateTemplate(updatedTemplate: Template) {
|
||||
const index = templates.value.findIndex((t) => t.id === updatedTemplate.id)
|
||||
const index = templates.value.findIndex(t => t.id === updatedTemplate.id);
|
||||
if (index !== -1) {
|
||||
templates.value[index] = updatedTemplate
|
||||
saveTemplates()
|
||||
templates.value[index] = updatedTemplate;
|
||||
saveTemplates();
|
||||
}
|
||||
}
|
||||
|
||||
// Delete template
|
||||
function deleteTemplate(id: string) {
|
||||
templates.value = templates.value.filter((template) => template.id !== id)
|
||||
saveTemplates()
|
||||
templates.value = templates.value.filter(template => template.id !== id);
|
||||
saveTemplates();
|
||||
}
|
||||
|
||||
// Get template names for dropdowns
|
||||
function getTemplateNames() {
|
||||
return templates.value.map((template) => ({
|
||||
return templates.value.map(template => ({
|
||||
text: template.name,
|
||||
value: template.id
|
||||
}))
|
||||
value: template.id,
|
||||
}));
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -86,6 +86,6 @@ export const useTemplateStore = defineStore('template', () => {
|
||||
addTemplate,
|
||||
updateTemplate,
|
||||
deleteTemplate,
|
||||
getTemplateNames
|
||||
}
|
||||
})
|
||||
getTemplateNames,
|
||||
};
|
||||
});
|
||||
Vendored
+1
-1
@@ -23,4 +23,4 @@ interface Window {
|
||||
off: (channel: string, listener: (event: any, ...args: any[]) => void) => void;
|
||||
invoke: (channel: string, ...data: any[]) => Promise<any>;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,25 +1,22 @@
|
||||
// 定义对话框选项接口
|
||||
interface BaseDialogOptions {
|
||||
title: string
|
||||
message: string
|
||||
confirmText?: string
|
||||
cancelText?: string
|
||||
confirmColor?: string
|
||||
title: string;
|
||||
message: string;
|
||||
confirmText?: string;
|
||||
cancelText?: string;
|
||||
confirmColor?: string;
|
||||
}
|
||||
|
||||
interface ConfirmDialogOptions extends BaseDialogOptions { }
|
||||
|
||||
interface MessageDialogOptions extends BaseDialogOptions {
|
||||
dialogColor?: string
|
||||
dialogColor?: string;
|
||||
}
|
||||
|
||||
// 对话框类型
|
||||
type DialogType = 'confirm' | 'info' | 'success' | 'warning' | 'error'
|
||||
|
||||
// 全局对话框管理器接口
|
||||
interface GlobalDialogManager {
|
||||
showConfirm: (options: ConfirmDialogOptions) => Promise<boolean>
|
||||
showMessage: (options: MessageDialogOptions) => Promise<void>
|
||||
showConfirm: (options: ConfirmDialogOptions) => Promise<boolean>;
|
||||
showMessage: (options: MessageDialogOptions) => Promise<void>;
|
||||
}
|
||||
|
||||
// 确认对话框服务
|
||||
@@ -27,11 +24,11 @@ export const ConfirmDialogService = {
|
||||
// 显示确认对话框
|
||||
async confirm(options: ConfirmDialogOptions): Promise<boolean> {
|
||||
if (window.ConfirmDialogManager) {
|
||||
return (window.ConfirmDialogManager as GlobalDialogManager).showConfirm(options)
|
||||
return (window.ConfirmDialogManager as GlobalDialogManager).showConfirm(options);
|
||||
} else {
|
||||
console.error('GlobalDialog component not properly initialized')
|
||||
console.error('GlobalDialog component not properly initialized');
|
||||
// 降级到原生 confirm
|
||||
return confirm(`${options.title}: ${options.message}`)
|
||||
return confirm(`${options.title}: ${options.message}`);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -41,12 +38,12 @@ export const ConfirmDialogService = {
|
||||
return (window.ConfirmDialogManager as GlobalDialogManager).showMessage({
|
||||
...options,
|
||||
dialogColor: options.dialogColor || 'info',
|
||||
confirmColor: options.confirmColor || 'white'
|
||||
})
|
||||
confirmColor: options.confirmColor || 'white',
|
||||
});
|
||||
} else {
|
||||
console.error('GlobalDialog component not properly initialized')
|
||||
console.error('GlobalDialog component not properly initialized');
|
||||
// 降级到原生 alert
|
||||
alert(`${options.title}: ${options.message}`)
|
||||
alert(`${options.title}: ${options.message}`);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -56,12 +53,12 @@ export const ConfirmDialogService = {
|
||||
return (window.ConfirmDialogManager as GlobalDialogManager).showMessage({
|
||||
...options,
|
||||
dialogColor: options.dialogColor || 'success',
|
||||
confirmColor: options.confirmColor || 'white'
|
||||
})
|
||||
confirmColor: options.confirmColor || 'white',
|
||||
});
|
||||
} else {
|
||||
console.error('GlobalDialog component not properly initialized')
|
||||
console.error('GlobalDialog component not properly initialized');
|
||||
// 降级到原生 alert
|
||||
alert(`${options.title}: ${options.message}`)
|
||||
alert(`${options.title}: ${options.message}`);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -71,12 +68,12 @@ export const ConfirmDialogService = {
|
||||
return (window.ConfirmDialogManager as GlobalDialogManager).showMessage({
|
||||
...options,
|
||||
dialogColor: options.dialogColor || 'warning',
|
||||
confirmColor: options.confirmColor || 'white'
|
||||
})
|
||||
confirmColor: options.confirmColor || 'white',
|
||||
});
|
||||
} else {
|
||||
console.error('GlobalDialog component not properly initialized')
|
||||
console.error('GlobalDialog component not properly initialized');
|
||||
// 降级到原生 alert
|
||||
alert(`${options.title}: ${options.message}`)
|
||||
alert(`${options.title}: ${options.message}`);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -86,12 +83,12 @@ export const ConfirmDialogService = {
|
||||
return (window.ConfirmDialogManager as GlobalDialogManager).showMessage({
|
||||
...options,
|
||||
dialogColor: options.dialogColor || 'error',
|
||||
confirmColor: options.confirmColor || 'white'
|
||||
})
|
||||
confirmColor: options.confirmColor || 'white',
|
||||
});
|
||||
} else {
|
||||
console.error('GlobalDialog component not properly initialized')
|
||||
console.error('GlobalDialog component not properly initialized');
|
||||
// 降级到原生 alert
|
||||
alert(`${options.title}: ${options.message}`)
|
||||
alert(`${options.title}: ${options.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -1,13 +1,13 @@
|
||||
export function getCurrentLocale(): string {
|
||||
return navigator?.language?.split('-')[0] || 'en'
|
||||
return navigator?.language?.split('-')[0] || 'en';
|
||||
}
|
||||
|
||||
export async function openExternal(url: string): Promise<void> {
|
||||
await window.mainApi.send('msgOpenExternalLink', url)
|
||||
await window.mainApi.send('msgOpenExternalLink', url);
|
||||
}
|
||||
|
||||
export async function openFile(type: string): Promise<any> {
|
||||
return window.mainApi.invoke('msgOpenFile', type)
|
||||
return window.mainApi.invoke('msgOpenFile', type);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -16,7 +16,7 @@ export async function openFile(type: string): Promise<any> {
|
||||
* @returns 唯一ID字符串
|
||||
*/
|
||||
export function generateUniqueId(prefix: string = ''): string {
|
||||
const timestamp = Date.now().toString(36) // 转换为36进制,更短
|
||||
const randomStr = Math.random().toString(36).substring(2, 9) // 7位随机字符
|
||||
return prefix ? `${prefix}-${timestamp}-${randomStr}` : `${timestamp}-${randomStr}`
|
||||
const timestamp = Date.now().toString(36); // 转换为36进制,更短
|
||||
const randomStr = Math.random().toString(36).substring(2, 9); // 7位随机字符
|
||||
return prefix ? `${prefix}-${timestamp}-${randomStr}` : `${timestamp}-${randomStr}`;
|
||||
}
|
||||
+12
-12
@@ -1,18 +1,18 @@
|
||||
// 定义树状结构的节点
|
||||
export interface TreeNode {
|
||||
id: string
|
||||
name: string
|
||||
path: string // 完整路径
|
||||
type: 'directory' | 'file'
|
||||
children?: TreeNode[]
|
||||
sourcePath?: string
|
||||
sourceType?: 'file' | 'folder' // 记录选择的是文件还是文件夹
|
||||
id: string;
|
||||
name: string;
|
||||
path: string; // 完整路径
|
||||
type: 'directory' | 'file';
|
||||
children?: TreeNode[];
|
||||
sourcePath?: string;
|
||||
sourceType?: 'file' | 'folder'; // 记录选择的是文件还是文件夹
|
||||
}
|
||||
|
||||
export interface Template {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
// 使用树状结构存储
|
||||
treeStructure: TreeNode[]
|
||||
}
|
||||
treeStructure: TreeNode[];
|
||||
}
|
||||
Vendored
+4
-3
@@ -1,6 +1,7 @@
|
||||
// For fix typescript import errors
|
||||
declare module '*.vue' {
|
||||
import { defineComponent } from 'vue'
|
||||
const component: ReturnType<typeof defineComponent>
|
||||
export default component
|
||||
import type { defineComponent } from 'vue';
|
||||
|
||||
const component: ReturnType<typeof defineComponent>;
|
||||
export default component;
|
||||
}
|
||||
Reference in new issue
Block a user