feat(electron):优化商标筛查面板与资源加载逻辑

- 将多个 v-if 条件渲染改为 v-show,提升组件切换性能
- 优化商标任务完成状态判断逻辑,确保准确显示采集完成图标- 调整任务统计数据显示条件,支持零数据展示- 更新 API 配置地址,切换至本地开发环境地址
- 降低 Spring Boot 线程池与数据库连接池配置,适应小规模并发- 禁用 devtools 热部署与 Swagger 接口文档,优化生产环境性能
- 配置 RestTemplate 使用 HttpClient 连接池,增强 HTTP 请求稳定性
- 改进静态资源拷贝脚本,确保 icon 与 image 文件夹正确复制
- 更新 electron-builder 配置,优化资源打包路径与应用图标
- 修改 HTTP 路由规则,明确区分客户端与管理端接口路径- 注册文件协议拦截器,解决生产环境下 icon/image 资源加载问题
- 调整商标 API 接口路径,指向 erp_client_sb服务
-重构 MarkController 控制器,专注 Token 管理功能
- 优化线程池参数,适配低并发业务场景- 强化商标筛查流程控制,完善任务取消与异常处理机制
- 新增方舟精选任务管理接口,实现 Excel 下载与数据解析功能
This commit is contained in:
2025-11-06 14:39:58 +08:00
parent cfb70d5830
commit 2f00fde3be
45 changed files with 593 additions and 311 deletions

View File

@@ -7,12 +7,11 @@
"compression": "maximum",
"asarUnpack": [
"public/jre/**/*",
"public/icon/icon.png",
"public/icon/image.png",
"public/icon/img.png",
"public/icon/**/*",
"public/image/**/*",
"public/splash.html",
"public/config/**/*"
"public/config/**/*",
"renderer/**/*"
],
"directories": {
"output": "dist"
@@ -26,7 +25,7 @@
},
"win": {
"target": "dir",
"icon": "public/icon/icon.png"
"icon": "public/icon/icon1.png"
},
"files": [
"package.json",
@@ -40,19 +39,12 @@
"to": "renderer",
"filter": ["**/*"]
},
{
"from": "src/main/static",
"to": "static",
"filter": ["**/*"]
},
{
"from": "public",
"to": "public",
"filter": [
"jre/**/*",
"icon/icon.png",
"icon/image.png",
"icon/img.png",
"icon/**/*",
"image/**/*",
"splash.html",
"config/**/*",

View File

Before

Width:  |  Height:  |  Size: 533 B

After

Width:  |  Height:  |  Size: 533 B

View File

Before

Width:  |  Height:  |  Size: 638 B

After

Width:  |  Height:  |  Size: 638 B

View File

Before

Width:  |  Height:  |  Size: 378 B

After

Width:  |  Height:  |  Size: 378 B

View File

Before

Width:  |  Height:  |  Size: 913 B

After

Width:  |  Height:  |  Size: 913 B

View File

Before

Width:  |  Height:  |  Size: 731 B

After

Width:  |  Height:  |  Size: 731 B

View File

Before

Width:  |  Height:  |  Size: 894 B

After

Width:  |  Height:  |  Size: 894 B

View File

Before

Width:  |  Height:  |  Size: 870 B

After

Width:  |  Height:  |  Size: 870 B

View File

Before

Width:  |  Height:  |  Size: 3.1 MiB

After

Width:  |  Height:  |  Size: 3.1 MiB

View File

Before

Width:  |  Height:  |  Size: 968 B

After

Width:  |  Height:  |  Size: 968 B

View File

Before

Width:  |  Height:  |  Size: 628 B

After

Width:  |  Height:  |  Size: 628 B

View File

Before

Width:  |  Height:  |  Size: 894 B

After

Width:  |  Height:  |  Size: 894 B

View File

Before

Width:  |  Height:  |  Size: 533 B

After

Width:  |  Height:  |  Size: 533 B

View File

Before

Width:  |  Height:  |  Size: 384 B

After

Width:  |  Height:  |  Size: 384 B

View File

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 67 KiB

View File

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 60 KiB

View File

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

@@ -2,7 +2,25 @@ const Path = require('path');
const FileSystem = require('fs-extra');
async function copyAssets() {
console.log('Static assets are now handled by Vite from src/renderer/public');
console.log('Copying static assets from public directory...');
const publicDir = Path.join(__dirname, '..', 'public');
const buildRendererDir = Path.join(__dirname, '..', 'build', 'renderer');
// 确保 build/renderer 下的 icon 和 image 目录存在且是最新的
// 这样打包后 renderer/icon 和 renderer/image 会包含所有图标
await FileSystem.copy(
Path.join(publicDir, 'icon'),
Path.join(buildRendererDir, 'icon'),
{ overwrite: true }
);
await FileSystem.copy(
Path.join(publicDir, 'image'),
Path.join(buildRendererDir, 'image'),
{ overwrite: true }
);
console.log('Static assets copied to build/renderer successfully!');
}
module.exports = copyAssets;

View File

@@ -1,6 +1,6 @@
import {app, BrowserWindow, ipcMain, Menu, screen, dialog} from 'electron';
import {existsSync, createWriteStream, promises as fs, mkdirSync, copyFileSync, readdirSync, writeFileSync} from 'fs';
import {join, dirname, basename} from 'path';
import {app, BrowserWindow, ipcMain, Menu, screen, dialog, protocol} from 'electron';
import {existsSync, createWriteStream, promises as fs, mkdirSync, copyFileSync, readdirSync, writeFileSync, readFileSync} from 'fs';
import {join, dirname, basename, extname} from 'path';
import {spawn, ChildProcess} from 'child_process';
import * as https from 'https';
import * as http from 'http';
@@ -74,7 +74,7 @@ function getJarFilePath(): string {
}
const getSplashPath = () => getResourcePath('../../public/splash.html', 'public/splash.html');
const getIconPath = () => getResourcePath('../../public/icon/icon.png', 'public/icon/icon.png', '../renderer/icon/icon.png');
const getIconPath = () => getResourcePath('../../public/icon/icon1.png', 'public/icon/icon1.png', '../renderer/icon/icon1.png');
const getLogbackConfigPath = () => getResourcePath('../../public/config/logback.xml', 'public/config/logback.xml');
function getDataDirectoryPath(): string {
@@ -211,7 +211,7 @@ function startSpringBoot() {
}
}
startSpringBoot();
// startSpringBoot();
function stopSpringBoot() {
if (!springProcess) return;
try {
@@ -305,6 +305,28 @@ if (!gotTheLock) {
}
app.whenReady().then(() => {
// 注册文件协议拦截器,将 /icon/ 和 /image/ 请求重定向到 public 目录
if (!isDev) {
protocol.interceptFileProtocol('file', (request, callback) => {
let url = request.url.substring(8); // 移除 'file:///'
// 检查是否是 icon 或 image 资源请求
if (url.includes('/icon/') || url.includes('/image/')) {
const match = url.match(/\/(icon|image)\/([^?#]+)/);
if (match) {
const [, type, filename] = match;
const publicPath = join(process.resourcesPath, 'app.asar.unpacked', 'public', type, filename);
if (existsSync(publicPath)) {
callback({ path: publicPath });
return;
}
}
}
callback({ path: url });
});
}
// 应用开机自启动配置
const config = loadConfig();
const shouldMinimize = config.launchMinimized || false;
@@ -348,9 +370,9 @@ app.whenReady().then(() => {
}
}
//666
// setTimeout(() => {
// openAppIfNotOpened();
// }, 100);
setTimeout(() => {
openAppIfNotOpened();
}, 100);
app.on('activate', () => {
if (mainWindow && !mainWindow.isDestroyed()) {

View File

@@ -7,11 +7,11 @@ let tray: Tray | null = null
function getIconPath(): string {
const isDev = process.env.NODE_ENV === 'development'
if (isDev) {
return join(__dirname, '../../public/icon/icon.png')
return join(__dirname, '../../public/icon/icon1.png')
}
const bundledPath = join(process.resourcesPath, 'app.asar.unpacked', 'public/icon/icon.png')
const bundledPath = join(process.resourcesPath, 'app.asar.unpacked', 'public/icon/icon1.png')
if (existsSync(bundledPath)) return bundledPath
return join(__dirname, '../renderer/icon/icon.png')
return join(__dirname, '../renderer/icon/icon1.png')
}
export function createTray(mainWindow: BrowserWindow | null) {

View File

@@ -1,13 +1,14 @@
export type HttpMethod = 'GET' | 'POST' | 'DELETE';
export const CONFIG = {
CLIENT_BASE: 'http://localhost:8081',
RUOYI_BASE: 'http://8.138.23.49:8085',
//RUOYI_BASE: 'http://192.168.1.89:8085',
SSE_URL: 'http://8.138.23.49:8085/monitor/account/events'
//RUOYI_BASE: 'http://8.138.23.49:8085',
RUOYI_BASE: 'http://192.168.1.89:8085',
SSE_URL: 'http://192.168.1.89:8085/monitor/account/events'
} as const;
function resolveBase(path: string): string {
if (path.startsWith('/monitor/') || path.startsWith('/system/') || path.startsWith('/tool/banma') || path.startsWith('/tool/genmai') || path.startsWith('/tool/mark')) {
// 路由到 ruoyi-admin (8085):仅系统管理和监控相关
if (path.startsWith('/monitor/') || path.startsWith('/system/') || path.startsWith('/tool/banma') || path.startsWith('/tool/genmai')) {
return CONFIG.RUOYI_BASE;
}
return CONFIG.CLIENT_BASE;

View File

@@ -1,16 +1,16 @@
import { http } from './http'
export const markApi = {
// 新建任务
// 新建任务(调用 erp_client_sb
newTask(file: File) {
const formData = new FormData()
formData.append('file', file)
return http.upload<{ code: number, data: any, msg: string }>('/tool/mark/newTask', formData)
return http.upload<{ code: number, data: any, msg: string }>('/api/trademark/newTask', formData)
},
// 获取任务列表及筛选数据(返回完整行数据和表头
// 获取任务列表及筛选数据(调用 erp_client_sb
getTask() {
return http.get<{
return http.post<{
code: number,
data: {
original: any,
@@ -18,7 +18,7 @@ export const markApi = {
headers: string[] // 表头
},
msg: string
}>('/tool/mark/task')
}>('/api/trademark/task')
},
// 品牌商标筛查

View File

@@ -150,7 +150,7 @@ function handleExportData() {
<!-- ASIN查询面板 -->
<AsinQueryPanel
v-if="currentTab === 'asin'"
v-show="currentTab === 'asin'"
ref="asinPanelRef"
:is-vip="isVip"
@update-data="handleAsinDataUpdate"
@@ -158,13 +158,13 @@ function handleExportData() {
<!-- 跟卖精灵面板 -->
<GenmaiSpiritPanel
v-if="currentTab === 'genmai'"
v-show="currentTab === 'genmai'"
:is-vip="isVip"
/>
<!-- 商标筛查面板 -->
<TrademarkCheckPanel
v-if="currentTab === 'trademark'"
v-show="currentTab === 'trademark'"
ref="trademarkPanelRef"
:is-vip="isVip"
@update-data="handleTrademarkDataUpdate"
@@ -296,7 +296,7 @@ function handleExportData() {
<div class="status-column">
<!-- 任务1产品商标筛查 -->
<div class="status-item">
<img v-if="(trademarkPanelRef?.taskProgress?.product?.total || 0) > 100" src="/icon/done.png" alt="采集完成" class="status-indicator-icon" />
<img v-if="(trademarkPanelRef?.taskProgress?.product?.total || 0) > 0 && (trademarkPanelRef?.taskProgress?.product?.current || 0) >= trademarkPanelRef.taskProgress.product.total" src="/icon/done.png" alt="采集完成" class="status-indicator-icon" />
<img v-else-if="((trademarkPanelRef?.taskProgress?.product?.total || 0) > 0 || (trademarkPanelRef?.taskProgress?.product?.current || 0) > 0) && (trademarkPanelRef?.queryStatus === 'error' || trademarkPanelRef?.queryStatus === 'networkError')" src="/icon/error.png" alt="筛查失败" class="status-indicator-icon" />
<img v-else-if="((trademarkPanelRef?.taskProgress?.product?.total || 0) > 0 || (trademarkPanelRef?.taskProgress?.product?.current || 0) > 0) && trademarkPanelRef?.queryStatus === 'cancel'" src="/icon/cancel.png" alt="已取消" class="status-indicator-icon" />
<img v-else-if="(trademarkPanelRef?.taskProgress?.product?.current || 0) > 0" src="/icon/inProgress.png" alt="采集中" class="status-indicator-icon" />
@@ -340,15 +340,15 @@ function handleExportData() {
<div class="task-stats">
<div class="task-stat">
<span class="stat-label">查询数量</span>
<span class="stat-value">{{ (trademarkPanelRef?.taskProgress?.product?.total || 0) > 100 ? trademarkPanelRef.taskProgress.product.total : '-' }}</span>
<span class="stat-value">{{ (trademarkPanelRef?.taskProgress?.product?.total || 0) > 0 ? trademarkPanelRef.taskProgress.product.total : '-' }}</span>
</div>
<div class="task-stat highlight">
<span class="stat-label">未注册/TM标</span>
<span class="stat-value">{{ (trademarkPanelRef?.taskProgress?.product?.total || 0) > 100 ? (trademarkPanelRef?.taskProgress?.product?.completed || 0) : '-' }}</span>
<span class="stat-value">{{ (trademarkPanelRef?.taskProgress?.product?.total || 0) > 0 && (trademarkPanelRef?.taskProgress?.product?.current || 0) >= trademarkPanelRef.taskProgress.product.total ? (trademarkPanelRef?.taskProgress?.product?.completed || 0) : '-' }}</span>
</div>
<div class="task-stat">
<span class="stat-label">已过滤</span>
<span class="stat-value">{{ (trademarkPanelRef?.taskProgress?.product?.total || 0) > 100 ? ((trademarkPanelRef?.taskProgress?.product?.total || 0) - (trademarkPanelRef?.taskProgress?.product?.completed || 0)) : '-' }}</span>
<span class="stat-value">{{ (trademarkPanelRef?.taskProgress?.product?.total || 0) > 0 && (trademarkPanelRef?.taskProgress?.product?.current || 0) >= trademarkPanelRef.taskProgress.product.total ? ((trademarkPanelRef?.taskProgress?.product?.total || 0) - (trademarkPanelRef?.taskProgress?.product?.completed || 0)) : '-' }}</span>
</div>
</div>
</div>

View File

@@ -332,7 +332,6 @@ async function startTrademarkQuery() {
try {
productResult = await pollTask()
if (!trademarkLoading.value) return
if (!productResult || (productResult.code !== 200 && productResult.code !== 0)) {
throw new Error('获取任务超时或失败,请重试')
@@ -360,6 +359,7 @@ async function startTrademarkQuery() {
if (filteredAsins.length === 0) {
showMessage('第三方筛查结果为空', 'warning')
queryStatus.value = 'done'
return
}
@@ -396,8 +396,6 @@ async function startTrademarkQuery() {
// 品牌商标筛查
if (needBrandCheck) {
if (!trademarkLoading.value) return
// 如果没有执行产品筛查需要先从Excel提取品牌列表和完整数据
if (!needProductCheck) {
const extractResult = await markApi.extractBrands(trademarkFile.value)
@@ -415,9 +413,7 @@ async function startTrademarkQuery() {
trademarkHeaders.value = extractResult.data.headers || []
}
if (brandList.length === 0) {
showMessage('没有品牌需要筛查', 'warning')
} else {
if (brandList.length > 0) {
const brandData = taskProgress.value.brand
brandData.total = brandList.length
brandData.current = 1 // 立即显示初始进度
@@ -439,8 +435,6 @@ async function startTrademarkQuery() {
const brandResult = await markApi.brandCheck(brandList, brandTaskId.value)
if (brandProgressTimer) clearInterval(brandProgressTimer)
if (!trademarkLoading.value) return
if (brandResult.code === 200 || brandResult.code === 0) {
// 完成显示100%
brandData.total = brandResult.data.checked || brandResult.data.total || brandData.total
@@ -489,18 +483,17 @@ async function startTrademarkQuery() {
}
}
if (trademarkLoading.value) {
queryStatus.value = 'done'
emit('updateData', trademarkData.value)
let summaryMsg = '筛查完成'
if (needProductCheck) summaryMsg += `,产品:${taskProgress.value.product.completed}/${taskProgress.value.product.total}`
if (needBrandCheck && brandList.length > 0) summaryMsg += `,品牌:${taskProgress.value.brand.completed}/${taskProgress.value.brand.total}`
showMessage(summaryMsg, 'success')
// 保存会话
await saveSession()
}
// 只要流程正常完成就设置为done状态不再依赖trademarkLoading
queryStatus.value = 'done'
emit('updateData', trademarkData.value)
let summaryMsg = '筛查完成'
if (needProductCheck) summaryMsg += `,产品:${taskProgress.value.product.completed}/${taskProgress.value.product.total}`
if (needBrandCheck && brandList.length > 0) summaryMsg += `,品牌:${taskProgress.value.brand.completed}/${taskProgress.value.brand.total}`
showMessage(summaryMsg, 'success')
// 保存会话
await saveSession()
} catch (error: any) {
const hasProductData = taskProgress.value.product.total > 0
@@ -536,6 +529,13 @@ async function startTrademarkQuery() {
clearInterval(brandProgressTimer)
brandProgressTimer = null
}
// 兜底:如果有数据且状态还是进行中,强制设置为完成(最优先处理)
if (trademarkData.value.length > 0 && queryStatus.value === 'inProgress') {
queryStatus.value = 'done'
emit('updateData', trademarkData.value)
}
trademarkLoading.value = false
currentStep.value = 0
totalSteps.value = 0

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 804 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

View File

@@ -1,33 +0,0 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>正在启动...</title>
<style>
html, body { height: 100%; margin: 0; }
body {
background: #fff; font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;
background-image: url('./image/splash_screen.png');
background-repeat: no-repeat;
background-position: center;
background-size: cover;
}
.box { position: fixed; left: 0; right: 0; bottom: 28px; padding: 0 0; }
.progress { position: relative; width: 100vw; height: 6px; background: rgba(0,0,0,0.08); }
.bar { position: absolute; left: 0; top: 0; height: 100%; width: 20vw; min-width: 120px; background: linear-gradient(90deg, #67C23A, #409EFF); animation: slide 1s ease-in-out infinite alternate; }
@keyframes slide { 0% { left: 0; } 100% { left: calc(100vw - 20vw); } }
</style>
<link rel="icon" href="icon/icon.png">
<link rel="apple-touch-icon" href="icon/icon.png">
<meta name="theme-color" content="#ffffff">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline';">
</head>
<body>
<div class="box">
<div class="progress"><div class="bar"></div></div>
</div>
</body>
</html>

View File

@@ -6,7 +6,7 @@ const { defineConfig } = require('vite');
*/
const config = defineConfig({
root: Path.join(__dirname, 'src', 'renderer'),
publicDir: Path.join(__dirname, 'src', 'renderer', 'public'),
publicDir: Path.join(__dirname, 'public'),
server: {
port: 8083,
},