Compare commits
2 Commits
132299c4b7
...
07e34c35c8
| Author | SHA1 | Date | |
|---|---|---|---|
| 07e34c35c8 | |||
| 6e1b4d00de |
@@ -75,7 +75,10 @@
|
|||||||
"!jre/lib/ct.sym",
|
"!jre/lib/ct.sym",
|
||||||
"!jre/lib/jvm.lib"
|
"!jre/lib/jvm.lib"
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
|
"!build",
|
||||||
|
"!dist",
|
||||||
|
"!scripts"
|
||||||
],
|
],
|
||||||
"extraResources": [
|
"extraResources": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
"@vitejs/plugin-vue": "^4.4.1",
|
"@vitejs/plugin-vue": "^4.4.1",
|
||||||
"chalk": "^4.1.2",
|
"chalk": "^4.1.2",
|
||||||
"chokidar": "^3.5.3",
|
"chokidar": "^3.5.3",
|
||||||
"electron": "^38.2.2",
|
"electron": "^32.1.2",
|
||||||
"electron-builder": "^25.1.6",
|
"electron-builder": "^25.1.6",
|
||||||
"electron-rebuild": "^3.2.9",
|
"electron-rebuild": "^3.2.9",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {join, dirname, basename} from 'path';
|
|||||||
import {spawn, ChildProcess} from 'child_process';
|
import {spawn, ChildProcess} from 'child_process';
|
||||||
import * as https from 'https';
|
import * as https from 'https';
|
||||||
import * as http from 'http';
|
import * as http from 'http';
|
||||||
|
import { createTray, destroyTray } from './tray';
|
||||||
|
|
||||||
const isDev = process.env.NODE_ENV === 'development';
|
const isDev = process.env.NODE_ENV === 'development';
|
||||||
|
|
||||||
@@ -16,6 +17,8 @@ let isDownloading = false;
|
|||||||
let downloadedFilePath: string | null = null;
|
let downloadedFilePath: string | null = null;
|
||||||
let downloadedAsarPath: string | null = null;
|
let downloadedAsarPath: string | null = null;
|
||||||
let downloadedJarPath: string | null = null;
|
let downloadedJarPath: string | null = null;
|
||||||
|
let isQuitting = false;
|
||||||
|
let currentDownloadAbortController: AbortController | null = null;
|
||||||
function openAppIfNotOpened() {
|
function openAppIfNotOpened() {
|
||||||
if (appOpened) return;
|
if (appOpened) return;
|
||||||
appOpened = true;
|
appOpened = true;
|
||||||
@@ -75,6 +78,31 @@ function getDataDirectoryPath(): string {
|
|||||||
return dataDir;
|
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 {
|
function migrateDataFromPublic(): void {
|
||||||
if (!isDev) return;
|
if (!isDev) return;
|
||||||
|
|
||||||
@@ -171,7 +199,7 @@ function startSpringBoot() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
startSpringBoot();
|
// startSpringBoot();
|
||||||
|
|
||||||
function stopSpringBoot() {
|
function stopSpringBoot() {
|
||||||
if (!springProcess) return;
|
if (!springProcess) return;
|
||||||
@@ -210,6 +238,22 @@ function createWindow() {
|
|||||||
Menu.setApplicationMenu(null);
|
Menu.setApplicationMenu(null);
|
||||||
mainWindow.setMenuBarVisibility(false);
|
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.on('closed', () => {
|
||||||
mainWindow = null;
|
mainWindow = null;
|
||||||
@@ -223,6 +267,7 @@ function createWindow() {
|
|||||||
|
|
||||||
app.whenReady().then(() => {
|
app.whenReady().then(() => {
|
||||||
createWindow();
|
createWindow();
|
||||||
|
createTray(mainWindow);
|
||||||
|
|
||||||
const {width: sw, height: sh} = screen.getPrimaryDisplay().workAreaSize;
|
const {width: sw, height: sh} = screen.getPrimaryDisplay().workAreaSize;
|
||||||
splashWindow = new BrowserWindow({
|
splashWindow = new BrowserWindow({
|
||||||
@@ -251,24 +296,32 @@ app.whenReady().then(() => {
|
|||||||
splashWindow.loadFile(splashPath);
|
splashWindow.loadFile(splashPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
// setTimeout(() => {
|
setTimeout(() => {
|
||||||
// openAppIfNotOpened();
|
openAppIfNotOpened();
|
||||||
// }, 2000);
|
}, 2000);
|
||||||
|
|
||||||
app.on('activate', () => {
|
app.on('activate', () => {
|
||||||
if (BrowserWindow.getAllWindows().length === 0) {
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
|
mainWindow.show();
|
||||||
|
} else if (BrowserWindow.getAllWindows().length === 0) {
|
||||||
createWindow();
|
createWindow();
|
||||||
|
createTray(mainWindow);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
app.on('window-all-closed', () => {
|
app.on('window-all-closed', () => {
|
||||||
stopSpringBoot();
|
// 允许在后台运行,不自动退出
|
||||||
if (process.platform !== 'darwin') app.quit();
|
if (process.platform !== 'darwin' && isQuitting) {
|
||||||
|
stopSpringBoot();
|
||||||
|
app.quit();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
app.on('before-quit', () => {
|
app.on('before-quit', () => {
|
||||||
|
isQuitting = true;
|
||||||
stopSpringBoot();
|
stopSpringBoot();
|
||||||
|
destroyTray();
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.on('message', (event, message) => {
|
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'};
|
if (downloadedFilePath === 'completed') return {success: true, filePath: 'already-completed'};
|
||||||
|
|
||||||
isDownloading = true;
|
isDownloading = true;
|
||||||
|
currentDownloadAbortController = new AbortController();
|
||||||
let totalDownloaded = 0;
|
let totalDownloaded = 0;
|
||||||
let totalSize = 0;
|
let combinedTotalSize = 0;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 获取总大小
|
// 预先获取文件大小,计算总下载大小
|
||||||
const sizes = await Promise.all([
|
let asarSize = 0;
|
||||||
downloadUrls.asarUrl ? getFileSize(downloadUrls.asarUrl) : 0,
|
let jarSize = 0;
|
||||||
downloadUrls.jarUrl ? getFileSize(downloadUrls.jarUrl) : 0
|
|
||||||
]);
|
|
||||||
totalSize = sizes[0] + sizes[1];
|
|
||||||
|
|
||||||
// 下载asar文件
|
|
||||||
if (downloadUrls.asarUrl) {
|
if (downloadUrls.asarUrl) {
|
||||||
const tempAsarPath = join(app.getPath('temp'), 'app.asar.new');
|
asarSize = await getFileSize(downloadUrls.asarUrl);
|
||||||
const asarSize = sizes[0];
|
}
|
||||||
|
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) => {
|
await downloadFile(downloadUrls.asarUrl, tempAsarPath, (progress) => {
|
||||||
const combinedProgress = {
|
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`,
|
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;
|
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');
|
const asarUpdatePath = join(process.resourcesPath, 'app.asar.update');
|
||||||
await fs.copyFile(tempAsarPath, asarUpdatePath);
|
await fs.copyFile(tempAsarPath, asarUpdatePath);
|
||||||
await fs.unlink(tempAsarPath);
|
await fs.unlink(tempAsarPath);
|
||||||
downloadedAsarPath = asarUpdatePath;
|
downloadedAsarPath = asarUpdatePath;
|
||||||
totalDownloaded += asarSize;
|
totalDownloaded = asarSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 下载jar文件
|
if (downloadUrls.jarUrl && !currentDownloadAbortController.signal.aborted) {
|
||||||
if (downloadUrls.jarUrl) {
|
|
||||||
let jarFileName = basename(downloadUrls.jarUrl);
|
let jarFileName = basename(downloadUrls.jarUrl);
|
||||||
if (!jarFileName.match(/^erp_client_sb-[\d.]+\.jar$/)) {
|
if (!jarFileName.match(/^erp_client_sb-[\d.]+\.jar$/)) {
|
||||||
const currentJar = getJarFilePath();
|
const currentJar = getJarFilePath();
|
||||||
@@ -379,17 +437,20 @@ ipcMain.handle('download-update', async (event, downloadUrls: {asarUrl?: string,
|
|||||||
}
|
}
|
||||||
|
|
||||||
const tempJarPath = join(app.getPath('temp'), jarFileName);
|
const tempJarPath = join(app.getPath('temp'), jarFileName);
|
||||||
|
|
||||||
await downloadFile(downloadUrls.jarUrl, tempJarPath, (progress) => {
|
await downloadFile(downloadUrls.jarUrl, tempJarPath, (progress) => {
|
||||||
const combinedProgress = {
|
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`,
|
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;
|
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');
|
const jarUpdatePath = join(process.resourcesPath, jarFileName + '.update');
|
||||||
await fs.copyFile(tempJarPath, jarUpdatePath);
|
await fs.copyFile(tempJarPath, jarUpdatePath);
|
||||||
await fs.unlink(tempJarPath);
|
await fs.unlink(tempJarPath);
|
||||||
@@ -398,10 +459,13 @@ ipcMain.handle('download-update', async (event, downloadUrls: {asarUrl?: string,
|
|||||||
|
|
||||||
downloadedFilePath = 'completed';
|
downloadedFilePath = 'completed';
|
||||||
isDownloading = false;
|
isDownloading = false;
|
||||||
|
currentDownloadAbortController = null;
|
||||||
|
|
||||||
return {success: true, asarPath: downloadedAsarPath, jarPath: downloadedJarPath};
|
return {success: true, asarPath: downloadedAsarPath, jarPath: downloadedJarPath};
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
|
await cleanupDownloadFiles();
|
||||||
isDownloading = false;
|
isDownloading = false;
|
||||||
|
currentDownloadAbortController = null;
|
||||||
downloadedFilePath = null;
|
downloadedFilePath = null;
|
||||||
downloadedAsarPath = null;
|
downloadedAsarPath = null;
|
||||||
downloadedJarPath = 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;
|
isDownloading = false;
|
||||||
downloadProgress = {percentage: 0, current: '0 MB', total: '0 MB'};
|
downloadProgress = {percentage: 0, current: '0 MB', total: '0 MB'};
|
||||||
downloadedFilePath = null;
|
downloadedFilePath = null;
|
||||||
downloadedAsarPath = null;
|
downloadedAsarPath = null;
|
||||||
downloadedJarPath = null;
|
downloadedJarPath = null;
|
||||||
|
|
||||||
return {success: true};
|
return {success: true};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -479,6 +552,77 @@ ipcMain.handle('get-update-status', () => {
|
|||||||
return {downloadedFilePath, isDownloading, downloadProgress, isPackaged: app.isPackaged};
|
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) => {
|
ipcMain.handle('show-save-dialog', async (event, options) => {
|
||||||
return await dialog.showSaveDialog(mainWindow!, 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> {
|
async function getFileSize(url: string): Promise<number> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve) => {
|
||||||
const protocol = url.startsWith('https') ? https : http;
|
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);
|
const size = parseInt(response.headers['content-length'] || '0', 10);
|
||||||
resolve(size);
|
resolve(size);
|
||||||
}).on('error', () => resolve(0));
|
}).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) => {
|
return new Promise((resolve, reject) => {
|
||||||
const protocol = url.startsWith('https') ? https : http;
|
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) {
|
if (response.statusCode !== 200) {
|
||||||
reject(new Error(`HTTP ${response.statusCode}`));
|
reject(new Error(`HTTP ${response.statusCode}`));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const totalSize = parseInt(response.headers['content-length'] || '0', 10);
|
||||||
let downloadedBytes = 0;
|
let downloadedBytes = 0;
|
||||||
const fileStream = createWriteStream(filePath);
|
const fileStream = createWriteStream(filePath);
|
||||||
|
|
||||||
|
if (currentDownloadAbortController) {
|
||||||
|
currentDownloadAbortController.signal.addEventListener('abort', () => {
|
||||||
|
request.destroy();
|
||||||
|
response.destroy();
|
||||||
|
fileStream.destroy();
|
||||||
|
reject(new Error('下载已取消'));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
response.on('data', (chunk) => {
|
response.on('data', (chunk) => {
|
||||||
downloadedBytes += chunk.length;
|
downloadedBytes += chunk.length;
|
||||||
onProgress({downloaded: downloadedBytes});
|
onProgress({downloaded: downloadedBytes, total: totalSize});
|
||||||
});
|
});
|
||||||
|
|
||||||
response.pipe(fileStream);
|
response.pipe(fileStream);
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ const electronAPI = {
|
|||||||
installUpdate: () => ipcRenderer.invoke('install-update'),
|
installUpdate: () => ipcRenderer.invoke('install-update'),
|
||||||
cancelDownload: () => ipcRenderer.invoke('cancel-download'),
|
cancelDownload: () => ipcRenderer.invoke('cancel-download'),
|
||||||
getUpdateStatus: () => ipcRenderer.invoke('get-update-status'),
|
getUpdateStatus: () => ipcRenderer.invoke('get-update-status'),
|
||||||
|
checkPendingUpdate: () => ipcRenderer.invoke('check-pending-update'),
|
||||||
|
clearUpdateFiles: () => ipcRenderer.invoke('clear-update-files'),
|
||||||
|
|
||||||
// 添加文件保存对话框 API
|
// 添加文件保存对话框 API
|
||||||
showSaveDialog: (options: any) => ipcRenderer.invoke('show-save-dialog', options),
|
showSaveDialog: (options: any) => ipcRenderer.invoke('show-save-dialog', options),
|
||||||
@@ -21,7 +23,12 @@ const electronAPI = {
|
|||||||
getLogDates: () => ipcRenderer.invoke('get-log-dates'),
|
getLogDates: () => ipcRenderer.invoke('get-log-dates'),
|
||||||
readLogFile: (logDate: string) => ipcRenderer.invoke('read-log-file', logDate),
|
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) => {
|
onDownloadProgress: (callback: (progress: any) => void) => {
|
||||||
|
ipcRenderer.removeAllListeners('download-progress')
|
||||||
ipcRenderer.on('download-progress', (event, progress) => callback(progress))
|
ipcRenderer.on('download-progress', (event, progress) => callback(progress))
|
||||||
},
|
},
|
||||||
removeDownloadProgressListener: () => {
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {onMounted, ref, computed, defineAsyncComponent, type Component, onUnmounted} from 'vue'
|
import {onMounted, ref, computed, defineAsyncComponent, type Component, onUnmounted, provide} from 'vue'
|
||||||
import {ElMessage, ElMessageBox} from 'element-plus'
|
import {ElMessage, ElMessageBox} from 'element-plus'
|
||||||
import zhCn from 'element-plus/es/locale/lang/zh-cn'
|
import zhCn from 'element-plus/es/locale/lang/zh-cn'
|
||||||
import 'element-plus/dist/index.css'
|
import 'element-plus/dist/index.css'
|
||||||
@@ -8,6 +8,7 @@ import {deviceApi, type DeviceItem, type DeviceQuota} from './api/device'
|
|||||||
import {getOrCreateDeviceId} from './utils/deviceId'
|
import {getOrCreateDeviceId} from './utils/deviceId'
|
||||||
import {getToken, setToken, removeToken, getUsernameFromToken, getClientIdFromToken} from './utils/token'
|
import {getToken, setToken, removeToken, getUsernameFromToken, getClientIdFromToken} from './utils/token'
|
||||||
import {CONFIG} from './api/http'
|
import {CONFIG} from './api/http'
|
||||||
|
import {getSettings} from './utils/settings'
|
||||||
const LoginDialog = defineAsyncComponent(() => import('./components/auth/LoginDialog.vue'))
|
const LoginDialog = defineAsyncComponent(() => import('./components/auth/LoginDialog.vue'))
|
||||||
const RegisterDialog = defineAsyncComponent(() => import('./components/auth/RegisterDialog.vue'))
|
const RegisterDialog = defineAsyncComponent(() => import('./components/auth/RegisterDialog.vue'))
|
||||||
const NavigationBar = defineAsyncComponent(() => import('./components/layout/NavigationBar.vue'))
|
const NavigationBar = defineAsyncComponent(() => import('./components/layout/NavigationBar.vue'))
|
||||||
@@ -16,6 +17,7 @@ const AmazonDashboard = defineAsyncComponent(() => import('./components/amazon/A
|
|||||||
const ZebraDashboard = defineAsyncComponent(() => import('./components/zebra/ZebraDashboard.vue'))
|
const ZebraDashboard = defineAsyncComponent(() => import('./components/zebra/ZebraDashboard.vue'))
|
||||||
const UpdateDialog = defineAsyncComponent(() => import('./components/common/UpdateDialog.vue'))
|
const UpdateDialog = defineAsyncComponent(() => import('./components/common/UpdateDialog.vue'))
|
||||||
const SettingsDialog = defineAsyncComponent(() => import('./components/common/SettingsDialog.vue'))
|
const SettingsDialog = defineAsyncComponent(() => import('./components/common/SettingsDialog.vue'))
|
||||||
|
const TrialExpiredDialog = defineAsyncComponent(() => import('./components/common/TrialExpiredDialog.vue'))
|
||||||
|
|
||||||
const dashboardsMap: Record<string, Component> = {
|
const dashboardsMap: Record<string, Component> = {
|
||||||
rakuten: RakutenDashboard,
|
rakuten: RakutenDashboard,
|
||||||
@@ -51,23 +53,39 @@ const userPermissions = ref<string>('')
|
|||||||
|
|
||||||
// VIP状态
|
// VIP状态
|
||||||
const vipExpireTime = ref<Date | null>(null)
|
const vipExpireTime = ref<Date | null>(null)
|
||||||
|
const deviceTrialExpired = ref(false)
|
||||||
|
const accountType = ref<string>('trial')
|
||||||
const vipStatus = computed(() => {
|
const vipStatus = computed(() => {
|
||||||
if (!vipExpireTime.value) return { isVip: false, daysLeft: 0, status: 'expired' }
|
if (!vipExpireTime.value) return { isVip: false, daysLeft: 0, status: 'expired' }
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
const expire = new Date(vipExpireTime.value)
|
const expire = new Date(vipExpireTime.value)
|
||||||
const daysLeft = Math.ceil((expire.getTime() - now.getTime()) / (1000 * 60 * 60 * 24))
|
const daysLeft = Math.ceil((expire.getTime() - now.getTime()) / (1000 * 60 * 60 * 24))
|
||||||
|
|
||||||
if (daysLeft <= 0) return { isVip: false, daysLeft: 0, status: 'expired' }
|
if (daysLeft <= 0) return { isVip: false, daysLeft: 0, status: 'expired' }
|
||||||
if (daysLeft <= 7) return { isVip: true, daysLeft, status: 'warning' }
|
if (daysLeft <= 7) return { isVip: true, daysLeft, status: 'warning' }
|
||||||
if (daysLeft <= 30) return { isVip: true, daysLeft, status: 'normal' }
|
if (daysLeft <= 30) return { isVip: true, daysLeft, status: 'normal' }
|
||||||
return { isVip: true, daysLeft, status: 'active' }
|
return { isVip: true, daysLeft, status: 'active' }
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 功能可用性(账号VIP + 设备试用期)
|
||||||
|
const canUseFunctions = computed(() => {
|
||||||
|
// 付费账号不受设备限制
|
||||||
|
if (accountType.value === 'paid') return vipStatus.value.isVip
|
||||||
|
// 试用账号需要账号VIP有效 且 设备未过期
|
||||||
|
return vipStatus.value.isVip && !deviceTrialExpired.value
|
||||||
|
})
|
||||||
|
|
||||||
// 更新对话框状态
|
// 更新对话框状态
|
||||||
const showUpdateDialog = ref(false)
|
const showUpdateDialog = ref(false)
|
||||||
|
const updateDialogRef = ref()
|
||||||
|
|
||||||
// 设置对话框状态
|
// 设置对话框状态
|
||||||
const showSettingsDialog = ref(false)
|
const showSettingsDialog = ref(false)
|
||||||
|
|
||||||
|
// 试用期过期对话框
|
||||||
|
const showTrialExpiredDialog = ref(false)
|
||||||
|
const trialExpiredType = ref<'device' | 'account' | 'both'>('device')
|
||||||
|
|
||||||
// 菜单配置 - 复刻ERP客户端格式
|
// 菜单配置 - 复刻ERP客户端格式
|
||||||
const menuConfig = [
|
const menuConfig = [
|
||||||
{key: 'rakuten', name: 'Rakuten', index: 'rakuten', icon: 'R'},
|
{key: 'rakuten', name: 'Rakuten', index: 'rakuten', icon: 'R'},
|
||||||
@@ -147,7 +165,7 @@ function handleMenuSelect(key: string) {
|
|||||||
addToHistory(key)
|
addToHistory(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleLoginSuccess(data: { token: string; permissions?: string; expireTime?: string }) {
|
async function handleLoginSuccess(data: { token: string; permissions?: string; expireTime?: string; accountType?: string; deviceTrialExpired?: boolean }) {
|
||||||
try {
|
try {
|
||||||
setToken(data.token)
|
setToken(data.token)
|
||||||
isAuthenticated.value = true
|
isAuthenticated.value = true
|
||||||
@@ -157,6 +175,8 @@ async function handleLoginSuccess(data: { token: string; permissions?: string; e
|
|||||||
currentUsername.value = getUsernameFromToken(data.token)
|
currentUsername.value = getUsernameFromToken(data.token)
|
||||||
userPermissions.value = data.permissions || ''
|
userPermissions.value = data.permissions || ''
|
||||||
vipExpireTime.value = data.expireTime ? new Date(data.expireTime) : null
|
vipExpireTime.value = data.expireTime ? new Date(data.expireTime) : null
|
||||||
|
accountType.value = data.accountType || 'trial'
|
||||||
|
deviceTrialExpired.value = data.deviceTrialExpired || false
|
||||||
|
|
||||||
const deviceId = await getOrCreateDeviceId()
|
const deviceId = await getOrCreateDeviceId()
|
||||||
await deviceApi.register({
|
await deviceApi.register({
|
||||||
@@ -165,6 +185,31 @@ async function handleLoginSuccess(data: { token: string; permissions?: string; e
|
|||||||
os: navigator.platform
|
os: navigator.platform
|
||||||
})
|
})
|
||||||
SSEManager.connect()
|
SSEManager.connect()
|
||||||
|
|
||||||
|
// 根据不同场景显示提示
|
||||||
|
const accountExpired = vipExpireTime.value && new Date() > vipExpireTime.value
|
||||||
|
const deviceExpired = deviceTrialExpired.value
|
||||||
|
const isPaid = accountType.value === 'paid'
|
||||||
|
|
||||||
|
if (isPaid) {
|
||||||
|
// 场景5: 付费用户
|
||||||
|
ElMessage.success('登录成功')
|
||||||
|
} else if (deviceExpired && accountExpired) {
|
||||||
|
// 场景4: 试用已到期,请订阅
|
||||||
|
trialExpiredType.value = 'both'
|
||||||
|
showTrialExpiredDialog.value = true
|
||||||
|
} else if (accountExpired) {
|
||||||
|
// 场景3: 账号试用已到期,请订阅
|
||||||
|
trialExpiredType.value = 'account'
|
||||||
|
showTrialExpiredDialog.value = true
|
||||||
|
} else if (deviceExpired) {
|
||||||
|
// 场景2: 设备试用已到期,请更换设备或订阅
|
||||||
|
trialExpiredType.value = 'device'
|
||||||
|
showTrialExpiredDialog.value = true
|
||||||
|
} else {
|
||||||
|
// 场景1: 允许使用
|
||||||
|
ElMessage.success('登录成功')
|
||||||
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
isAuthenticated.value = false
|
isAuthenticated.value = false
|
||||||
showAuthDialog.value = true
|
showAuthDialog.value = true
|
||||||
@@ -179,6 +224,8 @@ function clearLocalAuth() {
|
|||||||
currentUsername.value = ''
|
currentUsername.value = ''
|
||||||
userPermissions.value = ''
|
userPermissions.value = ''
|
||||||
vipExpireTime.value = null
|
vipExpireTime.value = null
|
||||||
|
deviceTrialExpired.value = false
|
||||||
|
accountType.value = 'trial'
|
||||||
showAuthDialog.value = true
|
showAuthDialog.value = true
|
||||||
showDeviceDialog.value = false
|
showDeviceDialog.value = false
|
||||||
SSEManager.disconnect()
|
SSEManager.disconnect()
|
||||||
@@ -187,7 +234,7 @@ function clearLocalAuth() {
|
|||||||
async function logout() {
|
async function logout() {
|
||||||
try {
|
try {
|
||||||
const deviceId = getClientIdFromToken()
|
const deviceId = getClientIdFromToken()
|
||||||
if (deviceId) await deviceApi.offline({ deviceId })
|
if (deviceId) await deviceApi.offline({ deviceId, username: currentUsername.value })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('离线通知失败:', error)
|
console.warn('离线通知失败:', error)
|
||||||
}
|
}
|
||||||
@@ -235,6 +282,8 @@ async function checkAuth() {
|
|||||||
isAuthenticated.value = true
|
isAuthenticated.value = true
|
||||||
currentUsername.value = getUsernameFromToken(token)
|
currentUsername.value = getUsernameFromToken(token)
|
||||||
userPermissions.value = res.data.permissions || ''
|
userPermissions.value = res.data.permissions || ''
|
||||||
|
deviceTrialExpired.value = res.data.deviceTrialExpired || false
|
||||||
|
accountType.value = res.data.accountType || 'trial'
|
||||||
|
|
||||||
if (res.data.expireTime) {
|
if (res.data.expireTime) {
|
||||||
vipExpireTime.value = new Date(res.data.expireTime)
|
vipExpireTime.value = new Date(res.data.expireTime)
|
||||||
@@ -249,6 +298,38 @@ async function checkAuth() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 刷新VIP状态(采集前调用)
|
||||||
|
async function refreshVipStatus() {
|
||||||
|
try {
|
||||||
|
const token = getToken()
|
||||||
|
if (!token) return false
|
||||||
|
|
||||||
|
const res = await authApi.verifyToken(token)
|
||||||
|
deviceTrialExpired.value = res.data.deviceTrialExpired || false
|
||||||
|
accountType.value = res.data.accountType || 'trial'
|
||||||
|
if (res.data.expireTime) {
|
||||||
|
vipExpireTime.value = new Date(res.data.expireTime)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断过期类型
|
||||||
|
function checkExpiredType(): 'device' | 'account' | 'both' {
|
||||||
|
const accountExpired = vipExpireTime.value && new Date() > vipExpireTime.value
|
||||||
|
const deviceExpired = deviceTrialExpired.value
|
||||||
|
|
||||||
|
if (deviceExpired && accountExpired) return 'both'
|
||||||
|
if (accountExpired) return 'account'
|
||||||
|
if (deviceExpired) return 'device'
|
||||||
|
return 'account' // 默认
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提供给子组件使用
|
||||||
|
provide('refreshVipStatus', refreshVipStatus)
|
||||||
|
provide('checkExpiredType', checkExpiredType)
|
||||||
|
|
||||||
const SSEManager = {
|
const SSEManager = {
|
||||||
connection: null as EventSource | null,
|
connection: null as EventSource | null,
|
||||||
@@ -364,7 +445,7 @@ async function confirmRemoveDevice(row: DeviceItem) {
|
|||||||
type: 'warning'
|
type: 'warning'
|
||||||
})
|
})
|
||||||
|
|
||||||
await deviceApi.remove({deviceId: row.deviceId})
|
await deviceApi.remove({deviceId: row.deviceId, username: currentUsername.value})
|
||||||
devices.value = devices.value.filter(d => d.deviceId !== row.deviceId)
|
devices.value = devices.value.filter(d => d.deviceId !== row.deviceId)
|
||||||
deviceQuota.value.used = Math.max(0, deviceQuota.value.used - 1)
|
deviceQuota.value.used = Math.max(0, deviceQuota.value.used - 1)
|
||||||
|
|
||||||
@@ -382,8 +463,30 @@ async function confirmRemoveDevice(row: DeviceItem) {
|
|||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
showContent()
|
showContent()
|
||||||
await checkAuth()
|
await checkAuth()
|
||||||
|
|
||||||
|
// 检查是否有待安装的更新
|
||||||
|
await checkPendingUpdate()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
async function checkPendingUpdate() {
|
||||||
|
try {
|
||||||
|
const result = await (window as any).electronAPI.checkPendingUpdate()
|
||||||
|
if (result && result.hasPendingUpdate) {
|
||||||
|
// 有待安装的更新,直接弹出安装对话框
|
||||||
|
showUpdateDialog.value = true
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('检查待安装更新失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理自动更新配置变化
|
||||||
|
function handleAutoUpdateChanged(enabled: boolean) {
|
||||||
|
if (enabled && updateDialogRef.value) {
|
||||||
|
updateDialogRef.value.checkForUpdatesNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
SSEManager.disconnect()
|
SSEManager.disconnect()
|
||||||
})
|
})
|
||||||
@@ -461,7 +564,7 @@ onUnmounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<keep-alive v-if="activeDashboard">
|
<keep-alive v-if="activeDashboard">
|
||||||
<component :is="activeDashboard" :key="activeMenu" :is-vip="vipStatus.isVip"/>
|
<component :is="activeDashboard" :key="activeMenu" :is-vip="canUseFunctions"/>
|
||||||
</keep-alive>
|
</keep-alive>
|
||||||
<div v-if="showPlaceholder" class="placeholder">
|
<div v-if="showPlaceholder" class="placeholder">
|
||||||
<div class="placeholder-card">
|
<div class="placeholder-card">
|
||||||
@@ -483,10 +586,13 @@ onUnmounted(() => {
|
|||||||
@back-to-login="backToLogin"/>
|
@back-to-login="backToLogin"/>
|
||||||
|
|
||||||
<!-- 更新对话框 -->
|
<!-- 更新对话框 -->
|
||||||
<UpdateDialog v-model="showUpdateDialog" />
|
<UpdateDialog ref="updateDialogRef" v-model="showUpdateDialog" />
|
||||||
|
|
||||||
<!-- 设置对话框 -->
|
<!-- 设置对话框 -->
|
||||||
<SettingsDialog v-model="showSettingsDialog" />
|
<SettingsDialog v-model="showSettingsDialog" @auto-update-changed="handleAutoUpdateChanged" />
|
||||||
|
|
||||||
|
<!-- 试用期过期弹框 -->
|
||||||
|
<TrialExpiredDialog v-model="showTrialExpiredDialog" :expired-type="trialExpiredType" />
|
||||||
|
|
||||||
<!-- 设备管理弹框 -->
|
<!-- 设备管理弹框 -->
|
||||||
<el-dialog
|
<el-dialog
|
||||||
|
|||||||
@@ -27,12 +27,22 @@ function buildQuery(params?: Record<string, unknown>): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function request<T>(path: string, options: RequestInit): Promise<T> {
|
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}`, {
|
const res = await fetch(`${resolveBase(path)}${path}`, {
|
||||||
credentials: 'omit',
|
credentials: 'omit',
|
||||||
cache: 'no-store',
|
cache: 'no-store',
|
||||||
...options,
|
...options,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
...(token ? { 'Authorization': `Bearer ${token}` } : {}),
|
||||||
...options.headers
|
...options.headers
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -72,12 +82,27 @@ export const http = {
|
|||||||
return request<T>(path, { method: 'DELETE' });
|
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}`, {
|
return fetch(`${resolveBase(path)}${path}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: form,
|
body: form,
|
||||||
credentials: 'omit',
|
credentials: 'omit',
|
||||||
cache: 'no-store'
|
cache: 'no-store',
|
||||||
|
headers
|
||||||
}).then(async res => {
|
}).then(async res => {
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const text = await res.text().catch(() => '');
|
const text = await res.text().catch(() => '');
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import { http } from './http'
|
import { http } from './http'
|
||||||
|
|
||||||
export const zebraApi = {
|
export const zebraApi = {
|
||||||
getAccounts() {
|
getAccounts(name?: string) {
|
||||||
return http.get('/tool/banma/accounts')
|
return http.get('/tool/banma/accounts', name ? { name } : undefined)
|
||||||
},
|
},
|
||||||
|
|
||||||
saveAccount(body: any) {
|
saveAccount(body: any, name?: string) {
|
||||||
return http.post('/tool/banma/accounts', body)
|
const url = name ? `/tool/banma/accounts?name=${encodeURIComponent(name)}` : '/tool/banma/accounts'
|
||||||
|
return http.post(url, body)
|
||||||
},
|
},
|
||||||
|
|
||||||
removeAccount(id: number) {
|
removeAccount(id: number) {
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
<script setup lang="ts">
|
<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 { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { amazonApi } from '../../api/amazon'
|
import { amazonApi } from '../../api/amazon'
|
||||||
import { handlePlatformFileExport } from '../../utils/settings'
|
import { handlePlatformFileExport } from '../../utils/settings'
|
||||||
|
|
||||||
|
const TrialExpiredDialog = defineAsyncComponent(() => import('../common/TrialExpiredDialog.vue'))
|
||||||
|
|
||||||
|
const refreshVipStatus = inject<() => Promise<boolean>>('refreshVipStatus')
|
||||||
|
|
||||||
// 接收VIP状态
|
// 接收VIP状态
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
isVip: boolean
|
isVip: boolean
|
||||||
@@ -24,6 +28,12 @@ const pageSize = ref(15)
|
|||||||
const amazonUpload = ref<HTMLInputElement | null>(null)
|
const amazonUpload = ref<HTMLInputElement | null>(null)
|
||||||
const dragActive = ref(false)
|
const dragActive = ref(false)
|
||||||
|
|
||||||
|
// 试用期过期弹框
|
||||||
|
const showTrialExpiredDialog = ref(false)
|
||||||
|
const trialExpiredType = ref<'device' | 'account' | 'both'>('account')
|
||||||
|
|
||||||
|
const checkExpiredType = inject<() => 'device' | 'account' | 'both'>('checkExpiredType')
|
||||||
|
|
||||||
// 计算属性 - 当前页数据
|
// 计算属性 - 当前页数据
|
||||||
const paginatedData = computed(() => {
|
const paginatedData = computed(() => {
|
||||||
const start = (currentPage.value - 1) * pageSize.value
|
const start = (currentPage.value - 1) * pageSize.value
|
||||||
@@ -90,19 +100,13 @@ async function onDrop(e: DragEvent) {
|
|||||||
|
|
||||||
// 批量获取产品信息 - 核心数据处理逻辑
|
// 批量获取产品信息 - 核心数据处理逻辑
|
||||||
async function batchGetProductInfo(asinList: string[]) {
|
async function batchGetProductInfo(asinList: string[]) {
|
||||||
|
// 刷新VIP状态
|
||||||
|
if (refreshVipStatus) await refreshVipStatus()
|
||||||
|
|
||||||
// VIP检查
|
// VIP检查
|
||||||
if (!props.isVip) {
|
if (!props.isVip) {
|
||||||
try {
|
if (checkExpiredType) trialExpiredType.value = checkExpiredType()
|
||||||
await ElMessageBox.confirm(
|
showTrialExpiredDialog.value = true
|
||||||
'VIP已过期,数据采集功能受限。请联系管理员续费后继续使用。',
|
|
||||||
'VIP功能限制',
|
|
||||||
{
|
|
||||||
confirmButtonText: '我知道了',
|
|
||||||
showCancelButton: false,
|
|
||||||
type: 'warning'
|
|
||||||
}
|
|
||||||
)
|
|
||||||
} catch {}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -391,6 +395,9 @@ onMounted(async () => {
|
|||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 试用期过期弹框 -->
|
||||||
|
<TrialExpiredDialog v-model="showTrialExpiredDialog" :expired-type="trialExpiredType" />
|
||||||
|
|
||||||
<!-- 表格上方进度条 -->
|
<!-- 表格上方进度条 -->
|
||||||
<div v-if="progressVisible" class="progress-head">
|
<div v-if="progressVisible" class="progress-head">
|
||||||
<div class="progress-section">
|
<div class="progress-section">
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ interface Props {
|
|||||||
|
|
||||||
interface Emits {
|
interface Emits {
|
||||||
(e: 'update:modelValue', value: boolean): void
|
(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
|
(e: 'showRegister'): void
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,7 +51,9 @@ async function handleAuth() {
|
|||||||
emit('loginSuccess', {
|
emit('loginSuccess', {
|
||||||
token: loginRes.data.accessToken || loginRes.data.token,
|
token: loginRes.data.accessToken || loginRes.data.token,
|
||||||
permissions: loginRes.data.permissions,
|
permissions: loginRes.data.permissions,
|
||||||
expireTime: loginRes.data.expireTime
|
expireTime: loginRes.data.expireTime,
|
||||||
|
accountType: loginRes.data.accountType,
|
||||||
|
deviceTrialExpired: loginRes.data.deviceTrialExpired || false
|
||||||
})
|
})
|
||||||
ElMessage.success('登录成功')
|
ElMessage.success('登录成功')
|
||||||
resetForm()
|
resetForm()
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ interface Props {
|
|||||||
|
|
||||||
interface Emits {
|
interface Emits {
|
||||||
(e: 'update:modelValue', value: boolean): void
|
(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
|
(e: 'backToLogin'): void
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,24 +65,20 @@ async function handleRegister() {
|
|||||||
deviceId: deviceId
|
deviceId: deviceId
|
||||||
})
|
})
|
||||||
|
|
||||||
// 显示注册成功和VIP信息
|
// 显示注册成功提示
|
||||||
if (registerRes.data.expireTime) {
|
if (registerRes.data.deviceTrialExpired) {
|
||||||
const expireDate = new Date(registerRes.data.expireTime)
|
ElMessage.warning('注册成功!您获得了3天VIP体验,但该设备试用期已过,请更换设备或联系管理员续费')
|
||||||
const now = new Date()
|
} else {
|
||||||
const daysLeft = Math.ceil((expireDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24))
|
ElMessage.success('注册成功!您获得了3天VIP体验')
|
||||||
|
|
||||||
if (daysLeft > 0) {
|
|
||||||
ElMessage.success(`注册成功!您获得了 ${daysLeft} 天VIP体验`)
|
|
||||||
} else {
|
|
||||||
ElMessage.warning('注册成功!该设备已使用过新人福利,请联系管理员续费')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用注册返回的token直接登录
|
// 使用注册返回的token直接登录
|
||||||
emit('loginSuccess', {
|
emit('loginSuccess', {
|
||||||
token: registerRes.data.accessToken || registerRes.data.token,
|
token: registerRes.data.accessToken || registerRes.data.token,
|
||||||
permissions: registerRes.data.permissions,
|
permissions: registerRes.data.permissions,
|
||||||
expireTime: registerRes.data.expireTime
|
expireTime: registerRes.data.expireTime,
|
||||||
|
accountType: registerRes.data.accountType,
|
||||||
|
deviceTrialExpired: registerRes.data.deviceTrialExpired || false
|
||||||
})
|
})
|
||||||
resetForm()
|
resetForm()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import { ref, onMounted, computed } from 'vue'
|
import { ref, onMounted, computed } from 'vue'
|
||||||
import { zebraApi, type BanmaAccount } from '../../api/zebra'
|
import { zebraApi, type BanmaAccount } from '../../api/zebra'
|
||||||
import { ElMessageBox, ElMessage } from 'element-plus'
|
import { ElMessageBox, ElMessage } from 'element-plus'
|
||||||
|
import { getUsernameFromToken } from '../../utils/token'
|
||||||
|
|
||||||
type PlatformKey = 'zebra' | 'shopee' | 'rakuten' | 'amazon'
|
type PlatformKey = 'zebra' | 'shopee' | 'rakuten' | 'amazon'
|
||||||
|
|
||||||
@@ -18,7 +19,8 @@ const PLATFORM_LABEL: Record<PlatformKey, string> = {
|
|||||||
|
|
||||||
const accounts = ref<BanmaAccount[]>([])
|
const accounts = ref<BanmaAccount[]>([])
|
||||||
async function load() {
|
async function load() {
|
||||||
const res = await zebraApi.getAccounts()
|
const username = getUsernameFromToken()
|
||||||
|
const res = await zebraApi.getAccounts(username)
|
||||||
const list = (res as any)?.data ?? res
|
const list = (res as any)?.data ?? res
|
||||||
accounts.value = Array.isArray(list) ? list : []
|
accounts.value = Array.isArray(list) ? list : []
|
||||||
}
|
}
|
||||||
|
|||||||
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-if="prog.current !== '0 MB' && prog.total !== '0 MB'">{{ prog.current }} / {{ prog.total }}</span>
|
||||||
<span style="font-weight: 500" v-else>下载完成</span>
|
<span style="font-weight: 500" v-else>下载完成</span>
|
||||||
<div class="action-buttons">
|
<div class="action-buttons">
|
||||||
|
<el-button size="small" @click="clearDownloadedFiles">清除下载</el-button>
|
||||||
<el-button size="small" type="primary" @click="installUpdate">立即重启</el-button>
|
<el-button size="small" type="primary" @click="installUpdate">立即重启</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -99,10 +100,18 @@
|
|||||||
import {ref, computed, onMounted, onUnmounted, watch} from 'vue'
|
import {ref, computed, onMounted, onUnmounted, watch} from 'vue'
|
||||||
import {ElMessage, ElMessageBox} from 'element-plus'
|
import {ElMessage, ElMessageBox} from 'element-plus'
|
||||||
import {updateApi} from '../../api/update'
|
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] }>()
|
const emit = defineEmits<{ 'update:modelValue': [value: boolean] }>()
|
||||||
|
|
||||||
|
// 暴露方法给父组件调用
|
||||||
|
defineExpose({
|
||||||
|
checkForUpdatesNow
|
||||||
|
})
|
||||||
|
|
||||||
const show = computed({
|
const show = computed({
|
||||||
get: () => props.modelValue,
|
get: () => props.modelValue,
|
||||||
set: (value) => emit('update:modelValue', value)
|
set: (value) => emit('update:modelValue', value)
|
||||||
@@ -119,7 +128,7 @@ const info = ref({
|
|||||||
downloadUrl: '',
|
downloadUrl: '',
|
||||||
asarUrl: '',
|
asarUrl: '',
|
||||||
jarUrl: '',
|
jarUrl: '',
|
||||||
updateNotes: '• 优化了用户界面体验\n• 修复了已知问题\n• 提升了系统稳定性',
|
updateNotes: '',
|
||||||
currentVersion: '',
|
currentVersion: '',
|
||||||
hasUpdate: false
|
hasUpdate: false
|
||||||
})
|
})
|
||||||
@@ -135,9 +144,7 @@ async function autoCheck(silent = false) {
|
|||||||
|
|
||||||
if (!result.needUpdate) {
|
if (!result.needUpdate) {
|
||||||
hasNewVersion.value = false
|
hasNewVersion.value = false
|
||||||
if (!silent) {
|
if (!silent) ElMessage.info('当前已是最新版本')
|
||||||
ElMessage.info('当前已是最新版本')
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,52 +155,50 @@ async function autoCheck(silent = false) {
|
|||||||
downloadUrl: result.downloadUrl || '',
|
downloadUrl: result.downloadUrl || '',
|
||||||
asarUrl: result.asarUrl || '',
|
asarUrl: result.asarUrl || '',
|
||||||
jarUrl: result.jarUrl || '',
|
jarUrl: result.jarUrl || '',
|
||||||
updateNotes: '• 优化了用户界面体验\n• 修复了已知问题\n• 提升了系统稳定性\n• 同步更新前端和后端',
|
updateNotes: result.updateNotes || '',
|
||||||
hasUpdate: true
|
hasUpdate: true
|
||||||
}
|
}
|
||||||
hasNewVersion.value = true
|
hasNewVersion.value = true
|
||||||
|
|
||||||
// 检查是否跳过此版本
|
|
||||||
const skippedVersion = localStorage.getItem(SKIP_VERSION_KEY)
|
const skippedVersion = localStorage.getItem(SKIP_VERSION_KEY)
|
||||||
if (skippedVersion === result.latestVersion) {
|
if (skippedVersion === result.latestVersion) return
|
||||||
// 跳过的版本:显示小红点,但不弹框
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否在稍后提醒时间内
|
|
||||||
const remindLater = localStorage.getItem(REMIND_LATER_KEY)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 首次发现新版本:显示小红点并弹框
|
|
||||||
show.value = true
|
show.value = true
|
||||||
stage.value = 'check'
|
stage.value = 'check'
|
||||||
if (!silent) {
|
if (!silent) ElMessage.success('发现新版本')
|
||||||
ElMessage.success('发现新版本')
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('检查更新失败:', error)
|
if (!silent) ElMessage.error('检查更新失败')
|
||||||
if (!silent) {
|
|
||||||
ElMessage.error('检查更新失败')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleVersionClick() {
|
function handleVersionClick() {
|
||||||
// 如果有新版本,直接显示更新对话框
|
if (stage.value === 'downloading' || stage.value === 'completed') {
|
||||||
|
show.value = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (hasNewVersion.value) {
|
if (hasNewVersion.value) {
|
||||||
// 重置状态确保从检查阶段开始
|
|
||||||
stage.value = 'check'
|
stage.value = 'check'
|
||||||
prog.value = {percentage: 0, current: '0 MB', total: '0 MB'}
|
|
||||||
show.value = true
|
show.value = true
|
||||||
} else {
|
} else {
|
||||||
// 没有新版本,执行检查更新
|
checkForUpdatesNow()
|
||||||
autoCheck(false)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 立即检查更新(供外部调用)
|
||||||
|
async function checkForUpdatesNow() {
|
||||||
|
await autoCheck(false)
|
||||||
|
}
|
||||||
|
|
||||||
function skipVersion() {
|
function skipVersion() {
|
||||||
localStorage.setItem(SKIP_VERSION_KEY, info.value.latestVersion)
|
localStorage.setItem(SKIP_VERSION_KEY, info.value.latestVersion)
|
||||||
show.value = false
|
show.value = false
|
||||||
@@ -206,11 +211,63 @@ function remindLater() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function start() {
|
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) {
|
if (!info.value.asarUrl && !info.value.jarUrl) {
|
||||||
ElMessage.error('下载链接不可用')
|
ElMessage.error('下载链接不可用')
|
||||||
return
|
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,
|
||||||
|
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
|
||||||
|
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) {
|
||||||
|
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'
|
stage.value = 'downloading'
|
||||||
prog.value = {percentage: 0, current: '0 MB', total: '0 MB'}
|
prog.value = {percentage: 0, current: '0 MB', total: '0 MB'}
|
||||||
|
|
||||||
@@ -231,32 +288,34 @@ async function start() {
|
|||||||
if (response.success) {
|
if (response.success) {
|
||||||
stage.value = 'completed'
|
stage.value = 'completed'
|
||||||
prog.value.percentage = 100
|
prog.value.percentage = 100
|
||||||
// 如果没有有效的进度信息,设置默认值
|
show.value = true
|
||||||
if (prog.value.current === '0 MB' && prog.value.total === '0 MB') {
|
ElMessage.success('更新已下载完成,可以安装了')
|
||||||
// 保持原有的"0 MB"值,让模板中的条件判断来处理显示
|
|
||||||
}
|
|
||||||
ElMessage.success('下载完成')
|
|
||||||
} else {
|
} else {
|
||||||
ElMessage.error('下载失败: ' + (response.error || '未知错误'))
|
|
||||||
stage.value = 'check'
|
stage.value = 'check'
|
||||||
|
;(window as any).electronAPI.removeDownloadProgressListener()
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('下载失败:', error)
|
|
||||||
ElMessage.error('下载失败')
|
|
||||||
stage.value = 'check'
|
stage.value = 'check'
|
||||||
|
;(window as any).electronAPI.removeDownloadProgressListener()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function cancelDownload() {
|
async function cancelDownload() {
|
||||||
try {
|
try {
|
||||||
(window as any).electronAPI.removeDownloadProgressListener()
|
;(window as any).electronAPI.removeDownloadProgressListener()
|
||||||
await (window as any).electronAPI.cancelDownload()
|
await (window as any).electronAPI.cancelDownload()
|
||||||
show.value = false
|
|
||||||
stage.value = 'check'
|
stage.value = 'check'
|
||||||
|
prog.value = {percentage: 0, current: '0 MB', total: '0 MB'}
|
||||||
|
hasNewVersion.value = false
|
||||||
|
show.value = false
|
||||||
|
|
||||||
|
ElMessage.info('已取消下载')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('取消下载失败:', error)
|
|
||||||
show.value = false
|
|
||||||
stage.value = 'check'
|
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 () => {
|
onMounted(async () => {
|
||||||
version.value = await (window as any).electronAPI.getJarVersion()
|
version.value = await (window as any).electronAPI.getJarVersion()
|
||||||
await autoCheck(true)
|
const pendingUpdate = await (window as any).electronAPI.checkPendingUpdate()
|
||||||
})
|
|
||||||
|
|
||||||
// 监听对话框关闭,重置状态
|
if (pendingUpdate && pendingUpdate.hasPendingUpdate) {
|
||||||
watch(show, (newValue) => {
|
stage.value = 'completed'
|
||||||
if (!newValue) {
|
prog.value.percentage = 100
|
||||||
// 对话框关闭时重置状态
|
return
|
||||||
stage.value = 'check'
|
|
||||||
prog.value = {percentage: 0, current: '0 MB', total: '0 MB'}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await autoCheck(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
@@ -315,8 +402,10 @@ onUnmounted(() => {
|
|||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
transition: all 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.update-badge {
|
.update-badge {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -2px;
|
top: -2px;
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
<script setup lang="ts">
|
<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 { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import {rakutenApi} from '../../api/rakuten'
|
import {rakutenApi} from '../../api/rakuten'
|
||||||
import { batchConvertImages } from '../../utils/imageProxy'
|
import { batchConvertImages } from '../../utils/imageProxy'
|
||||||
import { handlePlatformFileExport } from '../../utils/settings'
|
import { handlePlatformFileExport } from '../../utils/settings'
|
||||||
|
|
||||||
|
const TrialExpiredDialog = defineAsyncComponent(() => import('../common/TrialExpiredDialog.vue'))
|
||||||
|
|
||||||
|
const refreshVipStatus = inject<() => Promise<boolean>>('refreshVipStatus')
|
||||||
|
|
||||||
// 接收VIP状态
|
// 接收VIP状态
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
isVip: boolean
|
isVip: boolean
|
||||||
@@ -49,6 +53,12 @@ const activeStep = computed(() => {
|
|||||||
return 2
|
return 2
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 试用期过期弹框
|
||||||
|
const showTrialExpiredDialog = ref(false)
|
||||||
|
const trialExpiredType = ref<'device' | 'account' | 'both'>('account')
|
||||||
|
|
||||||
|
const checkExpiredType = inject<() => 'device' | 'account' | 'both'>('checkExpiredType')
|
||||||
|
|
||||||
// 左侧:上传文件名与地区
|
// 左侧:上传文件名与地区
|
||||||
const selectedFileName = ref('')
|
const selectedFileName = ref('')
|
||||||
const pendingFile = ref<File | null>(null)
|
const pendingFile = ref<File | null>(null)
|
||||||
@@ -128,7 +138,8 @@ async function searchProductInternal(product: any) {
|
|||||||
if (!product || !product.imgUrl) return
|
if (!product || !product.imgUrl) return
|
||||||
if (!needsSearch(product)) return
|
if (!needsSearch(product)) return
|
||||||
if (!props.isVip) {
|
if (!props.isVip) {
|
||||||
ElMessage.warning('VIP已过期,1688识图功能受限')
|
if (checkExpiredType) trialExpiredType.value = checkExpiredType()
|
||||||
|
showTrialExpiredDialog.value = true
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,19 +209,13 @@ async function onDrop(e: DragEvent) {
|
|||||||
|
|
||||||
// 点击"获取数据
|
// 点击"获取数据
|
||||||
async function handleStartSearch() {
|
async function handleStartSearch() {
|
||||||
|
// 刷新VIP状态
|
||||||
|
if (refreshVipStatus) await refreshVipStatus()
|
||||||
|
|
||||||
// VIP检查
|
// VIP检查
|
||||||
if (!props.isVip) {
|
if (!props.isVip) {
|
||||||
try {
|
if (checkExpiredType) trialExpiredType.value = checkExpiredType()
|
||||||
await ElMessageBox.confirm(
|
showTrialExpiredDialog.value = true
|
||||||
'VIP已过期,数据采集功能受限。请联系管理员续费后继续使用。',
|
|
||||||
'VIP功能限制',
|
|
||||||
{
|
|
||||||
confirmButtonText: '我知道了',
|
|
||||||
showCancelButton: false,
|
|
||||||
type: 'warning'
|
|
||||||
}
|
|
||||||
)
|
|
||||||
} catch {}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -509,6 +514,9 @@ onMounted(loadLatest)
|
|||||||
<el-button type="primary" class="btn-blue" @click="rakutenExampleVisible = false">我知道了</el-button>
|
<el-button type="primary" class="btn-blue" @click="rakutenExampleVisible = false">我知道了</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 试用期过期弹框 -->
|
||||||
|
<TrialExpiredDialog v-model="showTrialExpiredDialog" :expired-type="trialExpiredType" />
|
||||||
<!-- 数据显示区域 -->
|
<!-- 数据显示区域 -->
|
||||||
<div class="table-container">
|
<div class="table-container">
|
||||||
<div class="table-section">
|
<div class="table-section">
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
<script setup lang="ts">
|
<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 { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { zebraApi, type ZebraOrder, type BanmaAccount } from '../../api/zebra'
|
import { zebraApi, type ZebraOrder, type BanmaAccount } from '../../api/zebra'
|
||||||
import AccountManager from '../common/AccountManager.vue'
|
import AccountManager from '../common/AccountManager.vue'
|
||||||
import { batchConvertImages } from '../../utils/imageProxy'
|
import { batchConvertImages } from '../../utils/imageProxy'
|
||||||
import { handlePlatformFileExport } from '../../utils/settings'
|
import { handlePlatformFileExport } from '../../utils/settings'
|
||||||
|
import { getUsernameFromToken } from '../../utils/token'
|
||||||
|
|
||||||
|
const TrialExpiredDialog = defineAsyncComponent(() => import('../common/TrialExpiredDialog.vue'))
|
||||||
|
|
||||||
|
const refreshVipStatus = inject<() => Promise<boolean>>('refreshVipStatus')
|
||||||
|
|
||||||
// 接收VIP状态
|
// 接收VIP状态
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
@@ -35,6 +40,12 @@ const fetchCurrentPage = ref(1)
|
|||||||
const fetchTotalPages = ref(0)
|
const fetchTotalPages = ref(0)
|
||||||
const fetchTotalItems = ref(0)
|
const fetchTotalItems = ref(0)
|
||||||
const isFetching = ref(false)
|
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) {
|
function selectAccount(id: number) {
|
||||||
accountId.value = id
|
accountId.value = id
|
||||||
loadShops()
|
loadShops()
|
||||||
@@ -68,7 +79,8 @@ async function loadShops() {
|
|||||||
|
|
||||||
async function loadAccounts() {
|
async function loadAccounts() {
|
||||||
try {
|
try {
|
||||||
const res = await zebraApi.getAccounts()
|
const username = getUsernameFromToken()
|
||||||
|
const res = await zebraApi.getAccounts(username)
|
||||||
const list = (res as any)?.data ?? res
|
const list = (res as any)?.data ?? res
|
||||||
accounts.value = Array.isArray(list) ? list : []
|
accounts.value = Array.isArray(list) ? list : []
|
||||||
const def = accounts.value.find(a => a.isDefault === 1) || accounts.value[0]
|
const def = accounts.value.find(a => a.isDefault === 1) || accounts.value[0]
|
||||||
@@ -91,19 +103,13 @@ function handleCurrentChange(page: number) {
|
|||||||
async function fetchData() {
|
async function fetchData() {
|
||||||
if (isFetching.value) return
|
if (isFetching.value) return
|
||||||
|
|
||||||
|
// 刷新VIP状态
|
||||||
|
if (refreshVipStatus) await refreshVipStatus()
|
||||||
|
|
||||||
// VIP检查
|
// VIP检查
|
||||||
if (!props.isVip) {
|
if (!props.isVip) {
|
||||||
try {
|
if (checkExpiredType) trialExpiredType.value = checkExpiredType()
|
||||||
await ElMessageBox.confirm(
|
showTrialExpiredDialog.value = true
|
||||||
'VIP已过期,数据采集功能受限。请联系管理员续费后继续使用。',
|
|
||||||
'VIP功能限制',
|
|
||||||
{
|
|
||||||
confirmButtonText: '我知道了',
|
|
||||||
showCancelButton: false,
|
|
||||||
type: 'warning'
|
|
||||||
}
|
|
||||||
)
|
|
||||||
} catch {}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -318,7 +324,8 @@ async function submitAccount() {
|
|||||||
status: accountForm.value.status || 1,
|
status: accountForm.value.status || 1,
|
||||||
}
|
}
|
||||||
try {
|
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
|
const id = (res as any)?.data?.id || (res as any)?.id
|
||||||
if (!id) throw new Error((res as any)?.msg || '保存失败')
|
if (!id) throw new Error((res as any)?.msg || '保存失败')
|
||||||
accountDialogVisible.value = false
|
accountDialogVisible.value = false
|
||||||
@@ -368,7 +375,6 @@ async function removeCurrentAccount() {
|
|||||||
<span :class="['status-dot', a.status === 1 ? 'on' : 'off']"></span>
|
<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" />
|
<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 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 v-if="accountId === a.id" class="acct-check">✔️</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -544,6 +550,10 @@ async function removeCurrentAccount() {
|
|||||||
<el-button type="primary" class="btn-blue" style="width: 100%" @click="submitAccount">登录</el-button>
|
<el-button type="primary" class="btn-blue" style="width: 100%" @click="submitAccount">登录</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 试用期过期弹框 -->
|
||||||
|
<TrialExpiredDialog v-model="showTrialExpiredDialog" :expired-type="trialExpiredType" />
|
||||||
|
|
||||||
<AccountManager ref="accountManagerRef" v-model="managerVisible" platform="zebra" @add="openAddAccount" @refresh="loadAccounts" />
|
<AccountManager ref="accountManagerRef" v-model="managerVisible" platform="zebra" @add="openAddAccount" @refresh="loadAccounts" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ export interface AppSettings {
|
|||||||
rakuten: PlatformExportSettings
|
rakuten: PlatformExportSettings
|
||||||
zebra: PlatformExportSettings
|
zebra: PlatformExportSettings
|
||||||
}
|
}
|
||||||
|
// 更新设置
|
||||||
|
autoUpdate?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const SETTINGS_KEY = 'app-settings'
|
const SETTINGS_KEY = 'app-settings'
|
||||||
@@ -31,7 +33,8 @@ const defaultSettings: AppSettings = {
|
|||||||
amazon: { ...defaultPlatformSettings },
|
amazon: { ...defaultPlatformSettings },
|
||||||
rakuten: { ...defaultPlatformSettings },
|
rakuten: { ...defaultPlatformSettings },
|
||||||
zebra: { ...defaultPlatformSettings }
|
zebra: { ...defaultPlatformSettings }
|
||||||
}
|
},
|
||||||
|
autoUpdate: false
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取设置
|
// 获取设置
|
||||||
@@ -45,7 +48,8 @@ export function getSettings(): AppSettings {
|
|||||||
amazon: { ...defaultSettings.platforms.amazon, ...settings.platforms?.amazon },
|
amazon: { ...defaultSettings.platforms.amazon, ...settings.platforms?.amazon },
|
||||||
rakuten: { ...defaultSettings.platforms.rakuten, ...settings.platforms?.rakuten },
|
rakuten: { ...defaultSettings.platforms.rakuten, ...settings.platforms?.rakuten },
|
||||||
zebra: { ...defaultSettings.platforms.zebra, ...settings.platforms?.zebra }
|
zebra: { ...defaultSettings.platforms.zebra, ...settings.platforms?.zebra }
|
||||||
}
|
},
|
||||||
|
autoUpdate: settings.autoUpdate ?? defaultSettings.autoUpdate
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return defaultSettings
|
return defaultSettings
|
||||||
@@ -60,7 +64,8 @@ export function saveSettings(settings: Partial<AppSettings>): void {
|
|||||||
amazon: { ...current.platforms.amazon, ...settings.platforms?.amazon },
|
amazon: { ...current.platforms.amazon, ...settings.platforms?.amazon },
|
||||||
rakuten: { ...current.platforms.rakuten, ...settings.platforms?.rakuten },
|
rakuten: { ...current.platforms.rakuten, ...settings.platforms?.rakuten },
|
||||||
zebra: { ...current.platforms.zebra, ...settings.platforms?.zebra }
|
zebra: { ...current.platforms.zebra, ...settings.platforms?.zebra }
|
||||||
}
|
},
|
||||||
|
autoUpdate: settings.autoUpdate ?? current.autoUpdate
|
||||||
}
|
}
|
||||||
localStorage.setItem(SETTINGS_KEY, JSON.stringify(updated))
|
localStorage.setItem(SETTINGS_KEY, JSON.stringify(updated))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,9 +38,6 @@ public class SystemController {
|
|||||||
|
|
||||||
@Value("${api.server.base-url}")
|
@Value("${api.server.base-url}")
|
||||||
private String serverBaseUrl;
|
private String serverBaseUrl;
|
||||||
|
|
||||||
// ==================== 认证管理 ====================
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 保存认证密钥
|
* 保存认证密钥
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -54,36 +54,18 @@ public class BanmaOrderServiceImpl implements IBanmaOrderService {
|
|||||||
builder.readTimeout(Duration.ofSeconds(10));
|
builder.readTimeout(Duration.ofSeconds(10));
|
||||||
restTemplate = builder.build();
|
restTemplate = builder.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
private void fetchTokenFromServer(Long accountId) {
|
private void fetchTokenFromServer(Long accountId) {
|
||||||
ResponseEntity<Map> resp = restTemplate.getForEntity(RUOYI_ADMIN_BASE + "/tool/banma/accounts", Map.class);
|
Map<String, Object> resp = restTemplate.getForObject(RUOYI_ADMIN_BASE + "/tool/banma/accounts", Map.class);
|
||||||
Object body = resp.getBody();
|
List<Map<String, Object>> list = (List<Map<String, Object>>) resp.get("data");
|
||||||
if (body == null) return;
|
if (list == null || list.isEmpty()) return;
|
||||||
Object data = ((Map<String, Object>) body).get("data");
|
Map<String, Object> account = accountId != null
|
||||||
List<Map<String, Object>> list;
|
? list.stream().filter(m -> accountId.equals(((Number) m.get("id")).longValue())).findFirst().orElse(null)
|
||||||
if (data instanceof List) {
|
: list.stream().filter(m -> ((Number) m.getOrDefault("isDefault", 0)).intValue() == 1).findFirst().orElse(list.get(0));
|
||||||
list = (List<Map<String, Object>>) data;
|
|
||||||
} else if (body instanceof Map && ((Map) body).get("list") instanceof List) {
|
if (account != null) {
|
||||||
list = (List<Map<String, Object>>) ((Map) body).get("list");
|
String token = (String) account.get("token");
|
||||||
} else {
|
currentAuthToken = token != null && token.startsWith("Bearer ") ? token : "Bearer " + token;
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (list.isEmpty()) return;
|
|
||||||
Map<String, Object> picked;
|
|
||||||
if (accountId != null) {
|
|
||||||
picked = list.stream().filter(m -> Objects.equals(((Number) m.get("id")).longValue(), accountId)).findFirst().orElse(null);
|
|
||||||
if (picked == null) return;
|
|
||||||
} else {
|
|
||||||
picked = list.stream()
|
|
||||||
.filter(m -> Objects.equals(((Number) m.getOrDefault("status", 1)).intValue(), 1))
|
|
||||||
.sorted((a,b) -> Integer.compare(((Number) b.getOrDefault("isDefault", 0)).intValue(), ((Number) a.getOrDefault("isDefault", 0)).intValue()))
|
|
||||||
.findFirst().orElse(list.get(0));
|
|
||||||
}
|
|
||||||
Object token = picked.get("token");
|
|
||||||
if (token instanceof String && !((String) token).isEmpty()) {
|
|
||||||
String t = (String) token;
|
|
||||||
currentAuthToken = t.startsWith("Bearer ") ? t : ("Bearer " + t);
|
|
||||||
currentAccountId = accountId;
|
currentAccountId = accountId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ import com.ruoyi.system.domain.SysCache;
|
|||||||
public class CacheController
|
public class CacheController
|
||||||
{
|
{
|
||||||
@Autowired
|
@Autowired
|
||||||
private RedisTemplate<String, String> redisTemplate;
|
private RedisTemplate<Object, Object> redisTemplate;
|
||||||
|
|
||||||
private final static List<SysCache> caches = new ArrayList<SysCache>();
|
private final static List<SysCache> caches = new ArrayList<SysCache>();
|
||||||
{
|
{
|
||||||
@@ -80,15 +80,16 @@ public class CacheController
|
|||||||
@GetMapping("/getKeys/{cacheName}")
|
@GetMapping("/getKeys/{cacheName}")
|
||||||
public AjaxResult getCacheKeys(@PathVariable String cacheName)
|
public AjaxResult getCacheKeys(@PathVariable String cacheName)
|
||||||
{
|
{
|
||||||
Set<String> cacheKeys = redisTemplate.keys(cacheName + "*");
|
Set<Object> cacheKeys = redisTemplate.keys(cacheName + "*");
|
||||||
return AjaxResult.success(new TreeSet<>(cacheKeys));
|
return AjaxResult.success(cacheKeys != null ? new TreeSet<>(cacheKeys.stream().map(String::valueOf).collect(java.util.stream.Collectors.toSet())) : new TreeSet<>());
|
||||||
}
|
}
|
||||||
|
|
||||||
@PreAuthorize("@ss.hasPermi('monitor:cache:list')")
|
@PreAuthorize("@ss.hasPermi('monitor:cache:list')")
|
||||||
@GetMapping("/getValue/{cacheName}/{cacheKey}")
|
@GetMapping("/getValue/{cacheName}/{cacheKey}")
|
||||||
public AjaxResult getCacheValue(@PathVariable String cacheName, @PathVariable String cacheKey)
|
public AjaxResult getCacheValue(@PathVariable String cacheName, @PathVariable String cacheKey)
|
||||||
{
|
{
|
||||||
String cacheValue = redisTemplate.opsForValue().get(cacheKey);
|
Object cacheValueObj = redisTemplate.opsForValue().get(cacheKey);
|
||||||
|
String cacheValue = cacheValueObj != null ? cacheValueObj.toString() : null;
|
||||||
SysCache sysCache = new SysCache(cacheName, cacheKey, cacheValue);
|
SysCache sysCache = new SysCache(cacheName, cacheKey, cacheValue);
|
||||||
return AjaxResult.success(sysCache);
|
return AjaxResult.success(sysCache);
|
||||||
}
|
}
|
||||||
@@ -97,8 +98,10 @@ public class CacheController
|
|||||||
@DeleteMapping("/clearCacheName/{cacheName}")
|
@DeleteMapping("/clearCacheName/{cacheName}")
|
||||||
public AjaxResult clearCacheName(@PathVariable String cacheName)
|
public AjaxResult clearCacheName(@PathVariable String cacheName)
|
||||||
{
|
{
|
||||||
Collection<String> cacheKeys = redisTemplate.keys(cacheName + "*");
|
Set<Object> cacheKeys = redisTemplate.keys(cacheName + "*");
|
||||||
redisTemplate.delete(cacheKeys);
|
if (cacheKeys != null && !cacheKeys.isEmpty()) {
|
||||||
|
redisTemplate.delete(cacheKeys);
|
||||||
|
}
|
||||||
return AjaxResult.success();
|
return AjaxResult.success();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,8 +117,10 @@ public class CacheController
|
|||||||
@DeleteMapping("/clearCacheAll")
|
@DeleteMapping("/clearCacheAll")
|
||||||
public AjaxResult clearCacheAll()
|
public AjaxResult clearCacheAll()
|
||||||
{
|
{
|
||||||
Collection<String> cacheKeys = redisTemplate.keys("*");
|
Set<Object> cacheKeys = redisTemplate.keys("*");
|
||||||
redisTemplate.delete(cacheKeys);
|
if (cacheKeys != null && !cacheKeys.isEmpty()) {
|
||||||
|
redisTemplate.delete(cacheKeys);
|
||||||
|
}
|
||||||
return AjaxResult.success();
|
return AjaxResult.success();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -149,7 +149,6 @@ public class ClientAccountController extends BaseController {
|
|||||||
if (userDevice >= deviceLimit) {
|
if (userDevice >= deviceLimit) {
|
||||||
return AjaxResult.error("设备数量已达上限(" + deviceLimit + "个),请先移除其他设备");
|
return AjaxResult.error("设备数量已达上限(" + deviceLimit + "个),请先移除其他设备");
|
||||||
}
|
}
|
||||||
|
|
||||||
String token = Jwts.builder()
|
String token = Jwts.builder()
|
||||||
.setHeaderParam("kid", jwtRsaKeyService.getKeyId())
|
.setHeaderParam("kid", jwtRsaKeyService.getKeyId())
|
||||||
.setSubject(username)
|
.setSubject(username)
|
||||||
@@ -161,15 +160,24 @@ public class ClientAccountController extends BaseController {
|
|||||||
.signWith(SignatureAlgorithm.RS256, jwtRsaKeyService.getPrivateKey())
|
.signWith(SignatureAlgorithm.RS256, jwtRsaKeyService.getPrivateKey())
|
||||||
.compact();
|
.compact();
|
||||||
|
|
||||||
|
// 检查设备试用期(仅对trial账号生效)
|
||||||
|
boolean deviceTrialExpired = false;
|
||||||
|
if ("trial".equals(account.getAccountType())) {
|
||||||
|
ClientDevice device = clientDeviceMapper.selectByDeviceIdAndUsername(clientId, username);
|
||||||
|
deviceTrialExpired = device != null
|
||||||
|
&& device.getTrialExpireTime() != null
|
||||||
|
&& new Date().after(device.getTrialExpireTime());
|
||||||
|
}
|
||||||
|
|
||||||
Map<String, Object> data = new HashMap<>();
|
Map<String, Object> data = new HashMap<>();
|
||||||
data.put("token", token);
|
data.put("token", token);
|
||||||
data.put("permissions", account.getPermissions());
|
data.put("permissions", account.getPermissions());
|
||||||
data.put("accountName", account.getAccountName());
|
data.put("accountName", account.getAccountName());
|
||||||
data.put("expireTime", account.getExpireTime());
|
data.put("expireTime", account.getExpireTime());
|
||||||
|
data.put("accountType", account.getAccountType());
|
||||||
|
data.put("deviceTrialExpired", deviceTrialExpired);
|
||||||
return AjaxResult.success(data);
|
return AjaxResult.success(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 验证token
|
* 验证token
|
||||||
*/
|
*/
|
||||||
@@ -181,17 +189,35 @@ public class ClientAccountController extends BaseController {
|
|||||||
.parseClaimsJws(token)
|
.parseClaimsJws(token)
|
||||||
.getBody();
|
.getBody();
|
||||||
String username = (String) claims.get("sub");
|
String username = (String) claims.get("sub");
|
||||||
|
String clientId = (String) claims.get("clientId");
|
||||||
|
|
||||||
ClientAccount account = clientAccountService.selectClientAccountByUsername(username);
|
ClientAccount account = clientAccountService.selectClientAccountByUsername(username);
|
||||||
if (account == null || !"0".equals(account.getStatus())) {
|
if (account == null || !"0".equals(account.getStatus())) {
|
||||||
return AjaxResult.error("token无效");
|
return AjaxResult.error("token无效");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 付费账号到期自动降级为试用账号
|
||||||
|
if ("paid".equals(account.getAccountType()) && account.getExpireTime() != null && new Date().after(account.getExpireTime())) {
|
||||||
|
account.setAccountType("trial");
|
||||||
|
clientAccountService.updateClientAccount(account);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查设备试用期(仅对trial账号生效)
|
||||||
|
boolean deviceTrialExpired = false;
|
||||||
|
if ("trial".equals(account.getAccountType())) {
|
||||||
|
ClientDevice device = clientDeviceMapper.selectByDeviceIdAndUsername(clientId, username);
|
||||||
|
deviceTrialExpired = device != null
|
||||||
|
&& device.getTrialExpireTime() != null
|
||||||
|
&& new Date().after(device.getTrialExpireTime());
|
||||||
|
}
|
||||||
|
|
||||||
Map<String, Object> result = new HashMap<>();
|
Map<String, Object> result = new HashMap<>();
|
||||||
result.put("username", username);
|
result.put("username", username);
|
||||||
result.put("permissions", account.getPermissions());
|
result.put("permissions", account.getPermissions());
|
||||||
result.put("accountName", account.getAccountName());
|
result.put("accountName", account.getAccountName());
|
||||||
result.put("expireTime", account.getExpireTime());
|
result.put("expireTime", account.getExpireTime());
|
||||||
|
result.put("accountType", account.getAccountType());
|
||||||
|
result.put("deviceTrialExpired", deviceTrialExpired);
|
||||||
return AjaxResult.success(result);
|
return AjaxResult.success(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,7 +243,7 @@ public class ClientAccountController extends BaseController {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 客户端账号注册
|
* 客户端账号注册
|
||||||
* 新设备注册送3天VIP,同一设备ID重复注册不赠送
|
* 新账号注册赠送3天VIP试用期
|
||||||
*/
|
*/
|
||||||
@PostMapping("/register")
|
@PostMapping("/register")
|
||||||
public AjaxResult register(@RequestBody Map<String, String> registerData) {
|
public AjaxResult register(@RequestBody Map<String, String> registerData) {
|
||||||
@@ -232,16 +258,8 @@ public class ClientAccountController extends BaseController {
|
|||||||
clientAccount.setStatus("0");
|
clientAccount.setStatus("0");
|
||||||
clientAccount.setPermissions("{\"amazon\":true,\"rakuten\":true,\"zebra\":true}");
|
clientAccount.setPermissions("{\"amazon\":true,\"rakuten\":true,\"zebra\":true}");
|
||||||
clientAccount.setPassword(passwordEncoder.encode(password));
|
clientAccount.setPassword(passwordEncoder.encode(password));
|
||||||
|
clientAccount.setAccountType("trial");
|
||||||
// 新设备赠送3天VIP
|
clientAccount.setExpireTime(new Date(System.currentTimeMillis() + 3 * 24L * 60 * 60 * 1000));
|
||||||
ClientDevice existingDevice = clientDeviceMapper.selectByDeviceId(deviceId);
|
|
||||||
int vipDays = (existingDevice == null) ? 3 : 0;
|
|
||||||
|
|
||||||
if (vipDays > 0) {
|
|
||||||
clientAccount.setExpireTime(new Date(System.currentTimeMillis() + vipDays * 24L * 60 * 60 * 1000));
|
|
||||||
} else {
|
|
||||||
clientAccount.setExpireTime(new Date());
|
|
||||||
}
|
|
||||||
|
|
||||||
int result = clientAccountService.insertClientAccount(clientAccount);
|
int result = clientAccountService.insertClientAccount(clientAccount);
|
||||||
if (result <= 0) {
|
if (result <= 0) {
|
||||||
@@ -263,6 +281,8 @@ public class ClientAccountController extends BaseController {
|
|||||||
data.put("permissions", clientAccount.getPermissions());
|
data.put("permissions", clientAccount.getPermissions());
|
||||||
data.put("accountName", clientAccount.getAccountName());
|
data.put("accountName", clientAccount.getAccountName());
|
||||||
data.put("expireTime", clientAccount.getExpireTime());
|
data.put("expireTime", clientAccount.getExpireTime());
|
||||||
|
data.put("accountType", clientAccount.getAccountType());
|
||||||
|
data.put("deviceTrialExpired", false);
|
||||||
return AjaxResult.success(data);
|
return AjaxResult.success(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -300,6 +320,7 @@ public class ClientAccountController extends BaseController {
|
|||||||
Date newExpireTime = cal.getTime();
|
Date newExpireTime = cal.getTime();
|
||||||
|
|
||||||
account.setExpireTime(newExpireTime);
|
account.setExpireTime(newExpireTime);
|
||||||
|
account.setAccountType("paid");
|
||||||
account.setUpdateBy(getUsername());
|
account.setUpdateBy(getUsername());
|
||||||
clientAccountService.updateClientAccount(account);
|
clientAccountService.updateClientAccount(account);
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ public class VersionController extends BaseController {
|
|||||||
private static final String VERSION_REDIS_KEY = "erp:client:version";
|
private static final String VERSION_REDIS_KEY = "erp:client:version";
|
||||||
private static final String ASAR_URL_REDIS_KEY = "erp:client:asar_url";
|
private static final String ASAR_URL_REDIS_KEY = "erp:client:asar_url";
|
||||||
private static final String JAR_URL_REDIS_KEY = "erp:client:jar_url";
|
private static final String JAR_URL_REDIS_KEY = "erp:client:jar_url";
|
||||||
|
private static final String UPDATE_NOTES_REDIS_KEY = "erp:client:update_notes";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检查版本更新
|
* 检查版本更新
|
||||||
@@ -43,6 +44,7 @@ public class VersionController extends BaseController {
|
|||||||
data.put("needUpdate", needUpdate);
|
data.put("needUpdate", needUpdate);
|
||||||
data.put("asarUrl", redisTemplate.opsForValue().get(ASAR_URL_REDIS_KEY));
|
data.put("asarUrl", redisTemplate.opsForValue().get(ASAR_URL_REDIS_KEY));
|
||||||
data.put("jarUrl", redisTemplate.opsForValue().get(JAR_URL_REDIS_KEY));
|
data.put("jarUrl", redisTemplate.opsForValue().get(JAR_URL_REDIS_KEY));
|
||||||
|
data.put("updateNotes", redisTemplate.opsForValue().get(UPDATE_NOTES_REDIS_KEY));
|
||||||
|
|
||||||
return AjaxResult.success(data);
|
return AjaxResult.success(data);
|
||||||
}
|
}
|
||||||
@@ -61,6 +63,7 @@ public class VersionController extends BaseController {
|
|||||||
data.put("currentVersion", currentVersion);
|
data.put("currentVersion", currentVersion);
|
||||||
data.put("asarUrl", redisTemplate.opsForValue().get(ASAR_URL_REDIS_KEY));
|
data.put("asarUrl", redisTemplate.opsForValue().get(ASAR_URL_REDIS_KEY));
|
||||||
data.put("jarUrl", redisTemplate.opsForValue().get(JAR_URL_REDIS_KEY));
|
data.put("jarUrl", redisTemplate.opsForValue().get(JAR_URL_REDIS_KEY));
|
||||||
|
data.put("updateNotes", redisTemplate.opsForValue().get(UPDATE_NOTES_REDIS_KEY));
|
||||||
data.put("updateTime", System.currentTimeMillis());
|
data.put("updateTime", System.currentTimeMillis());
|
||||||
|
|
||||||
return AjaxResult.success(data);
|
return AjaxResult.success(data);
|
||||||
@@ -73,8 +76,9 @@ public class VersionController extends BaseController {
|
|||||||
@PreAuthorize("@ss.hasPermi('system:version:update')")
|
@PreAuthorize("@ss.hasPermi('system:version:update')")
|
||||||
@PostMapping("/update")
|
@PostMapping("/update")
|
||||||
public AjaxResult updateVersionInfo(@RequestParam("version") String version,
|
public AjaxResult updateVersionInfo(@RequestParam("version") String version,
|
||||||
@RequestParam(value = "asarUrl", required = false) String asarUrl,
|
@RequestParam(value = "asarUrl", required = false) String asarUrl,
|
||||||
@RequestParam(value = "jarUrl", required = false) String jarUrl) {
|
@RequestParam(value = "jarUrl", required = false) String jarUrl,
|
||||||
|
@RequestParam("updateNotes") String updateNotes) {
|
||||||
redisTemplate.opsForValue().set(VERSION_REDIS_KEY, version);
|
redisTemplate.opsForValue().set(VERSION_REDIS_KEY, version);
|
||||||
if (StringUtils.isNotEmpty(asarUrl)) {
|
if (StringUtils.isNotEmpty(asarUrl)) {
|
||||||
redisTemplate.opsForValue().set(ASAR_URL_REDIS_KEY, asarUrl);
|
redisTemplate.opsForValue().set(ASAR_URL_REDIS_KEY, asarUrl);
|
||||||
@@ -82,11 +86,13 @@ public class VersionController extends BaseController {
|
|||||||
if (StringUtils.isNotEmpty(jarUrl)) {
|
if (StringUtils.isNotEmpty(jarUrl)) {
|
||||||
redisTemplate.opsForValue().set(JAR_URL_REDIS_KEY, jarUrl);
|
redisTemplate.opsForValue().set(JAR_URL_REDIS_KEY, jarUrl);
|
||||||
}
|
}
|
||||||
|
redisTemplate.opsForValue().set(UPDATE_NOTES_REDIS_KEY, updateNotes);
|
||||||
|
|
||||||
Map<String, Object> data = new HashMap<>();
|
Map<String, Object> data = new HashMap<>();
|
||||||
data.put("version", version);
|
data.put("version", version);
|
||||||
data.put("asarUrl", asarUrl);
|
data.put("asarUrl", asarUrl);
|
||||||
data.put("jarUrl", jarUrl);
|
data.put("jarUrl", jarUrl);
|
||||||
|
data.put("updateNotes", updateNotes);
|
||||||
data.put("updateTime", System.currentTimeMillis());
|
data.put("updateTime", System.currentTimeMillis());
|
||||||
return AjaxResult.success(data);
|
return AjaxResult.success(data);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,32 +97,33 @@ public class ClientDeviceController {
|
|||||||
*/
|
*/
|
||||||
@PostMapping("/register")
|
@PostMapping("/register")
|
||||||
public AjaxResult register(@RequestBody ClientDevice device, HttpServletRequest request) {
|
public AjaxResult register(@RequestBody ClientDevice device, HttpServletRequest request) {
|
||||||
ClientDevice exists = clientDeviceMapper.selectByDeviceId(device.getDeviceId());
|
|
||||||
String ip = IpUtils.getIpAddr(request);
|
String ip = IpUtils.getIpAddr(request);
|
||||||
String username = device.getUsername();
|
String username = device.getUsername();
|
||||||
|
String deviceId = device.getDeviceId();
|
||||||
String os = device.getOs();
|
String os = device.getOs();
|
||||||
String deviceName = username + "@" + ip + " (" + os + ")";
|
String deviceName = username + "@" + ip + " (" + os + ")";
|
||||||
|
ClientDevice exists = clientDeviceMapper.selectByDeviceIdAndUsername(deviceId, username);
|
||||||
if (exists == null) {
|
if (exists == null) {
|
||||||
// 检查设备数量限制
|
// 检查设备数量限制
|
||||||
try {
|
try {
|
||||||
checkDeviceLimit(device.getUsername(), device.getDeviceId());
|
checkDeviceLimit(username, deviceId);
|
||||||
} catch (RuntimeException e) {
|
} catch (RuntimeException e) {
|
||||||
return AjaxResult.error(e.getMessage());
|
return AjaxResult.error(e.getMessage());
|
||||||
}
|
}
|
||||||
device.setIp(ip);
|
device.setIp(ip);
|
||||||
device.setStatus("online");
|
device.setStatus("online");
|
||||||
device.setLastActiveAt(new java.util.Date());
|
device.setLastActiveAt(new java.util.Date());
|
||||||
|
device.setTrialExpireTime(new java.util.Date(System.currentTimeMillis() + 3 * 24L * 60 * 60 * 1000));
|
||||||
device.setName(deviceName);
|
device.setName(deviceName);
|
||||||
clientDeviceMapper.insert(device);
|
clientDeviceMapper.insert(device);
|
||||||
} else {
|
} else {
|
||||||
exists.setUsername(device.getUsername());
|
|
||||||
exists.setName(deviceName);
|
exists.setName(deviceName);
|
||||||
exists.setOs(device.getOs());
|
exists.setOs(os);
|
||||||
exists.setStatus("online");
|
exists.setStatus("online");
|
||||||
exists.setIp(ip);
|
exists.setIp(ip);
|
||||||
exists.setLocation(device.getLocation());
|
exists.setLocation(device.getLocation());
|
||||||
exists.setLastActiveAt(new java.util.Date());
|
exists.setLastActiveAt(new java.util.Date());
|
||||||
clientDeviceMapper.updateByDeviceId(exists);
|
clientDeviceMapper.updateByDeviceIdAndUsername(exists);
|
||||||
}
|
}
|
||||||
return AjaxResult.success();
|
return AjaxResult.success();
|
||||||
}
|
}
|
||||||
@@ -137,6 +138,15 @@ public class ClientDeviceController {
|
|||||||
clientDeviceMapper.updateByDeviceId(device);
|
clientDeviceMapper.updateByDeviceId(device);
|
||||||
return AjaxResult.success();
|
return AjaxResult.success();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改设备试用期过期时间
|
||||||
|
*/
|
||||||
|
@PostMapping("/updateExpire")
|
||||||
|
public AjaxResult updateExpire(@RequestBody ClientDevice device) {
|
||||||
|
clientDeviceMapper.updateByDeviceIdAndUsername(device);
|
||||||
|
return AjaxResult.success();
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* 移除设备
|
* 移除设备
|
||||||
* 根据 deviceId 删除设备绑定记录。
|
* 根据 deviceId 删除设备绑定记录。
|
||||||
@@ -144,19 +154,20 @@ public class ClientDeviceController {
|
|||||||
@PostMapping("/remove")
|
@PostMapping("/remove")
|
||||||
public AjaxResult remove(@RequestBody Map<String, String> body) {
|
public AjaxResult remove(@RequestBody Map<String, String> body) {
|
||||||
String deviceId = body.get("deviceId");
|
String deviceId = body.get("deviceId");
|
||||||
|
String username = body.get("username");
|
||||||
if (deviceId == null || deviceId.isEmpty()) {
|
if (deviceId == null || deviceId.isEmpty()) {
|
||||||
return AjaxResult.error("deviceId不能为空");
|
return AjaxResult.error("deviceId不能为空");
|
||||||
}
|
}
|
||||||
ClientDevice exists = clientDeviceMapper.selectByDeviceId(deviceId);
|
ClientDevice exists = clientDeviceMapper.selectByDeviceIdAndUsername(deviceId, username);
|
||||||
if (exists == null) {
|
if (exists == null) {
|
||||||
return AjaxResult.success();
|
return AjaxResult.success();
|
||||||
}
|
}
|
||||||
if (!"removed".equals(exists.getStatus())) {
|
if (!"removed".equals(exists.getStatus())) {
|
||||||
exists.setStatus("removed");
|
exists.setStatus("removed");
|
||||||
exists.setLastActiveAt(new java.util.Date());
|
exists.setLastActiveAt(new java.util.Date());
|
||||||
clientDeviceMapper.updateByDeviceId(exists);
|
clientDeviceMapper.updateByDeviceIdAndUsername(exists);
|
||||||
sseHubService.sendEvent(exists.getUsername(), deviceId, "DEVICE_REMOVED", "{}");
|
sseHubService.sendEvent(username, deviceId, "DEVICE_REMOVED", "{}");
|
||||||
sseHubService.disconnectDevice(exists.getUsername(), deviceId);
|
sseHubService.disconnectDevice(username, deviceId);
|
||||||
}
|
}
|
||||||
return AjaxResult.success();
|
return AjaxResult.success();
|
||||||
}
|
}
|
||||||
@@ -167,11 +178,13 @@ public class ClientDeviceController {
|
|||||||
@PostMapping("/offline")
|
@PostMapping("/offline")
|
||||||
public AjaxResult offline(@RequestBody Map<String, String> body) {
|
public AjaxResult offline(@RequestBody Map<String, String> body) {
|
||||||
String deviceId = body.get("deviceId");
|
String deviceId = body.get("deviceId");
|
||||||
ClientDevice device = clientDeviceMapper.selectByDeviceId(deviceId);
|
String username = body.get("username");
|
||||||
|
ClientDevice device = clientDeviceMapper.selectByDeviceIdAndUsername(deviceId, username);
|
||||||
|
if (device != null) {
|
||||||
device.setStatus("offline");
|
device.setStatus("offline");
|
||||||
device.setLastActiveAt(new java.util.Date());
|
device.setLastActiveAt(new java.util.Date());
|
||||||
clientDeviceMapper.updateByDeviceId(device);
|
clientDeviceMapper.updateByDeviceIdAndUsername(device);
|
||||||
|
}
|
||||||
return AjaxResult.success();
|
return AjaxResult.success();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,38 +195,37 @@ public class ClientDeviceController {
|
|||||||
*/
|
*/
|
||||||
@PostMapping("/heartbeat")
|
@PostMapping("/heartbeat")
|
||||||
public AjaxResult heartbeat(@RequestBody ClientDevice device, HttpServletRequest request) {
|
public AjaxResult heartbeat(@RequestBody ClientDevice device, HttpServletRequest request) {
|
||||||
ClientDevice exists = clientDeviceMapper.selectByDeviceId(device.getDeviceId());
|
|
||||||
String ip = IpUtils.getIpAddr(request);
|
String ip = IpUtils.getIpAddr(request);
|
||||||
String username = device.getUsername();
|
String username = device.getUsername();
|
||||||
|
String deviceId = device.getDeviceId();
|
||||||
String os = device.getOs();
|
String os = device.getOs();
|
||||||
String deviceName = username + "@" + ip + " (" + os + ")";
|
String deviceName = username + "@" + ip + " (" + os + ")";
|
||||||
|
|
||||||
|
ClientDevice exists = clientDeviceMapper.selectByDeviceIdAndUsername(deviceId, username);
|
||||||
if (exists == null) {
|
if (exists == null) {
|
||||||
// 新设备注册
|
// 新设备注册
|
||||||
device.setIp(ip);
|
device.setIp(ip);
|
||||||
device.setStatus("online");
|
device.setStatus("online");
|
||||||
device.setLastActiveAt(new java.util.Date());
|
device.setLastActiveAt(new java.util.Date());
|
||||||
|
device.setTrialExpireTime(new java.util.Date(System.currentTimeMillis() + 3 * 24L * 60 * 60 * 1000));
|
||||||
device.setName(deviceName);
|
device.setName(deviceName);
|
||||||
clientDeviceMapper.insert(device);
|
clientDeviceMapper.insert(device);
|
||||||
} else if ("removed".equals(exists.getStatus())) {
|
} else if ("removed".equals(exists.getStatus())) {
|
||||||
// 被移除设备重新激活
|
// 被移除设备重新激活
|
||||||
exists.setUsername(device.getUsername());
|
|
||||||
exists.setName(deviceName);
|
exists.setName(deviceName);
|
||||||
exists.setOs(device.getOs());
|
exists.setOs(os);
|
||||||
exists.setStatus("online");
|
exists.setStatus("online");
|
||||||
exists.setIp(ip);
|
exists.setIp(ip);
|
||||||
exists.setLocation(device.getLocation());
|
exists.setLocation(device.getLocation());
|
||||||
exists.setLastActiveAt(new java.util.Date());
|
exists.setLastActiveAt(new java.util.Date());
|
||||||
clientDeviceMapper.updateByDeviceId(exists);
|
clientDeviceMapper.updateByDeviceIdAndUsername(exists);
|
||||||
} else {
|
} else {
|
||||||
// 已存在设备更新
|
// 已存在设备更新
|
||||||
exists.setUsername(device.getUsername());
|
|
||||||
exists.setStatus("online");
|
exists.setStatus("online");
|
||||||
exists.setIp(ip);
|
exists.setIp(ip);
|
||||||
exists.setLastActiveAt(new java.util.Date());
|
exists.setLastActiveAt(new java.util.Date());
|
||||||
exists.setName(deviceName);
|
exists.setName(deviceName);
|
||||||
clientDeviceMapper.updateByDeviceId(exists);
|
clientDeviceMapper.updateByDeviceIdAndUsername(exists);
|
||||||
}
|
}
|
||||||
return AjaxResult.success();
|
return AjaxResult.success();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.ruoyi.web.controller.tool;
|
package com.ruoyi.web.controller.tool;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import com.ruoyi.common.annotation.Anonymous;
|
import com.ruoyi.common.annotation.Anonymous;
|
||||||
@@ -10,9 +11,7 @@ import com.ruoyi.system.domain.BanmaAccount;
|
|||||||
import com.ruoyi.system.service.IBanmaAccountService;
|
import com.ruoyi.system.service.IBanmaAccountService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 斑马账号管理(数据库版,极简接口):
|
* 斑马账号管理
|
||||||
* - 仅负责账号与 Token 的存取
|
|
||||||
* - 不参与登录/刷新与数据采集,客户端自行处理
|
|
||||||
*/
|
*/
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/tool/banma")
|
@RequestMapping("/tool/banma")
|
||||||
@@ -22,46 +21,35 @@ public class BanmaOrderController extends BaseController {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private IBanmaAccountService accountService;
|
private IBanmaAccountService accountService;
|
||||||
|
|
||||||
/**
|
|
||||||
* 查询账号列表(
|
|
||||||
*/
|
|
||||||
@GetMapping("/accounts")
|
@GetMapping("/accounts")
|
||||||
public R<?> listAccounts() {
|
public R<?> listAccounts(String name) {
|
||||||
List<BanmaAccount> list = accountService.listSimple();
|
return R.ok(accountService.listSimple(name));
|
||||||
return R.ok(list);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 新增或编辑账号(含设为默认)
|
|
||||||
*/
|
|
||||||
@PostMapping("/accounts")
|
@PostMapping("/accounts")
|
||||||
public R<?> saveAccount(@RequestBody BanmaAccount body) {
|
public R<?> saveAccount(@RequestBody BanmaAccount body, String name) {
|
||||||
Long id = accountService.saveOrUpdate(body);
|
if (body.getId() == null && accountService.validateAndGetToken(body.getUsername(), body.getPassword()) == null) {
|
||||||
boolean ok = false;
|
throw new RuntimeException("账号或密码错误");
|
||||||
try { ok = accountService.refreshToken(id); } catch (Exception ignore) {}
|
}
|
||||||
return ok ? R.ok(Map.of("id", id)) : R.fail("账号或密码错误,无法获取Token");
|
Long id = accountService.saveOrUpdate(body, name);
|
||||||
|
accountService.refreshToken(id);
|
||||||
|
return R.ok(Map.of("id", id));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 删除账号
|
|
||||||
*/
|
|
||||||
@DeleteMapping("/accounts/{id}")
|
@DeleteMapping("/accounts/{id}")
|
||||||
public R<?> remove(@PathVariable Long id) {
|
public R<?> remove(@PathVariable Long id) {
|
||||||
accountService.remove(id);
|
accountService.remove(id);
|
||||||
return R.ok();
|
return R.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 手动刷新单个账号 Token */
|
|
||||||
@PostMapping("/accounts/{id}/refresh-token")
|
@PostMapping("/accounts/{id}/refresh-token")
|
||||||
public R<?> refreshOne(@PathVariable Long id) {
|
public R<?> refreshOne(@PathVariable Long id) {
|
||||||
accountService.refreshToken(id);
|
accountService.refreshToken(id);
|
||||||
return R.ok();
|
return R.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 手动刷新全部启用账号 Token */
|
|
||||||
@PostMapping("/refresh-all")
|
@PostMapping("/refresh-all")
|
||||||
public R<?> refreshAll() {
|
public R<?> refreshAll() {
|
||||||
accountService.refreshAllTokens();
|
accountService.listSimple().forEach(account -> accountService.refreshToken(account.getId()));
|
||||||
return R.ok();
|
return R.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ public class BanmaAccount extends BaseEntity {
|
|||||||
private String username;
|
private String username;
|
||||||
/** 登录密码(服务端刷新Token用,不对外返回) */
|
/** 登录密码(服务端刷新Token用,不对外返回) */
|
||||||
private String password;
|
private String password;
|
||||||
|
/** 客户端账号用户名(关联client_account.username) */
|
||||||
|
private String clientUsername;
|
||||||
/** 访问 Token(客户端刷新后回写) */
|
/** 访问 Token(客户端刷新后回写) */
|
||||||
private String token;
|
private String token;
|
||||||
/** Token 过期时间(可选) */
|
/** Token 过期时间(可选) */
|
||||||
@@ -37,6 +39,8 @@ public class BanmaAccount extends BaseEntity {
|
|||||||
public void setUsername(String username) { this.username = username; }
|
public void setUsername(String username) { this.username = username; }
|
||||||
public String getPassword() { return password; }
|
public String getPassword() { return password; }
|
||||||
public void setPassword(String password) { this.password = password; }
|
public void setPassword(String password) { this.password = password; }
|
||||||
|
public String getClientUsername() { return clientUsername; }
|
||||||
|
public void setClientUsername(String clientUsername) { this.clientUsername = clientUsername; }
|
||||||
public String getToken() { return token; }
|
public String getToken() { return token; }
|
||||||
public void setToken(String token) { this.token = token; }
|
public void setToken(String token) { this.token = token; }
|
||||||
public Date getTokenExpireAt() { return tokenExpireAt; }
|
public Date getTokenExpireAt() { return tokenExpireAt; }
|
||||||
|
|||||||
@@ -51,6 +51,10 @@ public class ClientAccount extends BaseEntity
|
|||||||
@Excel(name = "设备数量限制")
|
@Excel(name = "设备数量限制")
|
||||||
private Integer deviceLimit;
|
private Integer deviceLimit;
|
||||||
|
|
||||||
|
/** 账号类型 */
|
||||||
|
@Excel(name = "账号类型")
|
||||||
|
private String accountType; // trial试用, paid付费
|
||||||
|
|
||||||
public void setId(Long id)
|
public void setId(Long id)
|
||||||
{
|
{
|
||||||
this.id = id;
|
this.id = id;
|
||||||
@@ -147,4 +151,14 @@ public class ClientAccount extends BaseEntity
|
|||||||
{
|
{
|
||||||
return deviceLimit;
|
return deviceLimit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setAccountType(String accountType)
|
||||||
|
{
|
||||||
|
this.accountType = accountType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAccountType()
|
||||||
|
{
|
||||||
|
return accountType;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -30,6 +30,9 @@ public class ClientDevice extends BaseEntity {
|
|||||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
@Excel(name = "最近在线", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
|
@Excel(name = "最近在线", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
|
||||||
private Date lastActiveAt;
|
private Date lastActiveAt;
|
||||||
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
@Excel(name = "试用期过期时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
private Date trialExpireTime;
|
||||||
|
|
||||||
public Long getId() { return id; }
|
public Long getId() { return id; }
|
||||||
public void setId(Long id) { this.id = id; }
|
public void setId(Long id) { this.id = id; }
|
||||||
@@ -49,6 +52,8 @@ public class ClientDevice extends BaseEntity {
|
|||||||
public void setLocation(String location) { this.location = location; }
|
public void setLocation(String location) { this.location = location; }
|
||||||
public Date getLastActiveAt() { return lastActiveAt; }
|
public Date getLastActiveAt() { return lastActiveAt; }
|
||||||
public void setLastActiveAt(Date lastActiveAt) { this.lastActiveAt = lastActiveAt; }
|
public void setLastActiveAt(Date lastActiveAt) { this.lastActiveAt = lastActiveAt; }
|
||||||
|
public Date getTrialExpireTime() { return trialExpireTime; }
|
||||||
|
public void setTrialExpireTime(Date trialExpireTime) { this.trialExpireTime = trialExpireTime; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.ruoyi.system.mapper;
|
package com.ruoyi.system.mapper;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
import com.ruoyi.system.domain.BanmaAccount;
|
import com.ruoyi.system.domain.BanmaAccount;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -12,7 +13,7 @@ public interface BanmaAccountMapper {
|
|||||||
int insert(BanmaAccount entity);
|
int insert(BanmaAccount entity);
|
||||||
int update(BanmaAccount entity);
|
int update(BanmaAccount entity);
|
||||||
int deleteById(Long id);
|
int deleteById(Long id);
|
||||||
int clearDefault();
|
int clearDefault(@Param("clientUsername") String clientUsername);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,20 @@
|
|||||||
package com.ruoyi.system.mapper;
|
package com.ruoyi.system.mapper;
|
||||||
|
|
||||||
import com.ruoyi.system.domain.ClientDevice;
|
import com.ruoyi.system.domain.ClientDevice;
|
||||||
import io.lettuce.core.dynamic.annotation.Param;
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public interface ClientDeviceMapper {
|
public interface ClientDeviceMapper {
|
||||||
ClientDevice selectByDeviceId(String deviceId);
|
ClientDevice selectByDeviceId(@Param("deviceId") String deviceId);
|
||||||
|
ClientDevice selectByDeviceIdAndUsername(@Param("deviceId") String deviceId, @Param("username") String username);
|
||||||
List<ClientDevice> selectByUsername(@Param("username") String username);
|
List<ClientDevice> selectByUsername(@Param("username") String username);
|
||||||
List<ClientDevice> selectOnlineDevices();
|
List<ClientDevice> selectOnlineDevices();
|
||||||
int insert(ClientDevice device);
|
int insert(ClientDevice device);
|
||||||
int updateByDeviceId(ClientDevice device);
|
int updateByDeviceId(ClientDevice device);
|
||||||
int deleteByDeviceId(String deviceId);
|
int updateByDeviceIdAndUsername(ClientDevice device);
|
||||||
int countByUsername(String username);
|
int deleteByDeviceId(@Param("deviceId") String deviceId);
|
||||||
|
int countByUsername(@Param("username") String username);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
package com.ruoyi.system.service;
|
package com.ruoyi.system.service;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import com.ruoyi.system.domain.BanmaAccount;
|
import com.ruoyi.system.domain.BanmaAccount;
|
||||||
|
|
||||||
@@ -8,10 +7,13 @@ import com.ruoyi.system.domain.BanmaAccount;
|
|||||||
*/
|
*/
|
||||||
public interface IBanmaAccountService {
|
public interface IBanmaAccountService {
|
||||||
List<BanmaAccount> listSimple();
|
List<BanmaAccount> listSimple();
|
||||||
|
List<BanmaAccount> listSimple(String clientUsername);
|
||||||
Long saveOrUpdate(BanmaAccount entity);
|
Long saveOrUpdate(BanmaAccount entity);
|
||||||
|
Long saveOrUpdate(BanmaAccount entity, String clientUsername);
|
||||||
void remove(Long id);
|
void remove(Long id);
|
||||||
boolean refreshToken(Long id);
|
boolean refreshToken(Long id);
|
||||||
void refreshAllTokens();
|
void refreshAllTokens();
|
||||||
|
String validateAndGetToken(String username, String password);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,14 @@ public class BanmaAccountServiceImpl implements IBanmaAccountService {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<BanmaAccount> listSimple() {
|
public List<BanmaAccount> listSimple() {
|
||||||
List<BanmaAccount> list = mapper.selectList(new BanmaAccount());
|
return listSimple(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<BanmaAccount> listSimple(String clientUsername) {
|
||||||
|
BanmaAccount query = new BanmaAccount();
|
||||||
|
query.setClientUsername(clientUsername);
|
||||||
|
List<BanmaAccount> list = mapper.selectList(query);
|
||||||
// 隐藏密码
|
// 隐藏密码
|
||||||
for (BanmaAccount a : list) { a.setPassword(null); }
|
for (BanmaAccount a : list) { a.setPassword(null); }
|
||||||
return list;
|
return list;
|
||||||
@@ -34,13 +41,25 @@ public class BanmaAccountServiceImpl implements IBanmaAccountService {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Long saveOrUpdate(BanmaAccount entity) {
|
public Long saveOrUpdate(BanmaAccount entity) {
|
||||||
|
return saveOrUpdate(entity, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Long saveOrUpdate(BanmaAccount entity, String clientUsername) {
|
||||||
|
// 设置客户端用户名
|
||||||
|
if (clientUsername != null) {
|
||||||
|
entity.setClientUsername(clientUsername);
|
||||||
|
}
|
||||||
|
|
||||||
if (entity.getId() == null) {
|
if (entity.getId() == null) {
|
||||||
mapper.insert(entity);
|
mapper.insert(entity);
|
||||||
} else {
|
} else {
|
||||||
mapper.update(entity);
|
mapper.update(entity);
|
||||||
}
|
}
|
||||||
if (Objects.equals(entity.getIsDefault(), 1)) {
|
|
||||||
mapper.clearDefault();
|
// 如果设为默认,则清除该客户端的其他默认账号
|
||||||
|
if (Objects.equals(entity.getIsDefault(), 1) && entity.getClientUsername() != null) {
|
||||||
|
mapper.clearDefault(entity.getClientUsername());
|
||||||
BanmaAccount only = new BanmaAccount();
|
BanmaAccount only = new BanmaAccount();
|
||||||
only.setId(entity.getId());
|
only.setId(entity.getId());
|
||||||
only.setIsDefault(1);
|
only.setIsDefault(1);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
<result property="name" column="name"/>
|
<result property="name" column="name"/>
|
||||||
<result property="username" column="username"/>
|
<result property="username" column="username"/>
|
||||||
<result property="password" column="password"/>
|
<result property="password" column="password"/>
|
||||||
|
<result property="clientUsername" column="client_username"/>
|
||||||
<result property="token" column="token"/>
|
<result property="token" column="token"/>
|
||||||
<result property="tokenExpireAt" column="token_expire_at"/>
|
<result property="tokenExpireAt" column="token_expire_at"/>
|
||||||
<result property="isDefault" column="is_default"/>
|
<result property="isDefault" column="is_default"/>
|
||||||
@@ -19,7 +20,7 @@
|
|||||||
</resultMap>
|
</resultMap>
|
||||||
|
|
||||||
<sql id="Base_Column_List">
|
<sql id="Base_Column_List">
|
||||||
id, name, username, password, token, token_expire_at, is_default, status, remark, create_by, create_time, update_by, update_time
|
id, name, username, password, client_username, token, token_expire_at, is_default, status, remark, create_by, create_time, update_by, update_time
|
||||||
</sql>
|
</sql>
|
||||||
|
|
||||||
<select id="selectById" parameterType="long" resultMap="BanmaAccountResult">
|
<select id="selectById" parameterType="long" resultMap="BanmaAccountResult">
|
||||||
@@ -29,6 +30,7 @@
|
|||||||
<select id="selectList" parameterType="com.ruoyi.system.domain.BanmaAccount" resultMap="BanmaAccountResult">
|
<select id="selectList" parameterType="com.ruoyi.system.domain.BanmaAccount" resultMap="BanmaAccountResult">
|
||||||
select <include refid="Base_Column_List"/> from banma_account
|
select <include refid="Base_Column_List"/> from banma_account
|
||||||
<where>
|
<where>
|
||||||
|
<if test="clientUsername != null and clientUsername != ''"> and client_username = #{clientUsername}</if>
|
||||||
<if test="name != null and name != ''"> and name like concat('%', #{name}, '%')</if>
|
<if test="name != null and name != ''"> and name like concat('%', #{name}, '%')</if>
|
||||||
<if test="username != null and username != ''"> and username like concat('%', #{username}, '%')</if>
|
<if test="username != null and username != ''"> and username like concat('%', #{username}, '%')</if>
|
||||||
<if test="status != null"> and status = #{status}</if>
|
<if test="status != null"> and status = #{status}</if>
|
||||||
@@ -42,6 +44,7 @@
|
|||||||
<if test="name != null">name,</if>
|
<if test="name != null">name,</if>
|
||||||
<if test="username != null">username,</if>
|
<if test="username != null">username,</if>
|
||||||
<if test="password != null">password,</if>
|
<if test="password != null">password,</if>
|
||||||
|
<if test="clientUsername != null">client_username,</if>
|
||||||
<if test="token != null">token,</if>
|
<if test="token != null">token,</if>
|
||||||
<if test="tokenExpireAt != null">token_expire_at,</if>
|
<if test="tokenExpireAt != null">token_expire_at,</if>
|
||||||
<if test="isDefault != null">is_default,</if>
|
<if test="isDefault != null">is_default,</if>
|
||||||
@@ -54,6 +57,7 @@
|
|||||||
<if test="name != null">#{name},</if>
|
<if test="name != null">#{name},</if>
|
||||||
<if test="username != null">#{username},</if>
|
<if test="username != null">#{username},</if>
|
||||||
<if test="password != null">#{password},</if>
|
<if test="password != null">#{password},</if>
|
||||||
|
<if test="clientUsername != null">#{clientUsername},</if>
|
||||||
<if test="token != null">#{token},</if>
|
<if test="token != null">#{token},</if>
|
||||||
<if test="tokenExpireAt != null">#{tokenExpireAt},</if>
|
<if test="tokenExpireAt != null">#{tokenExpireAt},</if>
|
||||||
<if test="isDefault != null">#{isDefault},</if>
|
<if test="isDefault != null">#{isDefault},</if>
|
||||||
@@ -70,6 +74,7 @@
|
|||||||
<if test="name != null">name=#{name},</if>
|
<if test="name != null">name=#{name},</if>
|
||||||
<if test="username != null">username=#{username},</if>
|
<if test="username != null">username=#{username},</if>
|
||||||
<if test="password != null">password=#{password},</if>
|
<if test="password != null">password=#{password},</if>
|
||||||
|
<if test="clientUsername != null">client_username=#{clientUsername},</if>
|
||||||
<if test="token != null">token=#{token},</if>
|
<if test="token != null">token=#{token},</if>
|
||||||
<if test="tokenExpireAt != null">token_expire_at=#{tokenExpireAt},</if>
|
<if test="tokenExpireAt != null">token_expire_at=#{tokenExpireAt},</if>
|
||||||
<if test="isDefault != null">is_default=#{isDefault},</if>
|
<if test="isDefault != null">is_default=#{isDefault},</if>
|
||||||
@@ -83,6 +88,9 @@
|
|||||||
|
|
||||||
<update id="clearDefault">
|
<update id="clearDefault">
|
||||||
update banma_account set is_default = 0 where is_default = 1
|
update banma_account set is_default = 0 where is_default = 1
|
||||||
|
<if test="clientUsername != null and clientUsername != ''">
|
||||||
|
and client_username = #{clientUsername}
|
||||||
|
</if>
|
||||||
</update>
|
</update>
|
||||||
|
|
||||||
<delete id="deleteById" parameterType="long">
|
<delete id="deleteById" parameterType="long">
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
<result property="remark" column="remark" />
|
<result property="remark" column="remark" />
|
||||||
<result property="permissions" column="permissions" />
|
<result property="permissions" column="permissions" />
|
||||||
<result property="deviceLimit" column="device_limit" />
|
<result property="deviceLimit" column="device_limit" />
|
||||||
|
<result property="accountType" column="account_type" />
|
||||||
<result property="createBy" column="create_by" />
|
<result property="createBy" column="create_by" />
|
||||||
<result property="createTime" column="create_time" />
|
<result property="createTime" column="create_time" />
|
||||||
<result property="updateBy" column="update_by" />
|
<result property="updateBy" column="update_by" />
|
||||||
@@ -23,7 +24,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
|
|
||||||
<sql id="selectClientAccountVo">
|
<sql id="selectClientAccountVo">
|
||||||
select id, account_name, username, password, status, expire_time,
|
select id, account_name, username, password, status, expire_time,
|
||||||
allowed_ip_range, remark, permissions, device_limit, create_by, create_time, update_by, update_time
|
allowed_ip_range, remark, permissions, device_limit, account_type, create_by, create_time, update_by, update_time
|
||||||
from client_account
|
from client_account
|
||||||
</sql>
|
</sql>
|
||||||
|
|
||||||
@@ -59,6 +60,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
<if test="remark != null">remark,</if>
|
<if test="remark != null">remark,</if>
|
||||||
<if test="permissions != null">permissions,</if>
|
<if test="permissions != null">permissions,</if>
|
||||||
<if test="deviceLimit != null">device_limit,</if>
|
<if test="deviceLimit != null">device_limit,</if>
|
||||||
|
<if test="accountType != null">account_type,</if>
|
||||||
<if test="createBy != null">create_by,</if>
|
<if test="createBy != null">create_by,</if>
|
||||||
create_time
|
create_time
|
||||||
</trim>
|
</trim>
|
||||||
@@ -72,6 +74,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
<if test="remark != null">#{remark},</if>
|
<if test="remark != null">#{remark},</if>
|
||||||
<if test="permissions != null">#{permissions},</if>
|
<if test="permissions != null">#{permissions},</if>
|
||||||
<if test="deviceLimit != null">#{deviceLimit},</if>
|
<if test="deviceLimit != null">#{deviceLimit},</if>
|
||||||
|
<if test="accountType != null">#{accountType},</if>
|
||||||
<if test="createBy != null">#{createBy},</if>
|
<if test="createBy != null">#{createBy},</if>
|
||||||
sysdate()
|
sysdate()
|
||||||
</trim>
|
</trim>
|
||||||
@@ -89,6 +92,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
<if test="remark != null">remark = #{remark},</if>
|
<if test="remark != null">remark = #{remark},</if>
|
||||||
<if test="permissions != null">permissions = #{permissions},</if>
|
<if test="permissions != null">permissions = #{permissions},</if>
|
||||||
<if test="deviceLimit != null">device_limit = #{deviceLimit},</if>
|
<if test="deviceLimit != null">device_limit = #{deviceLimit},</if>
|
||||||
|
<if test="accountType != null">account_type = #{accountType},</if>
|
||||||
<if test="updateBy != null">update_by = #{updateBy},</if>
|
<if test="updateBy != null">update_by = #{updateBy},</if>
|
||||||
update_time = sysdate()
|
update_time = sysdate()
|
||||||
</trim>
|
</trim>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
<result property="ip" column="ip"/>
|
<result property="ip" column="ip"/>
|
||||||
<result property="location" column="location"/>
|
<result property="location" column="location"/>
|
||||||
<result property="lastActiveAt" column="last_active_at"/>
|
<result property="lastActiveAt" column="last_active_at"/>
|
||||||
|
<result property="trialExpireTime" column="trial_expire_time"/>
|
||||||
<result property="createTime" column="create_time"/>
|
<result property="createTime" column="create_time"/>
|
||||||
<result property="updateTime" column="update_time"/>
|
<result property="updateTime" column="update_time"/>
|
||||||
</resultMap>
|
</resultMap>
|
||||||
@@ -19,13 +20,17 @@
|
|||||||
select * from client_device where device_id = #{deviceId}
|
select * from client_device where device_id = #{deviceId}
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<select id="selectByDeviceIdAndUsername" resultMap="ClientDeviceMap">
|
||||||
|
select * from client_device where device_id = #{deviceId} and username = #{username}
|
||||||
|
</select>
|
||||||
|
|
||||||
<select id="selectByUsername" resultMap="ClientDeviceMap">
|
<select id="selectByUsername" resultMap="ClientDeviceMap">
|
||||||
select * from client_device where username = #{username} and status != 'removed' order by update_time desc
|
select * from client_device where username = #{username} and status != 'removed' order by update_time desc
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<insert id="insert" parameterType="com.ruoyi.system.domain.ClientDevice" useGeneratedKeys="true" keyProperty="id">
|
<insert id="insert" parameterType="com.ruoyi.system.domain.ClientDevice" useGeneratedKeys="true" keyProperty="id">
|
||||||
insert into client_device(username, device_id, name, os, status, ip, location, last_active_at, create_time, update_time)
|
insert into client_device(username, device_id, name, os, status, ip, location, last_active_at, trial_expire_time, create_time, update_time)
|
||||||
values(#{username}, #{deviceId}, #{name}, #{os}, #{status}, #{ip}, #{location}, #{lastActiveAt}, now(), now())
|
values(#{username}, #{deviceId}, #{name}, #{os}, #{status}, #{ip}, #{location}, #{lastActiveAt}, #{trialExpireTime}, now(), now())
|
||||||
</insert>
|
</insert>
|
||||||
|
|
||||||
<update id="updateByDeviceId" parameterType="com.ruoyi.system.domain.ClientDevice">
|
<update id="updateByDeviceId" parameterType="com.ruoyi.system.domain.ClientDevice">
|
||||||
@@ -37,10 +42,26 @@
|
|||||||
ip = #{ip},
|
ip = #{ip},
|
||||||
location = #{location},
|
location = #{location},
|
||||||
last_active_at = #{lastActiveAt},
|
last_active_at = #{lastActiveAt},
|
||||||
|
trial_expire_time = #{trialExpireTime},
|
||||||
update_time = now()
|
update_time = now()
|
||||||
where device_id = #{deviceId}
|
where device_id = #{deviceId}
|
||||||
</update>
|
</update>
|
||||||
|
|
||||||
|
<update id="updateByDeviceIdAndUsername" parameterType="com.ruoyi.system.domain.ClientDevice">
|
||||||
|
update client_device
|
||||||
|
<set>
|
||||||
|
<if test="name != null">name = #{name},</if>
|
||||||
|
<if test="os != null">os = #{os},</if>
|
||||||
|
<if test="status != null">status = #{status},</if>
|
||||||
|
<if test="ip != null">ip = #{ip},</if>
|
||||||
|
<if test="location != null">location = #{location},</if>
|
||||||
|
<if test="lastActiveAt != null">last_active_at = #{lastActiveAt},</if>
|
||||||
|
<if test="trialExpireTime != null">trial_expire_time = #{trialExpireTime},</if>
|
||||||
|
update_time = now()
|
||||||
|
</set>
|
||||||
|
where device_id = #{deviceId} and username = #{username}
|
||||||
|
</update>
|
||||||
|
|
||||||
<delete id="deleteByDeviceId">
|
<delete id="deleteByDeviceId">
|
||||||
delete from client_device where device_id = #{deviceId}
|
delete from client_device where device_id = #{deviceId}
|
||||||
</delete>
|
</delete>
|
||||||
|
|||||||
@@ -69,3 +69,21 @@ export function renewAccount(data) {
|
|||||||
data: data
|
data: data
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取账号设备列表
|
||||||
|
export function getDeviceList(username) {
|
||||||
|
return request({
|
||||||
|
url: '/monitor/device/list',
|
||||||
|
method: 'get',
|
||||||
|
params: { username }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修改设备试用期过期时间
|
||||||
|
export function updateDeviceExpire(data) {
|
||||||
|
return request({
|
||||||
|
url: '/monitor/device/updateExpire',
|
||||||
|
method: 'post',
|
||||||
|
data: data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -82,19 +82,25 @@
|
|||||||
<el-table-column label="ID" align="center" prop="id" width="80" />
|
<el-table-column label="ID" align="center" prop="id" width="80" />
|
||||||
<el-table-column label="账号名称" align="center" prop="accountName" :show-overflow-tooltip="true" />
|
<el-table-column label="账号名称" align="center" prop="accountName" :show-overflow-tooltip="true" />
|
||||||
<el-table-column label="用户名" align="center" prop="username" :show-overflow-tooltip="true" />
|
<el-table-column label="用户名" align="center" prop="username" :show-overflow-tooltip="true" />
|
||||||
<el-table-column label="状态" align="center" prop="status">
|
<el-table-column label="状态" align="center" prop="status" width="80">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<dict-tag :options="statusOptions" :value="scope.row.status" />
|
<dict-tag :options="statusOptions" :value="scope.row.status" />
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="过期时间" align="center" prop="expireTime" width="180">
|
<el-table-column label="账号类型" align="center" prop="accountType" width="90">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-tag :type="scope.row.accountType === 'paid' ? 'success' : 'warning'" size="small">
|
||||||
|
{{ scope.row.accountType === 'paid' ? '付费' : '试用' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="过期时间" align="center" prop="expireTime" width="130">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<el-tag
|
<el-tag
|
||||||
:type="getRemainingDays(scope.row).type"
|
:type="getRemainingDays(scope.row.expireTime).type"
|
||||||
size="small"
|
size="small"
|
||||||
style="margin-top: 5px;"
|
|
||||||
>
|
>
|
||||||
{{ getRemainingDays(scope.row).text }}
|
{{ getRemainingDays(scope.row.expireTime).text }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
@@ -104,7 +110,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="创建时间" align="center" prop="createTime" width="180" />
|
<el-table-column label="创建时间" align="center" prop="createTime" width="180" />
|
||||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="150">
|
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="200">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<el-button
|
<el-button
|
||||||
size="mini"
|
size="mini"
|
||||||
@@ -113,6 +119,12 @@
|
|||||||
@click="handleUpdate(scope.row)"
|
@click="handleUpdate(scope.row)"
|
||||||
v-hasPermi="['monitor:account:edit']"
|
v-hasPermi="['monitor:account:edit']"
|
||||||
>修改</el-button>
|
>修改</el-button>
|
||||||
|
<el-button
|
||||||
|
size="mini"
|
||||||
|
type="text"
|
||||||
|
icon="el-icon-mobile-phone"
|
||||||
|
@click="viewDevices(scope.row)"
|
||||||
|
>设备</el-button>
|
||||||
<el-button
|
<el-button
|
||||||
size="mini"
|
size="mini"
|
||||||
type="text"
|
type="text"
|
||||||
@@ -206,6 +218,67 @@
|
|||||||
</div>
|
</div>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 设备列表对话框 -->
|
||||||
|
<el-dialog :title="'账号【' + currentAccountName + '】的设备列表'" :visible.sync="deviceListVisible" width="800px" append-to-body>
|
||||||
|
<el-table :data="deviceList" v-loading="deviceListLoading">
|
||||||
|
<el-table-column label="设备名称" prop="name" :show-overflow-tooltip="true" />
|
||||||
|
<el-table-column label="操作系统" prop="os" width="120" />
|
||||||
|
<el-table-column label="状态" prop="status" width="80" align="center">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-tag :type="scope.row.status === 'online' ? 'success' : 'info'" size="small">
|
||||||
|
{{ scope.row.status === 'online' ? '在线' : '离线' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="设备试用期" prop="trialExpireTime" width="130" align="center">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-tag
|
||||||
|
:type="getRemainingDays(scope.row.trialExpireTime).type"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
{{ getRemainingDays(scope.row.trialExpireTime).text }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="最近在线" prop="lastActiveAt" width="160" />
|
||||||
|
<el-table-column label="操作" align="center" width="100">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-button
|
||||||
|
size="mini"
|
||||||
|
type="text"
|
||||||
|
icon="el-icon-edit"
|
||||||
|
@click="editDeviceExpire(scope.row)"
|
||||||
|
>修改</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<div slot="footer" class="dialog-footer">
|
||||||
|
<el-button @click="deviceListVisible = false">关 闭</el-button>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 修改设备过期时间对话框 -->
|
||||||
|
<el-dialog title="修改设备试用期" :visible.sync="deviceExpireDialogVisible" width="400px" append-to-body>
|
||||||
|
<el-form label-width="120px">
|
||||||
|
<el-form-item label="设备名称">
|
||||||
|
<el-input v-model="currentDevice.name" disabled />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="试用期过期时间">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="currentDevice.trialExpireTime"
|
||||||
|
type="datetime"
|
||||||
|
placeholder="选择过期时间"
|
||||||
|
value-format="yyyy-MM-dd HH:mm:ss"
|
||||||
|
style="width: 100%;"
|
||||||
|
></el-date-picker>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<div slot="footer" class="dialog-footer">
|
||||||
|
<el-button type="primary" @click="submitDeviceExpire">确 定</el-button>
|
||||||
|
<el-button @click="deviceExpireDialogVisible = false">取 消</el-button>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
<!-- 注册对话框 -->
|
<!-- 注册对话框 -->
|
||||||
<el-dialog title="客户端账号注册" :visible.sync="registerOpen" width="400px" append-to-body>
|
<el-dialog title="客户端账号注册" :visible.sync="registerOpen" width="400px" append-to-body>
|
||||||
<el-form ref="registerForm" :model="registerForm" :rules="registerRules" label-width="80px">
|
<el-form ref="registerForm" :model="registerForm" :rules="registerRules" label-width="80px">
|
||||||
@@ -231,7 +304,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { listAccount, getAccount, delAccount, addAccount, updateAccount, registerAccount, checkUsername, renewAccount } from "@/api/monitor/account";
|
import { listAccount, getAccount, delAccount, addAccount, updateAccount, registerAccount, checkUsername, renewAccount, getDeviceList, updateDeviceExpire } from "@/api/monitor/account";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "Account",
|
name: "Account",
|
||||||
@@ -262,6 +335,14 @@ export default {
|
|||||||
registerLoading: false,
|
registerLoading: false,
|
||||||
usernameChecking: false,
|
usernameChecking: false,
|
||||||
usernameAvailable: null,
|
usernameAvailable: null,
|
||||||
|
// 设备列表
|
||||||
|
deviceListVisible: false,
|
||||||
|
deviceListLoading: false,
|
||||||
|
deviceList: [],
|
||||||
|
currentAccountName: '',
|
||||||
|
// 设备过期时间编辑
|
||||||
|
deviceExpireDialogVisible: false,
|
||||||
|
currentDevice: {},
|
||||||
// 状态数据字典
|
// 状态数据字典
|
||||||
statusOptions: [
|
statusOptions: [
|
||||||
{ dictLabel: "正常", dictValue: "0" },
|
{ dictLabel: "正常", dictValue: "0" },
|
||||||
@@ -325,11 +406,6 @@ export default {
|
|||||||
this.loading = false;
|
this.loading = false;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
// 检查是否过期
|
|
||||||
isExpired(row) {
|
|
||||||
if (!row.expireTime) return false;
|
|
||||||
return new Date(row.expireTime) < new Date();
|
|
||||||
},
|
|
||||||
// 表单重置
|
// 表单重置
|
||||||
reset() {
|
reset() {
|
||||||
this.form = {
|
this.form = {
|
||||||
@@ -338,6 +414,7 @@ export default {
|
|||||||
username: null,
|
username: null,
|
||||||
password: null,
|
password: null,
|
||||||
status: "0",
|
status: "0",
|
||||||
|
accountType: "trial",
|
||||||
expireTime: null,
|
expireTime: null,
|
||||||
renewDays: null,
|
renewDays: null,
|
||||||
deviceLimit: 3,
|
deviceLimit: 3,
|
||||||
@@ -518,6 +595,43 @@ export default {
|
|||||||
this.registerOpen = false;
|
this.registerOpen = false;
|
||||||
this.resetRegisterForm();
|
this.resetRegisterForm();
|
||||||
},
|
},
|
||||||
|
/** 查看设备列表 */
|
||||||
|
async viewDevices(row) {
|
||||||
|
this.currentAccountName = row.accountName || row.username;
|
||||||
|
this.deviceListVisible = true;
|
||||||
|
this.deviceListLoading = true;
|
||||||
|
try {
|
||||||
|
const response = await getDeviceList(row.username);
|
||||||
|
this.deviceList = response.data || [];
|
||||||
|
} catch (error) {
|
||||||
|
this.$modal.msgError('获取设备列表失败');
|
||||||
|
this.deviceList = [];
|
||||||
|
} finally {
|
||||||
|
this.deviceListLoading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/** 编辑设备过期时间 */
|
||||||
|
editDeviceExpire(row) {
|
||||||
|
this.currentDevice = { ...row };
|
||||||
|
this.deviceExpireDialogVisible = true;
|
||||||
|
},
|
||||||
|
/** 提交设备过期时间修改 */
|
||||||
|
async submitDeviceExpire() {
|
||||||
|
try {
|
||||||
|
await updateDeviceExpire({
|
||||||
|
deviceId: this.currentDevice.deviceId,
|
||||||
|
username: this.currentDevice.username,
|
||||||
|
trialExpireTime: this.currentDevice.trialExpireTime
|
||||||
|
});
|
||||||
|
this.$modal.msgSuccess('修改成功');
|
||||||
|
this.deviceExpireDialogVisible = false;
|
||||||
|
// 刷新设备列表
|
||||||
|
const response = await getDeviceList(this.currentDevice.username);
|
||||||
|
this.deviceList = response.data || [];
|
||||||
|
} catch (error) {
|
||||||
|
this.$modal.msgError('修改失败: ' + (error.message || '未知错误'));
|
||||||
|
}
|
||||||
|
},
|
||||||
/** 计算续费后的新到期时间 */
|
/** 计算续费后的新到期时间 */
|
||||||
calculateNewExpireTime() {
|
calculateNewExpireTime() {
|
||||||
if (!this.form.renewDays) return '';
|
if (!this.form.renewDays) return '';
|
||||||
@@ -542,13 +656,13 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
/** 获取剩余天数 */
|
/** 获取剩余天数 */
|
||||||
getRemainingDays(row) {
|
getRemainingDays(expireTime) {
|
||||||
if (!row.expireTime) {
|
if (!expireTime) {
|
||||||
return { text: '已过期', type: 'danger' };
|
return { text: '未设置', type: 'info' };
|
||||||
}
|
}
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const expireDate = new Date(row.expireTime);
|
const expireDate = new Date(expireTime);
|
||||||
const diffTime = expireDate - now;
|
const diffTime = expireDate - now;
|
||||||
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user