feat(electron):优化跟卖精灵启动流程和UI细节
- 移除启动时的固定延时,改为即时反馈启动状态- 更新跟卖精灵描述文案,移除初始化时间说明 - 统一spinner样式类名,优化加载动画显示逻辑 - 调整菜单激活背景色,增强视觉层次感- 引入平台图标图片替代文字标识- 修改VIP状态栏背景及文字颜色,提升可读性 - 配置ChromeDriver国内镜像源并实现后台预加载 - 添加WebDriverManager依赖以自动管理浏览器驱动 -优化electron-builder资源打包配置
@@ -7,7 +7,9 @@
|
||||
"compression": "maximum",
|
||||
"asarUnpack": [
|
||||
"public/jre/**/*",
|
||||
"public/icon/**/*",
|
||||
"public/icon/icon.png",
|
||||
"public/icon/image.png",
|
||||
"public/icon/img.png",
|
||||
"public/image/**/*",
|
||||
"public/splash.html",
|
||||
"public/config/**/*"
|
||||
@@ -48,7 +50,9 @@
|
||||
"to": "public",
|
||||
"filter": [
|
||||
"jre/**/*",
|
||||
"icon/**/*",
|
||||
"icon/icon.png",
|
||||
"icon/image.png",
|
||||
"icon/img.png",
|
||||
"image/**/*",
|
||||
"splash.html",
|
||||
"config/**/*",
|
||||
|
||||
BIN
electron-vue-template/public/icon/amazon.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
electron-vue-template/public/icon/rakuten.png
Normal file
|
After Width: | Height: | Size: 804 B |
BIN
electron-vue-template/public/icon/zebra.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
@@ -217,12 +217,12 @@ function startSpringBoot() {
|
||||
openAppIfNotOpened();
|
||||
}
|
||||
}, 15000);
|
||||
|
||||
} catch (error) {
|
||||
dialog.showErrorBox('启动异常', `无法启动应用: ${error}`);
|
||||
app.quit();
|
||||
}
|
||||
}
|
||||
|
||||
startSpringBoot();
|
||||
function stopSpringBoot() {
|
||||
if (!springProcess) return;
|
||||
@@ -362,7 +362,7 @@ app.whenReady().then(() => {
|
||||
|
||||
// setTimeout(() => {
|
||||
// openAppIfNotOpened();
|
||||
// }, 2000);
|
||||
// }, 100);
|
||||
|
||||
app.on('activate', () => {
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
|
||||
@@ -19,6 +19,10 @@ import UpdateDialog from './components/common/UpdateDialog.vue'
|
||||
import SettingsDialog from './components/common/SettingsDialog.vue'
|
||||
import TrialExpiredDialog from './components/common/TrialExpiredDialog.vue'
|
||||
import AccountManager from './components/common/AccountManager.vue'
|
||||
// 导入平台图标
|
||||
import rakutenIcon from '/icon/rakuten.png'
|
||||
import amazonIcon from '/icon/amazon.png'
|
||||
import zebraIcon from '/icon/zebra.png'
|
||||
|
||||
const dashboardsMap: Record<string, Component> = {
|
||||
rakuten: RakutenDashboard,
|
||||
@@ -116,9 +120,9 @@ const currentVersion = ref('')
|
||||
|
||||
// 菜单配置 - 复刻ERP客户端格式
|
||||
const menuConfig = [
|
||||
{key: 'rakuten', name: 'Rakuten', index: 'rakuten', icon: 'R'},
|
||||
{key: 'amazon', name: 'Amazon', index: 'amazon', icon: 'A'},
|
||||
{key: 'zebra', name: 'Zebra', index: 'zebra', icon: 'Z'},
|
||||
{key: 'rakuten', name: 'Rakuten', index: 'rakuten', icon: 'R', iconImage: rakutenIcon},
|
||||
{key: 'amazon', name: 'Amazon', index: 'amazon', icon: 'A', iconImage: amazonIcon},
|
||||
{key: 'zebra', name: 'Zebra', index: 'zebra', icon: 'Z', iconImage: zebraIcon},
|
||||
{key: 'shopee', name: 'Shopee', index: 'shopee', icon: 'S'},
|
||||
]
|
||||
|
||||
@@ -589,9 +593,13 @@ onUnmounted(() => {
|
||||
:class="{ active: activeMenu === item.key }"
|
||||
@click="handleMenuSelect(item.key)"
|
||||
>
|
||||
<span class="menu-text"><span class="menu-icon" :data-k="item.key">{{ item.icon }}</span>{{
|
||||
item.name
|
||||
}}</span>
|
||||
<span class="menu-text">
|
||||
<span class="menu-icon" :data-k="item.key">
|
||||
<img v-if="item.iconImage" :src="item.iconImage" :alt="item.name" class="menu-icon-img" />
|
||||
<template v-else>{{ item.icon }}</template>
|
||||
</span>
|
||||
{{ item.name }}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -812,7 +820,6 @@ onUnmounted(() => {
|
||||
width: 90px;
|
||||
|
||||
object-fit: contain;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.menu-group-title {
|
||||
@@ -843,8 +850,8 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
.menu-item.active {
|
||||
background: #ecf5ff !important;
|
||||
color: #409EFF !important;
|
||||
background: rgba(0, 0, 0, 0.1) !important;
|
||||
/* color: #409EFF !important; */
|
||||
}
|
||||
|
||||
.menu-text {
|
||||
@@ -868,17 +875,10 @@ onUnmounted(() => {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.menu-icon[data-k="rakuten"] {
|
||||
background: #BF0000;
|
||||
}
|
||||
|
||||
.menu-icon[data-k="amazon"] {
|
||||
background: #FF9900;
|
||||
color: #1A1A1A;
|
||||
}
|
||||
|
||||
.menu-icon[data-k="zebra"] {
|
||||
background: #34495e;
|
||||
.menu-icon-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.menu-icon[data-k="shopee"] {
|
||||
@@ -1003,7 +1003,7 @@ onUnmounted(() => {
|
||||
justify-content: space-between;
|
||||
padding: 10px 12px;
|
||||
box-sizing: border-box;
|
||||
background: linear-gradient(90deg, #FFF7CC 0%, #FFEB80 100%);
|
||||
background: #BAE0FF;
|
||||
box-shadow: 0 2px 8px rgba(255, 215, 0, 0.15);
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
@@ -1024,7 +1024,7 @@ onUnmounted(() => {
|
||||
.vip-status-card.vip-active,
|
||||
.vip-status-card.vip-normal,
|
||||
.vip-status-card.vip-warning {
|
||||
background: linear-gradient(90deg, #FFF7CC 0%, #FFEB80 100%);
|
||||
background: #BAE0FF;
|
||||
box-shadow: 0 2px 8px rgba(255, 215, 0, 0.15);
|
||||
}
|
||||
|
||||
@@ -1046,15 +1046,16 @@ onUnmounted(() => {
|
||||
.vip-status-text {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #8B6914;
|
||||
color: #001D66;
|
||||
text-align: left;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
.vip-expire-date {
|
||||
font-size: 10px;
|
||||
color: #A67C00;
|
||||
color: #001D66;
|
||||
line-height: 1.3;
|
||||
|
||||
text-align: left;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
@@ -270,10 +270,8 @@ async function openGenmaiSpirit() {
|
||||
genmaiLoading.value = true
|
||||
try {
|
||||
await systemApi.openGenmaiSpirit(selectedGenmaiAccountId.value)
|
||||
showMessage('跟卖精灵正在启动,请稍候...', 'success')
|
||||
setTimeout(() => { genmaiLoading.value = false }, 3000)
|
||||
} catch (error: any) {
|
||||
showMessage(error.message || '启动失败', 'error')
|
||||
showMessage('跟卖精灵已打开', 'success')
|
||||
} finally {
|
||||
genmaiLoading.value = false
|
||||
}
|
||||
}
|
||||
@@ -390,7 +388,7 @@ onMounted(async () => {
|
||||
<div class="step-index">2</div>
|
||||
<div class="step-card">
|
||||
<div class="step-header"><div class="title">启动服务</div></div>
|
||||
<div class="desc">请确保设备已安装Chrome浏览器,否则服务将无法启动。打开跟卖精灵将关闭Chrome浏览器进程。首次启动需初始化浏览器驱动,可能需要1-3分钟。</div>
|
||||
<div class="desc">请确保设备已安装Chrome浏览器,否则服务将无法启动。打开跟卖精灵将关闭Chrome浏览器进程。</div>
|
||||
<div class="action-buttons column">
|
||||
<el-button
|
||||
size="small"
|
||||
@@ -399,7 +397,7 @@ onMounted(async () => {
|
||||
@click="openGenmaiSpirit"
|
||||
>
|
||||
<span v-if="!genmaiLoading">启动服务</span>
|
||||
<span v-else><span class="spinner">⟳</span> 启动中...</span>
|
||||
<span v-else><span class="inline-spinner">⟳</span> 启动中...</span>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -679,7 +677,7 @@ onMounted(async () => {
|
||||
.price { color: #e6a23c; font-weight: 600; }
|
||||
.table-loading { position: absolute; inset: 0; background: rgba(255, 255, 255, 0.95); display: flex; flex-direction: column; justify-content: center; align-items: center; font-size: 14px; color: #606266; }
|
||||
.spinner { font-size: 24px; animation: spin 1s linear infinite; margin-bottom: 8px; }
|
||||
.action-buttons .spinner { font-size: 14px; margin-bottom: 0; display: inline-block; }
|
||||
.inline-spinner { display: inline-block; animation: spin 1s linear infinite; }
|
||||
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
|
||||
.pagination-fixed { flex-shrink: 0; padding: 8px 12px 0 12px; background: #fff; display: flex; justify-content: flex-end; }
|
||||
.pagination-fixed :deep(.el-pager li.is-active) { border: 1px solid #1677FF; border-radius: 4px; color: #1677FF; background: #fff; }
|
||||
@@ -696,4 +694,4 @@ onMounted(async () => {
|
||||
export default {
|
||||
name: 'AmazonDashboard',
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
BIN
electron-vue-template/src/renderer/public/icon/amazon.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 4.6 KiB |
BIN
electron-vue-template/src/renderer/public/icon/rakuten.png
Normal file
|
After Width: | Height: | Size: 804 B |
BIN
electron-vue-template/src/renderer/public/icon/zebra.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
@@ -95,6 +95,11 @@
|
||||
<artifactId>selenium-java</artifactId>
|
||||
<version>4.23.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.github.bonigarcia</groupId>
|
||||
<artifactId>webdrivermanager</artifactId>
|
||||
<version>5.9.2</version>
|
||||
</dependency>
|
||||
|
||||
<!-- JWT parsing for local RS256 verification -->
|
||||
<dependency>
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
package com.tashow.erp.config;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.scheduling.annotation.EnableAsync;
|
||||
|
||||
@Configuration
|
||||
@EnableAsync
|
||||
public class AsyncConfig {
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.tashow.erp.config;
|
||||
|
||||
import io.github.bonigarcia.wdm.WebDriverManager;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.openqa.selenium.chrome.ChromeDriver;
|
||||
import org.openqa.selenium.chrome.ChromeOptions;
|
||||
import org.springframework.boot.ApplicationArguments;
|
||||
import org.springframework.boot.ApplicationRunner;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.stereotype.Component;
|
||||
import java.net.URL;
|
||||
|
||||
/**
|
||||
* ChromeDriver预加载器
|
||||
* 在应用启动时后台下载ChromeDriver,避免用户首次使用时等待
|
||||
* 使用国内镜像源加速下载
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@Order(2) // 在DatabaseConfig之后运行
|
||||
public class ChromeDriverPreloader implements ApplicationRunner {
|
||||
|
||||
@Override
|
||||
public void run(ApplicationArguments args) {
|
||||
// 使用后台线程执行,不阻塞应用启动
|
||||
Thread preloadThread = new Thread(() -> {
|
||||
try {
|
||||
log.info("开始预加载ChromeDriver驱动(使用国内镜像加速)...");
|
||||
|
||||
// 配置WebDriverManager使用国内镜像源
|
||||
WebDriverManager.chromedriver()
|
||||
.driverRepositoryUrl(new URL("https://registry.npmmirror.com/-/binary/chromedriver/"))
|
||||
.setup();
|
||||
|
||||
log.info("ChromeDriver驱动下载完成,开始验证...");
|
||||
|
||||
// 快速验证驱动可用性
|
||||
ChromeOptions options = new ChromeOptions();
|
||||
options.addArguments("--headless"); // 无头模式,不显示浏览器
|
||||
options.addArguments("--disable-gpu");
|
||||
options.addArguments("--no-sandbox");
|
||||
|
||||
ChromeDriver driver = new ChromeDriver(options);
|
||||
driver.quit(); // 立即关闭
|
||||
|
||||
log.info("ChromeDriver驱动预加载成功");
|
||||
|
||||
} catch (Exception e) {
|
||||
// 预加载失败不影响应用启动,第一次使用时会自动下载
|
||||
log.warn("ChromeDriver驱动预加载失败(不影响使用): {}", e.getMessage());
|
||||
}
|
||||
}, "ChromeDriver-Preloader");
|
||||
|
||||
preloadThread.setDaemon(true); // 设置为守护线程
|
||||
preloadThread.start();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,9 +84,14 @@ public class SystemController {
|
||||
|
||||
@PostMapping("/genmai/open")
|
||||
public JsonData openGenmaiWebsite(@RequestParam(required = false) Long accountId, HttpServletRequest request) {
|
||||
String username = com.tashow.erp.utils.JwtUtil.getUsernameFromRequest(request);
|
||||
genmaiService.openGenmaiWebsite(accountId, username);
|
||||
return JsonData.buildSuccess("跟卖精灵正在启动");
|
||||
try {
|
||||
String username = com.tashow.erp.utils.JwtUtil.getUsernameFromRequest(request);
|
||||
genmaiService.openGenmaiWebsite(accountId, username);
|
||||
return JsonData.buildSuccess("跟卖精灵已打开");
|
||||
} catch (Exception e) {
|
||||
logger.error("打开跟卖精灵失败", e);
|
||||
return JsonData.buildError(e.getMessage() != null ? e.getMessage() : "打开跟卖精灵失败");
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/proxy/image")
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
package com.tashow.erp.service.impl;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import org.openqa.selenium.chrome.ChromeDriver;
|
||||
import org.openqa.selenium.chrome.ChromeOptions;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class ChromeDriverPreloader {
|
||||
@PostConstruct
|
||||
public void preloadDriver() {
|
||||
new Thread(() -> {
|
||||
try {
|
||||
ChromeOptions options = new ChromeOptions();
|
||||
options.addArguments("--headless", "--disable-gpu");
|
||||
ChromeDriver driver = new ChromeDriver(options);
|
||||
driver.quit();
|
||||
} catch (Exception ignored) {}
|
||||
}).start();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,14 +2,15 @@ package com.tashow.erp.service.impl;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.qiniu.util.UrlUtils;
|
||||
import io.github.bonigarcia.wdm.WebDriverManager;
|
||||
import org.openqa.selenium.JavascriptExecutor;
|
||||
import org.openqa.selenium.chrome.ChromeDriver;
|
||||
import org.openqa.selenium.chrome.ChromeOptions;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.*;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import java.net.URL;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@@ -20,27 +21,29 @@ public class GenmaiServiceImpl {
|
||||
private final RestTemplate restTemplate = new RestTemplate();
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
@Async
|
||||
public void openGenmaiWebsite(Long accountId, String username) {
|
||||
try {
|
||||
String token = getAndValidateToken(accountId, username);
|
||||
Runtime.getRuntime().exec("taskkill /f /im chrome.exe");
|
||||
Thread.sleep(1000);
|
||||
|
||||
ChromeOptions options = new ChromeOptions();
|
||||
String systemUser = System.getProperty("user.name");
|
||||
char firstChar = systemUser.charAt(0);
|
||||
char flipped = Character.isUpperCase(firstChar) ? Character.toLowerCase(firstChar) : Character.toUpperCase(firstChar);
|
||||
String chromeUserData = System.getProperty("user.home")
|
||||
.replace(systemUser, UrlUtils.urlEncode(flipped + systemUser.substring(1)))
|
||||
+ "\\AppData\\Local\\Google\\Chrome\\User Data";
|
||||
options.addArguments("user-data-dir=" + chromeUserData.toLowerCase(), "profile-directory=Default");
|
||||
|
||||
ChromeDriver driver = new ChromeDriver(options);
|
||||
driver.get("https://www.genmaijl.com/#/profile");
|
||||
((JavascriptExecutor) driver).executeScript("localStorage.setItem('token','" + token + "')");
|
||||
driver.navigate().refresh();
|
||||
} catch (Exception ignored) {}
|
||||
public void openGenmaiWebsite(Long accountId, String username) throws Exception {
|
||||
WebDriverManager.chromedriver()
|
||||
.driverRepositoryUrl(new URL("https://registry.npmmirror.com/-/binary/chromedriver/"))
|
||||
.setup();
|
||||
|
||||
String token = getAndValidateToken(accountId, username);
|
||||
Runtime.getRuntime().exec("taskkill /f /im chrome.exe");
|
||||
Thread.sleep(1000);
|
||||
|
||||
|
||||
ChromeOptions options = new ChromeOptions();
|
||||
String systemUser = System.getProperty("user.name");
|
||||
char firstChar = systemUser.charAt(0);
|
||||
char flipped = Character.isUpperCase(firstChar) ? Character.toLowerCase(firstChar) : Character.toUpperCase(firstChar);
|
||||
String chromeUserData = System.getProperty("user.home")
|
||||
.replace(systemUser, UrlUtils.urlEncode(flipped + systemUser.substring(1)))
|
||||
+ "\\AppData\\Local\\Google\\Chrome\\User Data";
|
||||
options.addArguments("user-data-dir=" + chromeUserData.toLowerCase(), "profile-directory=Default");
|
||||
|
||||
ChromeDriver driver = new ChromeDriver(options);
|
||||
driver.get("https://www.genmaijl.com/#/profile");
|
||||
((JavascriptExecutor) driver).executeScript("localStorage.setItem('token','" + token + "')");
|
||||
driver.navigate().refresh();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||