feat(electron): 实现系统托盘和关闭行为配置功能
- 添加系统托盘创建和销毁逻辑- 实现窗口关闭行为配置(退出/最小化/托盘) - 添加配置文件读写功能 - 实现下载取消和清理功能 - 添加待更新文件检查机制 - 优化文件下载进度和错误处理 - 添加自动更新配置选项- 实现平滑滚动动画效果 - 添加试用期过期类型检查 -优化VIP状态刷新逻辑
This commit is contained in:
@@ -75,7 +75,10 @@
|
||||
"!jre/lib/ct.sym",
|
||||
"!jre/lib/jvm.lib"
|
||||
]
|
||||
}
|
||||
},
|
||||
"!build",
|
||||
"!dist",
|
||||
"!scripts"
|
||||
],
|
||||
"extraResources": [
|
||||
{
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"@vitejs/plugin-vue": "^4.4.1",
|
||||
"chalk": "^4.1.2",
|
||||
"chokidar": "^3.5.3",
|
||||
"electron": "^38.2.2",
|
||||
"electron": "^32.1.2",
|
||||
"electron-builder": "^25.1.6",
|
||||
"electron-rebuild": "^3.2.9",
|
||||
"express": "^5.1.0",
|
||||
|
||||
@@ -4,6 +4,7 @@ import {join, dirname, basename} from 'path';
|
||||
import {spawn, ChildProcess} from 'child_process';
|
||||
import * as https from 'https';
|
||||
import * as http from 'http';
|
||||
import { createTray, destroyTray } from './tray';
|
||||
|
||||
const isDev = process.env.NODE_ENV === 'development';
|
||||
|
||||
@@ -16,6 +17,8 @@ let isDownloading = false;
|
||||
let downloadedFilePath: string | null = null;
|
||||
let downloadedAsarPath: string | null = null;
|
||||
let downloadedJarPath: string | null = null;
|
||||
let isQuitting = false;
|
||||
let currentDownloadAbortController: AbortController | null = null;
|
||||
function openAppIfNotOpened() {
|
||||
if (appOpened) return;
|
||||
appOpened = true;
|
||||
@@ -75,6 +78,31 @@ function getDataDirectoryPath(): string {
|
||||
return dataDir;
|
||||
}
|
||||
|
||||
function getConfigPath(): string {
|
||||
return join(app.getPath('userData'), 'config.json');
|
||||
}
|
||||
|
||||
function loadConfig(): { closeAction?: 'quit' | 'minimize' | 'tray' } {
|
||||
try {
|
||||
const configPath = getConfigPath();
|
||||
if (existsSync(configPath)) {
|
||||
return JSON.parse(require('fs').readFileSync(configPath, 'utf-8'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载配置失败:', error);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
function saveConfig(config: any) {
|
||||
try {
|
||||
const configPath = getConfigPath();
|
||||
require('fs').writeFileSync(configPath, JSON.stringify(config, null, 2));
|
||||
} catch (error) {
|
||||
console.error('保存配置失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function migrateDataFromPublic(): void {
|
||||
if (!isDev) return;
|
||||
|
||||
@@ -171,7 +199,7 @@ function startSpringBoot() {
|
||||
}
|
||||
}
|
||||
|
||||
startSpringBoot();
|
||||
// startSpringBoot();
|
||||
|
||||
function stopSpringBoot() {
|
||||
if (!springProcess) return;
|
||||
@@ -210,6 +238,22 @@ function createWindow() {
|
||||
Menu.setApplicationMenu(null);
|
||||
mainWindow.setMenuBarVisibility(false);
|
||||
|
||||
// 拦截关闭事件
|
||||
mainWindow.on('close', (event) => {
|
||||
if (isQuitting) return;
|
||||
|
||||
const config = loadConfig();
|
||||
const closeAction = config.closeAction || 'tray';
|
||||
|
||||
if (closeAction === 'quit') {
|
||||
isQuitting = true;
|
||||
app.quit();
|
||||
} else if (closeAction === 'tray' || closeAction === 'minimize') {
|
||||
event.preventDefault();
|
||||
mainWindow?.hide();
|
||||
}
|
||||
});
|
||||
|
||||
// 监听窗口关闭事件,确保正确清理引用
|
||||
mainWindow.on('closed', () => {
|
||||
mainWindow = null;
|
||||
@@ -223,6 +267,7 @@ function createWindow() {
|
||||
|
||||
app.whenReady().then(() => {
|
||||
createWindow();
|
||||
createTray(mainWindow);
|
||||
|
||||
const {width: sw, height: sh} = screen.getPrimaryDisplay().workAreaSize;
|
||||
splashWindow = new BrowserWindow({
|
||||
@@ -251,24 +296,32 @@ app.whenReady().then(() => {
|
||||
splashWindow.loadFile(splashPath);
|
||||
}
|
||||
|
||||
// setTimeout(() => {
|
||||
// openAppIfNotOpened();
|
||||
// }, 2000);
|
||||
setTimeout(() => {
|
||||
openAppIfNotOpened();
|
||||
}, 2000);
|
||||
|
||||
app.on('activate', () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.show();
|
||||
} else if (BrowserWindow.getAllWindows().length === 0) {
|
||||
createWindow();
|
||||
createTray(mainWindow);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
stopSpringBoot();
|
||||
if (process.platform !== 'darwin') app.quit();
|
||||
// 允许在后台运行,不自动退出
|
||||
if (process.platform !== 'darwin' && isQuitting) {
|
||||
stopSpringBoot();
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
app.on('before-quit', () => {
|
||||
isQuitting = true;
|
||||
stopSpringBoot();
|
||||
destroyTray();
|
||||
});
|
||||
|
||||
ipcMain.on('message', (event, message) => {
|
||||
@@ -330,41 +383,46 @@ ipcMain.handle('download-update', async (event, downloadUrls: {asarUrl?: string,
|
||||
if (downloadedFilePath === 'completed') return {success: true, filePath: 'already-completed'};
|
||||
|
||||
isDownloading = true;
|
||||
currentDownloadAbortController = new AbortController();
|
||||
let totalDownloaded = 0;
|
||||
let totalSize = 0;
|
||||
let combinedTotalSize = 0;
|
||||
|
||||
try {
|
||||
// 获取总大小
|
||||
const sizes = await Promise.all([
|
||||
downloadUrls.asarUrl ? getFileSize(downloadUrls.asarUrl) : 0,
|
||||
downloadUrls.jarUrl ? getFileSize(downloadUrls.jarUrl) : 0
|
||||
]);
|
||||
totalSize = sizes[0] + sizes[1];
|
||||
|
||||
// 下载asar文件
|
||||
// 预先获取文件大小,计算总下载大小
|
||||
let asarSize = 0;
|
||||
let jarSize = 0;
|
||||
if (downloadUrls.asarUrl) {
|
||||
const tempAsarPath = join(app.getPath('temp'), 'app.asar.new');
|
||||
const asarSize = sizes[0];
|
||||
asarSize = await getFileSize(downloadUrls.asarUrl);
|
||||
}
|
||||
if (downloadUrls.jarUrl) {
|
||||
jarSize = await getFileSize(downloadUrls.jarUrl);
|
||||
}
|
||||
combinedTotalSize = asarSize + jarSize;
|
||||
|
||||
if (downloadUrls.asarUrl && !currentDownloadAbortController.signal.aborted) {
|
||||
const tempAsarPath = join(app.getPath('temp'), 'app.asar.new');
|
||||
await downloadFile(downloadUrls.asarUrl, tempAsarPath, (progress) => {
|
||||
const combinedProgress = {
|
||||
percentage: Math.round(((totalDownloaded + progress.downloaded) / totalSize) * 100),
|
||||
percentage: combinedTotalSize > 0 ? Math.round(((totalDownloaded + progress.downloaded) / combinedTotalSize) * 100) : 0,
|
||||
current: `${((totalDownloaded + progress.downloaded) / 1024 / 1024).toFixed(1)} MB`,
|
||||
total: `${(totalSize / 1024 / 1024).toFixed(1)} MB`
|
||||
total: combinedTotalSize > 0 ? `${(combinedTotalSize / 1024 / 1024).toFixed(1)} MB` : '0 MB'
|
||||
};
|
||||
downloadProgress = combinedProgress;
|
||||
if (mainWindow) mainWindow.webContents.send('download-progress', combinedProgress);
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.webContents.send('download-progress', combinedProgress);
|
||||
}
|
||||
});
|
||||
|
||||
if (currentDownloadAbortController.signal.aborted) throw new Error('下载已取消');
|
||||
|
||||
const asarUpdatePath = join(process.resourcesPath, 'app.asar.update');
|
||||
await fs.copyFile(tempAsarPath, asarUpdatePath);
|
||||
await fs.unlink(tempAsarPath);
|
||||
downloadedAsarPath = asarUpdatePath;
|
||||
totalDownloaded += asarSize;
|
||||
totalDownloaded = asarSize;
|
||||
}
|
||||
|
||||
// 下载jar文件
|
||||
if (downloadUrls.jarUrl) {
|
||||
if (downloadUrls.jarUrl && !currentDownloadAbortController.signal.aborted) {
|
||||
let jarFileName = basename(downloadUrls.jarUrl);
|
||||
if (!jarFileName.match(/^erp_client_sb-[\d.]+\.jar$/)) {
|
||||
const currentJar = getJarFilePath();
|
||||
@@ -379,17 +437,20 @@ ipcMain.handle('download-update', async (event, downloadUrls: {asarUrl?: string,
|
||||
}
|
||||
|
||||
const tempJarPath = join(app.getPath('temp'), jarFileName);
|
||||
|
||||
await downloadFile(downloadUrls.jarUrl, tempJarPath, (progress) => {
|
||||
const combinedProgress = {
|
||||
percentage: Math.round(((totalDownloaded + progress.downloaded) / totalSize) * 100),
|
||||
percentage: combinedTotalSize > 0 ? Math.round(((totalDownloaded + progress.downloaded) / combinedTotalSize) * 100) : 0,
|
||||
current: `${((totalDownloaded + progress.downloaded) / 1024 / 1024).toFixed(1)} MB`,
|
||||
total: `${(totalSize / 1024 / 1024).toFixed(1)} MB`
|
||||
total: combinedTotalSize > 0 ? `${(combinedTotalSize / 1024 / 1024).toFixed(1)} MB` : '0 MB'
|
||||
};
|
||||
downloadProgress = combinedProgress;
|
||||
if (mainWindow) mainWindow.webContents.send('download-progress', combinedProgress);
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.webContents.send('download-progress', combinedProgress);
|
||||
}
|
||||
});
|
||||
|
||||
if (currentDownloadAbortController.signal.aborted) throw new Error('下载已取消');
|
||||
|
||||
const jarUpdatePath = join(process.resourcesPath, jarFileName + '.update');
|
||||
await fs.copyFile(tempJarPath, jarUpdatePath);
|
||||
await fs.unlink(tempJarPath);
|
||||
@@ -398,10 +459,13 @@ ipcMain.handle('download-update', async (event, downloadUrls: {asarUrl?: string,
|
||||
|
||||
downloadedFilePath = 'completed';
|
||||
isDownloading = false;
|
||||
|
||||
currentDownloadAbortController = null;
|
||||
|
||||
return {success: true, asarPath: downloadedAsarPath, jarPath: downloadedJarPath};
|
||||
} catch (error: unknown) {
|
||||
await cleanupDownloadFiles();
|
||||
isDownloading = false;
|
||||
currentDownloadAbortController = null;
|
||||
downloadedFilePath = null;
|
||||
downloadedAsarPath = null;
|
||||
downloadedJarPath = null;
|
||||
@@ -466,12 +530,21 @@ WshShell.Run Chr(34) & "${helperPath.replace(/\\/g, '\\\\')}" & Chr(34) & " " &
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('cancel-download', () => {
|
||||
ipcMain.handle('cancel-download', async () => {
|
||||
if (currentDownloadAbortController) {
|
||||
currentDownloadAbortController.abort();
|
||||
currentDownloadAbortController = null;
|
||||
}
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
await cleanupDownloadFiles();
|
||||
|
||||
isDownloading = false;
|
||||
downloadProgress = {percentage: 0, current: '0 MB', total: '0 MB'};
|
||||
downloadedFilePath = null;
|
||||
downloadedAsarPath = null;
|
||||
downloadedJarPath = null;
|
||||
|
||||
return {success: true};
|
||||
});
|
||||
|
||||
@@ -479,6 +552,77 @@ ipcMain.handle('get-update-status', () => {
|
||||
return {downloadedFilePath, isDownloading, downloadProgress, isPackaged: app.isPackaged};
|
||||
});
|
||||
|
||||
ipcMain.handle('check-pending-update', () => {
|
||||
try {
|
||||
const asarUpdatePath = join(process.resourcesPath, 'app.asar.update');
|
||||
const hasAsarUpdate = existsSync(asarUpdatePath);
|
||||
|
||||
// 查找jar更新文件
|
||||
let jarUpdatePath = '';
|
||||
if (process.resourcesPath && existsSync(process.resourcesPath)) {
|
||||
const files = readdirSync(process.resourcesPath);
|
||||
const jarUpdateFile = files.find(f => f.startsWith('erp_client_sb-') && f.endsWith('.jar.update'));
|
||||
if (jarUpdateFile) {
|
||||
jarUpdatePath = join(process.resourcesPath, jarUpdateFile);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
hasPendingUpdate: hasAsarUpdate || !!jarUpdatePath,
|
||||
asarUpdatePath: hasAsarUpdate ? asarUpdatePath : null,
|
||||
jarUpdatePath: jarUpdatePath || null
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('检查待安装更新失败:', error);
|
||||
return {
|
||||
hasPendingUpdate: false,
|
||||
asarUpdatePath: null,
|
||||
jarUpdatePath: null
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('clear-update-files', async () => {
|
||||
try {
|
||||
await cleanupDownloadFiles();
|
||||
|
||||
// 重置下载状态
|
||||
downloadedFilePath = null;
|
||||
downloadedAsarPath = null;
|
||||
downloadedJarPath = null;
|
||||
isDownloading = false;
|
||||
downloadProgress = {percentage: 0, current: '0 MB', total: '0 MB'};
|
||||
|
||||
return {success: true};
|
||||
} catch (error: unknown) {
|
||||
return {success: false, error: error instanceof Error ? error.message : '清除失败'};
|
||||
}
|
||||
});
|
||||
|
||||
async function cleanupDownloadFiles() {
|
||||
try {
|
||||
const tempAsarPath = join(app.getPath('temp'), 'app.asar.new');
|
||||
if (existsSync(tempAsarPath)) await fs.unlink(tempAsarPath).catch(() => {});
|
||||
|
||||
const asarUpdatePath = join(process.resourcesPath, 'app.asar.update');
|
||||
if (existsSync(asarUpdatePath)) await fs.unlink(asarUpdatePath).catch(() => {});
|
||||
|
||||
if (process.resourcesPath && existsSync(process.resourcesPath)) {
|
||||
const files = readdirSync(process.resourcesPath);
|
||||
const jarUpdateFiles = files.filter(f => f.startsWith('erp_client_sb-') && f.endsWith('.jar.update'));
|
||||
for (const file of jarUpdateFiles) {
|
||||
await fs.unlink(join(process.resourcesPath, file)).catch(() => {});
|
||||
}
|
||||
|
||||
const tempJarFiles = files.filter(f => f.startsWith('erp_client_sb-') && f.endsWith('.jar') && !f.includes('update'));
|
||||
for (const file of tempJarFiles) {
|
||||
const tempJarPath = join(app.getPath('temp'), file);
|
||||
if (existsSync(tempJarPath)) await fs.unlink(tempJarPath).catch(() => {});
|
||||
}
|
||||
}
|
||||
} catch (error) {}
|
||||
}
|
||||
|
||||
// 添加文件保存对话框处理器
|
||||
ipcMain.handle('show-save-dialog', async (event, options) => {
|
||||
return await dialog.showSaveDialog(mainWindow!, options);
|
||||
@@ -552,33 +696,79 @@ ipcMain.handle('read-log-file', async (event, logDate: string) => {
|
||||
}
|
||||
});
|
||||
|
||||
// 获取关闭行为配置
|
||||
ipcMain.handle('get-close-action', () => {
|
||||
const config = loadConfig();
|
||||
return config.closeAction || 'tray';
|
||||
});
|
||||
|
||||
// 保存关闭行为配置
|
||||
ipcMain.handle('set-close-action', (event, action: 'quit' | 'minimize' | 'tray') => {
|
||||
const config = loadConfig();
|
||||
config.closeAction = action;
|
||||
saveConfig(config);
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
|
||||
async function getFileSize(url: string): Promise<number> {
|
||||
return new Promise((resolve, reject) => {
|
||||
return new Promise((resolve) => {
|
||||
const protocol = url.startsWith('https') ? https : http;
|
||||
protocol.get(url, {method: 'HEAD'}, (response) => {
|
||||
|
||||
const request = protocol.get(url, {method: 'HEAD'}, (response) => {
|
||||
if (response.statusCode === 301 || response.statusCode === 302 || response.statusCode === 307) {
|
||||
const redirectUrl = response.headers.location;
|
||||
if (redirectUrl) {
|
||||
getFileSize(redirectUrl).then(resolve).catch(() => resolve(0));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const size = parseInt(response.headers['content-length'] || '0', 10);
|
||||
resolve(size);
|
||||
}).on('error', () => resolve(0));
|
||||
|
||||
request.setTimeout(10000, () => {
|
||||
request.destroy();
|
||||
resolve(0);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function downloadFile(url: string, filePath: string, onProgress: (progress: {downloaded: number}) => void): Promise<void> {
|
||||
async function downloadFile(url: string, filePath: string, onProgress: (progress: {downloaded: number, total: number}) => void): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const protocol = url.startsWith('https') ? https : http;
|
||||
|
||||
protocol.get(url, (response) => {
|
||||
const request = protocol.get(url, (response) => {
|
||||
if (response.statusCode === 301 || response.statusCode === 302 || response.statusCode === 307) {
|
||||
const redirectUrl = response.headers.location;
|
||||
if (redirectUrl) {
|
||||
downloadFile(redirectUrl, filePath, onProgress).then(resolve).catch(reject);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (response.statusCode !== 200) {
|
||||
reject(new Error(`HTTP ${response.statusCode}`));
|
||||
return;
|
||||
}
|
||||
|
||||
const totalSize = parseInt(response.headers['content-length'] || '0', 10);
|
||||
let downloadedBytes = 0;
|
||||
const fileStream = createWriteStream(filePath);
|
||||
|
||||
if (currentDownloadAbortController) {
|
||||
currentDownloadAbortController.signal.addEventListener('abort', () => {
|
||||
request.destroy();
|
||||
response.destroy();
|
||||
fileStream.destroy();
|
||||
reject(new Error('下载已取消'));
|
||||
});
|
||||
}
|
||||
|
||||
response.on('data', (chunk) => {
|
||||
downloadedBytes += chunk.length;
|
||||
onProgress({downloaded: downloadedBytes});
|
||||
onProgress({downloaded: downloadedBytes, total: totalSize});
|
||||
});
|
||||
|
||||
response.pipe(fileStream);
|
||||
|
||||
@@ -10,6 +10,8 @@ const electronAPI = {
|
||||
installUpdate: () => ipcRenderer.invoke('install-update'),
|
||||
cancelDownload: () => ipcRenderer.invoke('cancel-download'),
|
||||
getUpdateStatus: () => ipcRenderer.invoke('get-update-status'),
|
||||
checkPendingUpdate: () => ipcRenderer.invoke('check-pending-update'),
|
||||
clearUpdateFiles: () => ipcRenderer.invoke('clear-update-files'),
|
||||
|
||||
// 添加文件保存对话框 API
|
||||
showSaveDialog: (options: any) => ipcRenderer.invoke('show-save-dialog', options),
|
||||
@@ -21,7 +23,12 @@ const electronAPI = {
|
||||
getLogDates: () => ipcRenderer.invoke('get-log-dates'),
|
||||
readLogFile: (logDate: string) => ipcRenderer.invoke('read-log-file', logDate),
|
||||
|
||||
// 关闭行为配置 API
|
||||
getCloseAction: () => ipcRenderer.invoke('get-close-action'),
|
||||
setCloseAction: (action: 'quit' | 'minimize' | 'tray') => ipcRenderer.invoke('set-close-action', action),
|
||||
|
||||
onDownloadProgress: (callback: (progress: any) => void) => {
|
||||
ipcRenderer.removeAllListeners('download-progress')
|
||||
ipcRenderer.on('download-progress', (event, progress) => callback(progress))
|
||||
},
|
||||
removeDownloadProgressListener: () => {
|
||||
|
||||
75
electron-vue-template/src/main/tray.ts
Normal file
75
electron-vue-template/src/main/tray.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { app, Tray, Menu, BrowserWindow, nativeImage } from 'electron'
|
||||
import { join } from 'path'
|
||||
import { existsSync } from 'fs'
|
||||
|
||||
let tray: Tray | null = null
|
||||
|
||||
function getIconPath(): string {
|
||||
const isDev = process.env.NODE_ENV === 'development'
|
||||
if (isDev) {
|
||||
return join(__dirname, '../../public/icon/icon.png')
|
||||
}
|
||||
const bundledPath = join(process.resourcesPath, 'app.asar.unpacked', 'public/icon/icon.png')
|
||||
if (existsSync(bundledPath)) return bundledPath
|
||||
return join(__dirname, '../renderer/icon/icon.png')
|
||||
}
|
||||
|
||||
export function createTray(mainWindow: BrowserWindow | null) {
|
||||
if (tray) return tray
|
||||
|
||||
const iconPath = getIconPath()
|
||||
const icon = nativeImage.createFromPath(iconPath)
|
||||
tray = new Tray(icon.resize({ width: 16, height: 16 }))
|
||||
|
||||
tray.setToolTip('ERP客户端 - 后台运行中')
|
||||
|
||||
// 左键点击显示窗口
|
||||
tray.on('click', () => {
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
if (mainWindow.isVisible()) {
|
||||
mainWindow.hide()
|
||||
} else {
|
||||
mainWindow.show()
|
||||
mainWindow.focus()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 右键菜单
|
||||
updateTrayMenu(mainWindow)
|
||||
|
||||
return tray
|
||||
}
|
||||
|
||||
export function updateTrayMenu(mainWindow: BrowserWindow | null) {
|
||||
if (!tray) return
|
||||
|
||||
const contextMenu = Menu.buildFromTemplate([
|
||||
{
|
||||
label: '显示窗口',
|
||||
click: () => {
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.show()
|
||||
mainWindow.focus()
|
||||
}
|
||||
}
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: '退出应用',
|
||||
click: () => {
|
||||
app.quit()
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
tray.setContextMenu(contextMenu)
|
||||
}
|
||||
|
||||
export function destroyTray() {
|
||||
if (tray) {
|
||||
tray.destroy()
|
||||
tray = null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,12 +27,22 @@ function buildQuery(params?: Record<string, unknown>): string {
|
||||
}
|
||||
|
||||
async function request<T>(path: string, options: RequestInit): Promise<T> {
|
||||
// 获取token
|
||||
let token = '';
|
||||
try {
|
||||
const tokenModule = await import('../utils/token');
|
||||
token = tokenModule.getToken() || '';
|
||||
} catch (e) {
|
||||
console.warn('获取token失败:', e);
|
||||
}
|
||||
|
||||
const res = await fetch(`${resolveBase(path)}${path}`, {
|
||||
credentials: 'omit',
|
||||
cache: 'no-store',
|
||||
...options,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(token ? { 'Authorization': `Bearer ${token}` } : {}),
|
||||
...options.headers
|
||||
}
|
||||
});
|
||||
@@ -72,12 +82,27 @@ export const http = {
|
||||
return request<T>(path, { method: 'DELETE' });
|
||||
},
|
||||
|
||||
upload<T>(path: string, form: FormData) {
|
||||
async upload<T>(path: string, form: FormData) {
|
||||
// 获取token
|
||||
let token = '';
|
||||
try {
|
||||
const tokenModule = await import('../utils/token');
|
||||
token = tokenModule.getToken() || '';
|
||||
} catch (e) {
|
||||
console.warn('获取token失败:', e);
|
||||
}
|
||||
|
||||
const headers: Record<string, string> = {};
|
||||
if (token) {
|
||||
headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
return fetch(`${resolveBase(path)}${path}`, {
|
||||
method: 'POST',
|
||||
body: form,
|
||||
credentials: 'omit',
|
||||
cache: 'no-store'
|
||||
cache: 'no-store',
|
||||
headers
|
||||
}).then(async res => {
|
||||
if (!res.ok) {
|
||||
const text = await res.text().catch(() => '');
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { http } from './http'
|
||||
|
||||
export const zebraApi = {
|
||||
getAccounts() {
|
||||
return http.get('/tool/banma/accounts')
|
||||
getAccounts(name?: string) {
|
||||
return http.get('/tool/banma/accounts', name ? { name } : undefined)
|
||||
},
|
||||
|
||||
saveAccount(body: any) {
|
||||
return http.post('/tool/banma/accounts', body)
|
||||
saveAccount(body: any, name?: string) {
|
||||
const url = name ? `/tool/banma/accounts?name=${encodeURIComponent(name)}` : '/tool/banma/accounts'
|
||||
return http.post(url, body)
|
||||
},
|
||||
|
||||
removeAccount(id: number) {
|
||||
|
||||
@@ -12,7 +12,7 @@ interface Props {
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
(e: 'loginSuccess', data: { token: string; permissions?: string; expireTime?: string }): void
|
||||
(e: 'loginSuccess', data: { token: string; permissions?: string; expireTime?: string; accountType?: string; deviceTrialExpired?: boolean }): void
|
||||
(e: 'showRegister'): void
|
||||
}
|
||||
|
||||
@@ -51,7 +51,9 @@ async function handleAuth() {
|
||||
emit('loginSuccess', {
|
||||
token: loginRes.data.accessToken || loginRes.data.token,
|
||||
permissions: loginRes.data.permissions,
|
||||
expireTime: loginRes.data.expireTime
|
||||
expireTime: loginRes.data.expireTime,
|
||||
accountType: loginRes.data.accountType,
|
||||
deviceTrialExpired: loginRes.data.deviceTrialExpired || false
|
||||
})
|
||||
ElMessage.success('登录成功')
|
||||
resetForm()
|
||||
|
||||
@@ -11,7 +11,7 @@ interface Props {
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
(e: 'loginSuccess', data: { token: string; permissions?: string; expireTime?: string }): void
|
||||
(e: 'loginSuccess', data: { token: string; permissions?: string; expireTime?: string; accountType?: string; deviceTrialExpired?: boolean }): void
|
||||
(e: 'backToLogin'): void
|
||||
}
|
||||
|
||||
@@ -65,24 +65,20 @@ async function handleRegister() {
|
||||
deviceId: deviceId
|
||||
})
|
||||
|
||||
// 显示注册成功和VIP信息
|
||||
if (registerRes.data.expireTime) {
|
||||
const expireDate = new Date(registerRes.data.expireTime)
|
||||
const now = new Date()
|
||||
const daysLeft = Math.ceil((expireDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24))
|
||||
|
||||
if (daysLeft > 0) {
|
||||
ElMessage.success(`注册成功!您获得了 ${daysLeft} 天VIP体验`)
|
||||
} else {
|
||||
ElMessage.warning('注册成功!该设备已使用过新人福利,请联系管理员续费')
|
||||
}
|
||||
// 显示注册成功提示
|
||||
if (registerRes.data.deviceTrialExpired) {
|
||||
ElMessage.warning('注册成功!您获得了3天VIP体验,但该设备试用期已过,请更换设备或联系管理员续费')
|
||||
} else {
|
||||
ElMessage.success('注册成功!您获得了3天VIP体验')
|
||||
}
|
||||
|
||||
// 使用注册返回的token直接登录
|
||||
emit('loginSuccess', {
|
||||
token: registerRes.data.accessToken || registerRes.data.token,
|
||||
permissions: registerRes.data.permissions,
|
||||
expireTime: registerRes.data.expireTime
|
||||
expireTime: registerRes.data.expireTime,
|
||||
accountType: registerRes.data.accountType,
|
||||
deviceTrialExpired: registerRes.data.deviceTrialExpired || false
|
||||
})
|
||||
resetForm()
|
||||
} catch (err) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,187 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
expiredType: 'device' | 'account' | 'both' // 设备过期、账号过期、都过期
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
const titleText = computed(() => {
|
||||
if (props.expiredType === 'both') return '试用已到期'
|
||||
if (props.expiredType === 'account') return '账号试用已到期'
|
||||
return '设备试用已到期'
|
||||
})
|
||||
|
||||
const subtitleText = computed(() => {
|
||||
if (props.expiredType === 'both') return '试用已到期,请联系客服订阅以获取完整服务'
|
||||
if (props.expiredType === 'account') return '账号试用已到期,请联系客服订阅'
|
||||
return '当前设备试用已到期,请更换新设备体验或联系客服订阅'
|
||||
})
|
||||
|
||||
function handleConfirm() {
|
||||
visible.value = false
|
||||
}
|
||||
|
||||
function copyWechat() {
|
||||
navigator.clipboard.writeText('_linhong')
|
||||
// ElMessage.success('微信号已复制')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:close-on-click-modal="false"
|
||||
:show-close="true"
|
||||
width="380px"
|
||||
center
|
||||
class="trial-expired-dialog">
|
||||
<div class="expired-content">
|
||||
<!-- Logo -->
|
||||
<div style="text-align: center; margin-bottom: 16px;">
|
||||
<img src="/icon/image.png" alt="logo" class="expired-logo" />
|
||||
</div>
|
||||
|
||||
<!-- 标题 -->
|
||||
<h2 class="expired-title">{{ titleText }}</h2>
|
||||
|
||||
<!-- 副标题 -->
|
||||
<p class="expired-subtitle">{{ subtitleText }}</p>
|
||||
|
||||
<!-- 客服微信 -->
|
||||
<div class="wechat-card" @click="copyWechat">
|
||||
<div class="wechat-icon">
|
||||
<svg viewBox="0 0 1024 1024">
|
||||
<path d="M664.250054 368.541681c10.015098 0 19.892049 0.732687 29.67281 1.795902-26.647917-122.810047-159.358451-214.077703-310.826188-214.077703-169.353083 0-308.085774 114.232694-308.085774 259.274068 0 83.708494 46.165436 152.460344 123.281791 205.78483l-30.80868 91.730191 107.688651-53.455469c38.558178 7.53665 69.459978 15.308661 107.924012 15.308661 9.66308 0 19.230993-0.470721 28.752858-1.225921-6.025227-20.36584-9.521864-41.723264-9.521864-63.94508C402.328693 476.632491 517.908058 368.541681 664.250054 368.541681zM498.62897 285.87389c23.200398 0 38.557154 15.120372 38.557154 38.061874 0 22.846334-15.356756 38.298144-38.557154 38.298144-23.107277 0-46.260603-15.45181-46.260603-38.298144C452.368366 300.994262 475.522716 285.87389 498.62897 285.87389zM283.016307 362.23394c-23.107277 0-46.402843-15.45181-46.402843-38.298144 0-22.941502 23.295566-38.061874 46.402843-38.061874 23.081695 0 38.46301 15.120372 38.46301 38.061874C321.479317 346.78213 306.098002 362.23394 283.016307 362.23394zM945.448458 606.151333c0-121.888048-123.258255-221.236753-261.683954-221.236753-146.57838 0-262.015505 99.348706-262.015505 221.236753 0 122.06508 115.437126 221.200938 262.015505 221.200938 30.66644 0 61.617359-7.609305 92.423993-15.262612l84.513836 45.786813-23.178909-76.17082C899.379213 735.776599 945.448458 674.90216 945.448458 606.151333zM598.803483 567.994292c-15.332197 0-30.807656-15.096836-30.807656-30.501688 0-15.190981 15.47546-30.477129 30.807656-30.477129 23.295566 0 38.558178 15.286148 38.558178 30.477129C637.361661 552.897456 622.099049 567.994292 598.803483 567.994292zM768.25071 567.994292c-15.213493 0-30.594809-15.096836-30.594809-30.501688 0-15.190981 15.381315-30.477129 30.594809-30.477129 23.107277 0 38.558178 15.286148 38.558178 30.477129C806.808888 552.897456 791.357987 567.994292 768.25071 567.994292z" fill="#09BB07"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="wechat-info">
|
||||
<div class="wechat-label">客服微信</div>
|
||||
<div class="wechat-id">_linhong</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 按钮 -->
|
||||
<el-button
|
||||
type="primary"
|
||||
class="confirm-btn"
|
||||
@click="handleConfirm"
|
||||
style="width: 100%;">
|
||||
我知道了
|
||||
</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.trial-expired-dialog :deep(.el-dialog) {
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.trial-expired-dialog :deep(.el-dialog__header) {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.trial-expired-dialog :deep(.el-dialog__body) {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.expired-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.expired-logo {
|
||||
width: 160px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.expired-title {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #1f1f1f;
|
||||
margin: 0 0 8px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.expired-subtitle {
|
||||
font-size: 12px;
|
||||
color: #8c8c8c;
|
||||
margin: 0 0 20px 0;
|
||||
text-align: center;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.wechat-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px 16px;
|
||||
background: #f5f5f5;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 20px;
|
||||
width: 90%;
|
||||
cursor: pointer;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
|
||||
.wechat-card:hover {
|
||||
background: #ebebeb;
|
||||
}
|
||||
|
||||
.wechat-icon {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.wechat-icon svg {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.wechat-info {
|
||||
flex: 1;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.wechat-label {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.wechat-id {
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
color: #1f1f1f;
|
||||
}
|
||||
|
||||
.confirm-btn {
|
||||
height: 40px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
background: #1677FF;
|
||||
border-color: #1677FF;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.confirm-btn:hover {
|
||||
background: #4096ff;
|
||||
border-color: #4096ff;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -84,6 +84,7 @@
|
||||
<span style="font-weight: 500" v-if="prog.current !== '0 MB' && prog.total !== '0 MB'">{{ prog.current }} / {{ prog.total }}</span>
|
||||
<span style="font-weight: 500" v-else>下载完成</span>
|
||||
<div class="action-buttons">
|
||||
<el-button size="small" @click="clearDownloadedFiles">清除下载</el-button>
|
||||
<el-button size="small" type="primary" @click="installUpdate">立即重启</el-button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -99,10 +100,18 @@
|
||||
import {ref, computed, onMounted, onUnmounted, watch} from 'vue'
|
||||
import {ElMessage, ElMessageBox} from 'element-plus'
|
||||
import {updateApi} from '../../api/update'
|
||||
import {getSettings} from '../../utils/settings'
|
||||
|
||||
const props = defineProps<{ modelValue: boolean }>()
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
}>()
|
||||
const emit = defineEmits<{ 'update:modelValue': [value: boolean] }>()
|
||||
|
||||
// 暴露方法给父组件调用
|
||||
defineExpose({
|
||||
checkForUpdatesNow
|
||||
})
|
||||
|
||||
const show = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit('update:modelValue', value)
|
||||
@@ -119,7 +128,7 @@ const info = ref({
|
||||
downloadUrl: '',
|
||||
asarUrl: '',
|
||||
jarUrl: '',
|
||||
updateNotes: '• 优化了用户界面体验\n• 修复了已知问题\n• 提升了系统稳定性',
|
||||
updateNotes: '',
|
||||
currentVersion: '',
|
||||
hasUpdate: false
|
||||
})
|
||||
@@ -135,12 +144,10 @@ async function autoCheck(silent = false) {
|
||||
|
||||
if (!result.needUpdate) {
|
||||
hasNewVersion.value = false
|
||||
if (!silent) {
|
||||
ElMessage.info('当前已是最新版本')
|
||||
}
|
||||
if (!silent) ElMessage.info('当前已是最新版本')
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// 发现新版本,更新信息并显示小红点
|
||||
info.value = {
|
||||
currentVersion: result.currentVersion,
|
||||
@@ -148,52 +155,50 @@ async function autoCheck(silent = false) {
|
||||
downloadUrl: result.downloadUrl || '',
|
||||
asarUrl: result.asarUrl || '',
|
||||
jarUrl: result.jarUrl || '',
|
||||
updateNotes: '• 优化了用户界面体验\n• 修复了已知问题\n• 提升了系统稳定性\n• 同步更新前端和后端',
|
||||
updateNotes: result.updateNotes || '',
|
||||
hasUpdate: true
|
||||
}
|
||||
hasNewVersion.value = true
|
||||
|
||||
// 检查是否跳过此版本
|
||||
const skippedVersion = localStorage.getItem(SKIP_VERSION_KEY)
|
||||
if (skippedVersion === result.latestVersion) {
|
||||
// 跳过的版本:显示小红点,但不弹框
|
||||
return
|
||||
}
|
||||
if (skippedVersion === result.latestVersion) return
|
||||
|
||||
// 检查是否在稍后提醒时间内
|
||||
const remindLater = localStorage.getItem(REMIND_LATER_KEY)
|
||||
if (remindLater && Date.now() < parseInt(remindLater)) {
|
||||
// 稍后提醒期间:显示小红点,但不弹框
|
||||
if (remindLater && Date.now() < parseInt(remindLater)) return
|
||||
|
||||
const settings = getSettings()
|
||||
if (settings.autoUpdate) {
|
||||
await startAutoDownload()
|
||||
return
|
||||
}
|
||||
|
||||
// 首次发现新版本:显示小红点并弹框
|
||||
show.value = true
|
||||
stage.value = 'check'
|
||||
if (!silent) {
|
||||
ElMessage.success('发现新版本')
|
||||
}
|
||||
if (!silent) ElMessage.success('发现新版本')
|
||||
} catch (error) {
|
||||
console.error('检查更新失败:', error)
|
||||
if (!silent) {
|
||||
ElMessage.error('检查更新失败')
|
||||
}
|
||||
if (!silent) ElMessage.error('检查更新失败')
|
||||
}
|
||||
}
|
||||
|
||||
function handleVersionClick() {
|
||||
// 如果有新版本,直接显示更新对话框
|
||||
if (stage.value === 'downloading' || stage.value === 'completed') {
|
||||
show.value = true
|
||||
return
|
||||
}
|
||||
|
||||
if (hasNewVersion.value) {
|
||||
// 重置状态确保从检查阶段开始
|
||||
stage.value = 'check'
|
||||
prog.value = {percentage: 0, current: '0 MB', total: '0 MB'}
|
||||
show.value = true
|
||||
} else {
|
||||
// 没有新版本,执行检查更新
|
||||
autoCheck(false)
|
||||
checkForUpdatesNow()
|
||||
}
|
||||
}
|
||||
|
||||
// 立即检查更新(供外部调用)
|
||||
async function checkForUpdatesNow() {
|
||||
await autoCheck(false)
|
||||
}
|
||||
|
||||
function skipVersion() {
|
||||
localStorage.setItem(SKIP_VERSION_KEY, info.value.latestVersion)
|
||||
show.value = false
|
||||
@@ -206,14 +211,27 @@ function remindLater() {
|
||||
}
|
||||
|
||||
async function start() {
|
||||
// 如果已经在下载或已完成,不重复执行
|
||||
if (stage.value === 'downloading') {
|
||||
show.value = true
|
||||
return
|
||||
}
|
||||
|
||||
if (stage.value === 'completed') {
|
||||
show.value = true
|
||||
return
|
||||
}
|
||||
|
||||
if (!info.value.asarUrl && !info.value.jarUrl) {
|
||||
ElMessage.error('下载链接不可用')
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
stage.value = 'downloading'
|
||||
show.value = true
|
||||
prog.value = {percentage: 0, current: '0 MB', total: '0 MB'}
|
||||
|
||||
// 设置新的进度监听器(会自动清理旧的)
|
||||
;(window as any).electronAPI.onDownloadProgress((progress: any) => {
|
||||
prog.value = {
|
||||
percentage: progress.percentage || 0,
|
||||
@@ -231,32 +249,73 @@ async function start() {
|
||||
if (response.success) {
|
||||
stage.value = 'completed'
|
||||
prog.value.percentage = 100
|
||||
// 如果没有有效的进度信息,设置默认值
|
||||
if (prog.value.current === '0 MB' && prog.value.total === '0 MB') {
|
||||
// 保持原有的"0 MB"值,让模板中的条件判断来处理显示
|
||||
}
|
||||
ElMessage.success('下载完成')
|
||||
show.value = true
|
||||
} else {
|
||||
ElMessage.error('下载失败: ' + (response.error || '未知错误'))
|
||||
stage.value = 'check'
|
||||
prog.value = {percentage: 0, current: '0 MB', total: '0 MB'}
|
||||
;(window as any).electronAPI.removeDownloadProgressListener()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('下载失败:', error)
|
||||
ElMessage.error('下载失败')
|
||||
stage.value = 'check'
|
||||
prog.value = {percentage: 0, current: '0 MB', total: '0 MB'}
|
||||
;(window as any).electronAPI.removeDownloadProgressListener()
|
||||
}
|
||||
}
|
||||
|
||||
async function startAutoDownload() {
|
||||
if (!info.value.asarUrl && !info.value.jarUrl) return
|
||||
|
||||
stage.value = 'downloading'
|
||||
prog.value = {percentage: 0, current: '0 MB', total: '0 MB'}
|
||||
|
||||
;(window as any).electronAPI.onDownloadProgress((progress: any) => {
|
||||
prog.value = {
|
||||
percentage: progress.percentage || 0,
|
||||
current: progress.current || '0 MB',
|
||||
total: progress.total || '0 MB'
|
||||
}
|
||||
})
|
||||
|
||||
try {
|
||||
const response = await (window as any).electronAPI.downloadUpdate({
|
||||
asarUrl: info.value.asarUrl,
|
||||
jarUrl: info.value.jarUrl
|
||||
})
|
||||
|
||||
if (response.success) {
|
||||
stage.value = 'completed'
|
||||
prog.value.percentage = 100
|
||||
show.value = true
|
||||
ElMessage.success('更新已下载完成,可以安装了')
|
||||
} else {
|
||||
stage.value = 'check'
|
||||
;(window as any).electronAPI.removeDownloadProgressListener()
|
||||
}
|
||||
} catch (error) {
|
||||
stage.value = 'check'
|
||||
;(window as any).electronAPI.removeDownloadProgressListener()
|
||||
}
|
||||
}
|
||||
|
||||
async function cancelDownload() {
|
||||
try {
|
||||
(window as any).electronAPI.removeDownloadProgressListener()
|
||||
;(window as any).electronAPI.removeDownloadProgressListener()
|
||||
await (window as any).electronAPI.cancelDownload()
|
||||
show.value = false
|
||||
|
||||
stage.value = 'check'
|
||||
prog.value = {percentage: 0, current: '0 MB', total: '0 MB'}
|
||||
hasNewVersion.value = false
|
||||
show.value = false
|
||||
|
||||
ElMessage.info('已取消下载')
|
||||
} catch (error) {
|
||||
console.error('取消下载失败:', error)
|
||||
show.value = false
|
||||
stage.value = 'check'
|
||||
prog.value = {percentage: 0, current: '0 MB', total: '0 MB'}
|
||||
hasNewVersion.value = false
|
||||
show.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -281,18 +340,46 @@ async function installUpdate() {
|
||||
}
|
||||
}
|
||||
|
||||
async function clearDownloadedFiles() {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
'确定要清除已下载的更新文件吗?清除后需要重新下载。',
|
||||
'确认清除',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
|
||||
const response = await (window as any).electronAPI.clearUpdateFiles()
|
||||
|
||||
if (response.success) {
|
||||
ElMessage.success('已清除下载文件')
|
||||
// 重置状态
|
||||
stage.value = 'check'
|
||||
prog.value = {percentage: 0, current: '0 MB', total: '0 MB'}
|
||||
hasNewVersion.value = false
|
||||
show.value = false
|
||||
} else {
|
||||
ElMessage.error('清除失败: ' + (response.error || '未知错误'))
|
||||
}
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') ElMessage.error('清除失败')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
version.value = await (window as any).electronAPI.getJarVersion()
|
||||
await autoCheck(true)
|
||||
})
|
||||
|
||||
// 监听对话框关闭,重置状态
|
||||
watch(show, (newValue) => {
|
||||
if (!newValue) {
|
||||
// 对话框关闭时重置状态
|
||||
stage.value = 'check'
|
||||
prog.value = {percentage: 0, current: '0 MB', total: '0 MB'}
|
||||
const pendingUpdate = await (window as any).electronAPI.checkPendingUpdate()
|
||||
|
||||
if (pendingUpdate && pendingUpdate.hasPendingUpdate) {
|
||||
stage.value = 'completed'
|
||||
prog.value.percentage = 100
|
||||
return
|
||||
}
|
||||
|
||||
await autoCheck(true)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
@@ -315,8 +402,10 @@ onUnmounted(() => {
|
||||
z-index: 1000;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
|
||||
.update-badge {
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import {ref, computed, onMounted} from 'vue'
|
||||
import {ref, computed, onMounted, defineAsyncComponent, inject} from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import {rakutenApi} from '../../api/rakuten'
|
||||
import { batchConvertImages } from '../../utils/imageProxy'
|
||||
import { handlePlatformFileExport } from '../../utils/settings'
|
||||
|
||||
const TrialExpiredDialog = defineAsyncComponent(() => import('../common/TrialExpiredDialog.vue'))
|
||||
|
||||
const refreshVipStatus = inject<() => Promise<boolean>>('refreshVipStatus')
|
||||
|
||||
// 接收VIP状态
|
||||
const props = defineProps<{
|
||||
isVip: boolean
|
||||
@@ -49,6 +53,12 @@ const activeStep = computed(() => {
|
||||
return 2
|
||||
})
|
||||
|
||||
// 试用期过期弹框
|
||||
const showTrialExpiredDialog = ref(false)
|
||||
const trialExpiredType = ref<'device' | 'account' | 'both'>('account')
|
||||
|
||||
const checkExpiredType = inject<() => 'device' | 'account' | 'both'>('checkExpiredType')
|
||||
|
||||
// 左侧:上传文件名与地区
|
||||
const selectedFileName = ref('')
|
||||
const pendingFile = ref<File | null>(null)
|
||||
@@ -128,7 +138,8 @@ async function searchProductInternal(product: any) {
|
||||
if (!product || !product.imgUrl) return
|
||||
if (!needsSearch(product)) return
|
||||
if (!props.isVip) {
|
||||
ElMessage.warning('VIP已过期,1688识图功能受限')
|
||||
if (checkExpiredType) trialExpiredType.value = checkExpiredType()
|
||||
showTrialExpiredDialog.value = true
|
||||
return
|
||||
}
|
||||
|
||||
@@ -198,19 +209,13 @@ async function onDrop(e: DragEvent) {
|
||||
|
||||
// 点击"获取数据
|
||||
async function handleStartSearch() {
|
||||
// 刷新VIP状态
|
||||
if (refreshVipStatus) await refreshVipStatus()
|
||||
|
||||
// VIP检查
|
||||
if (!props.isVip) {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
'VIP已过期,数据采集功能受限。请联系管理员续费后继续使用。',
|
||||
'VIP功能限制',
|
||||
{
|
||||
confirmButtonText: '我知道了',
|
||||
showCancelButton: false,
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
} catch {}
|
||||
if (checkExpiredType) trialExpiredType.value = checkExpiredType()
|
||||
showTrialExpiredDialog.value = true
|
||||
return
|
||||
}
|
||||
|
||||
@@ -509,6 +514,9 @@ onMounted(loadLatest)
|
||||
<el-button type="primary" class="btn-blue" @click="rakutenExampleVisible = false">我知道了</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 试用期过期弹框 -->
|
||||
<TrialExpiredDialog v-model="showTrialExpiredDialog" :expired-type="trialExpiredType" />
|
||||
<!-- 数据显示区域 -->
|
||||
<div class="table-container">
|
||||
<div class="table-section">
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { ref, computed, onMounted, defineAsyncComponent, inject } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { zebraApi, type ZebraOrder, type BanmaAccount } from '../../api/zebra'
|
||||
import AccountManager from '../common/AccountManager.vue'
|
||||
import { batchConvertImages } from '../../utils/imageProxy'
|
||||
import { handlePlatformFileExport } from '../../utils/settings'
|
||||
import { getUsernameFromToken } from '../../utils/token'
|
||||
|
||||
const TrialExpiredDialog = defineAsyncComponent(() => import('../common/TrialExpiredDialog.vue'))
|
||||
|
||||
const refreshVipStatus = inject<() => Promise<boolean>>('refreshVipStatus')
|
||||
|
||||
// 接收VIP状态
|
||||
const props = defineProps<{
|
||||
@@ -35,6 +40,12 @@ const fetchCurrentPage = ref(1)
|
||||
const fetchTotalPages = ref(0)
|
||||
const fetchTotalItems = ref(0)
|
||||
const isFetching = ref(false)
|
||||
|
||||
// 试用期过期弹框
|
||||
const showTrialExpiredDialog = ref(false)
|
||||
const trialExpiredType = ref<'device' | 'account' | 'both'>('account')
|
||||
|
||||
const checkExpiredType = inject<() => 'device' | 'account' | 'both'>('checkExpiredType')
|
||||
function selectAccount(id: number) {
|
||||
accountId.value = id
|
||||
loadShops()
|
||||
@@ -68,7 +79,8 @@ async function loadShops() {
|
||||
|
||||
async function loadAccounts() {
|
||||
try {
|
||||
const res = await zebraApi.getAccounts()
|
||||
const username = getUsernameFromToken()
|
||||
const res = await zebraApi.getAccounts(username)
|
||||
const list = (res as any)?.data ?? res
|
||||
accounts.value = Array.isArray(list) ? list : []
|
||||
const def = accounts.value.find(a => a.isDefault === 1) || accounts.value[0]
|
||||
@@ -91,19 +103,13 @@ function handleCurrentChange(page: number) {
|
||||
async function fetchData() {
|
||||
if (isFetching.value) return
|
||||
|
||||
// 刷新VIP状态
|
||||
if (refreshVipStatus) await refreshVipStatus()
|
||||
|
||||
// VIP检查
|
||||
if (!props.isVip) {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
'VIP已过期,数据采集功能受限。请联系管理员续费后继续使用。',
|
||||
'VIP功能限制',
|
||||
{
|
||||
confirmButtonText: '我知道了',
|
||||
showCancelButton: false,
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
} catch {}
|
||||
if (checkExpiredType) trialExpiredType.value = checkExpiredType()
|
||||
showTrialExpiredDialog.value = true
|
||||
return
|
||||
}
|
||||
|
||||
@@ -318,7 +324,8 @@ async function submitAccount() {
|
||||
status: accountForm.value.status || 1,
|
||||
}
|
||||
try {
|
||||
const res = await zebraApi.saveAccount(payload)
|
||||
const username = getUsernameFromToken()
|
||||
const res = await zebraApi.saveAccount(payload, username)
|
||||
const id = (res as any)?.data?.id || (res as any)?.id
|
||||
if (!id) throw new Error((res as any)?.msg || '保存失败')
|
||||
accountDialogVisible.value = false
|
||||
@@ -368,7 +375,6 @@ async function removeCurrentAccount() {
|
||||
<span :class="['status-dot', a.status === 1 ? 'on' : 'off']"></span>
|
||||
<img class="avatar" src="/image/img_v3_02qd_052605f0-4be3-44db-9691-35ee5ff6201g.jpg" alt="avatar" />
|
||||
<span class="acct-text">{{ a.name || a.username }}</span>
|
||||
<span v-if="a.isDefault===1" class="tag">默认</span>
|
||||
<span v-if="accountId === a.id" class="acct-check">✔️</span>
|
||||
</span>
|
||||
</div>
|
||||
@@ -544,6 +550,10 @@ async function removeCurrentAccount() {
|
||||
<el-button type="primary" class="btn-blue" style="width: 100%" @click="submitAccount">登录</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 试用期过期弹框 -->
|
||||
<TrialExpiredDialog v-model="showTrialExpiredDialog" :expired-type="trialExpiredType" />
|
||||
|
||||
<AccountManager ref="accountManagerRef" v-model="managerVisible" platform="zebra" @add="openAddAccount" @refresh="loadAccounts" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -15,6 +15,8 @@ export interface AppSettings {
|
||||
rakuten: PlatformExportSettings
|
||||
zebra: PlatformExportSettings
|
||||
}
|
||||
// 更新设置
|
||||
autoUpdate?: boolean
|
||||
}
|
||||
|
||||
const SETTINGS_KEY = 'app-settings'
|
||||
@@ -31,7 +33,8 @@ const defaultSettings: AppSettings = {
|
||||
amazon: { ...defaultPlatformSettings },
|
||||
rakuten: { ...defaultPlatformSettings },
|
||||
zebra: { ...defaultPlatformSettings }
|
||||
}
|
||||
},
|
||||
autoUpdate: false
|
||||
}
|
||||
|
||||
// 获取设置
|
||||
@@ -45,7 +48,8 @@ export function getSettings(): AppSettings {
|
||||
amazon: { ...defaultSettings.platforms.amazon, ...settings.platforms?.amazon },
|
||||
rakuten: { ...defaultSettings.platforms.rakuten, ...settings.platforms?.rakuten },
|
||||
zebra: { ...defaultSettings.platforms.zebra, ...settings.platforms?.zebra }
|
||||
}
|
||||
},
|
||||
autoUpdate: settings.autoUpdate ?? defaultSettings.autoUpdate
|
||||
}
|
||||
}
|
||||
return defaultSettings
|
||||
@@ -60,7 +64,8 @@ export function saveSettings(settings: Partial<AppSettings>): void {
|
||||
amazon: { ...current.platforms.amazon, ...settings.platforms?.amazon },
|
||||
rakuten: { ...current.platforms.rakuten, ...settings.platforms?.rakuten },
|
||||
zebra: { ...current.platforms.zebra, ...settings.platforms?.zebra }
|
||||
}
|
||||
},
|
||||
autoUpdate: settings.autoUpdate ?? current.autoUpdate
|
||||
}
|
||||
localStorage.setItem(SETTINGS_KEY, JSON.stringify(updated))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user