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 2f00fde3be
commit 7c7009ffed
13 changed files with 255 additions and 114 deletions

View File

@@ -10,8 +10,7 @@
"public/icon/**/*", "public/icon/**/*",
"public/image/**/*", "public/image/**/*",
"public/splash.html", "public/splash.html",
"public/config/**/*", "public/config/**/*"
"renderer/**/*"
], ],
"directories": { "directories": {
"output": "dist" "output": "dist"
@@ -37,7 +36,15 @@
{ {
"from": "build/renderer", "from": "build/renderer",
"to": "renderer", "to": "renderer",
"filter": ["**/*"] "filter": [
"**/*",
"!icon/**/*",
"!image/**/*",
"!jre/**/*",
"!config/**/*",
"!*.jar",
"!splash.html"
]
}, },
{ {
"from": "public", "from": "public",

View File

@@ -4,23 +4,25 @@ const FileSystem = require('fs-extra');
async function copyAssets() { async function copyAssets() {
console.log('Copying static assets from public directory...'); console.log('Copying static assets from public directory...');
const publicDir = Path.join(__dirname, '..', 'public'); // 注释icon 和 image 资源已统一由 public 目录管理
const buildRendererDir = Path.join(__dirname, '..', 'build', 'renderer'); // electron-builder 会直接从 public 打包这些资源到 app.asar.unpacked
// 不需要复制到 build/renderer避免重复打包导致体积增大
// 确保 build/renderer 下的 icon 和 image 目录存在且是最新的 // const publicDir = Path.join(__dirname, '..', 'public');
// 这样打包后 renderer/icon 和 renderer/image 会包含所有图标 // const buildRendererDir = Path.join(__dirname, '..', 'build', 'renderer');
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!'); // 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 copy skipped (resources managed by public directory).');
} }
module.exports = copyAssets; module.exports = copyAssets;

View File

@@ -4,10 +4,9 @@ import {join, dirname, basename, extname} 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 { fileURLToPath } from 'url';
import { createTray, destroyTray } from './tray'; import { createTray, destroyTray } from './tray';
const isDev = process.env.NODE_ENV === 'development'; const isDev = process.env.NODE_ENV === 'development';
let springProcess: ChildProcess | null = null; let springProcess: ChildProcess | null = null;
let mainWindow: BrowserWindow | null = null; let mainWindow: BrowserWindow | null = null;
let splashWindow: BrowserWindow | null = null; let splashWindow: BrowserWindow | null = null;
@@ -74,7 +73,7 @@ function getJarFilePath(): string {
} }
const getSplashPath = () => getResourcePath('../../public/splash.html', 'public/splash.html'); const getSplashPath = () => getResourcePath('../../public/splash.html', 'public/splash.html');
const getIconPath = () => getResourcePath('../../public/icon/icon1.png', 'public/icon/icon1.png', '../renderer/icon/icon1.png'); const getIconPath = () => getResourcePath('../../public/icon/icon1.png', 'public/icon/icon1.png');
const getLogbackConfigPath = () => getResourcePath('../../public/config/logback.xml', 'public/config/logback.xml'); const getLogbackConfigPath = () => getResourcePath('../../public/config/logback.xml', 'public/config/logback.xml');
function getDataDirectoryPath(): string { function getDataDirectoryPath(): string {
@@ -211,7 +210,7 @@ function startSpringBoot() {
} }
} }
// startSpringBoot(); startSpringBoot();
function stopSpringBoot() { function stopSpringBoot() {
if (!springProcess) return; if (!springProcess) return;
try { try {
@@ -305,14 +304,21 @@ if (!gotTheLock) {
} }
app.whenReady().then(() => { app.whenReady().then(() => {
// 注册文件协议拦截器,将 /icon/ 和 /image/ 请求重定向到 public 目录
if (!isDev) { if (!isDev) {
protocol.interceptFileProtocol('file', (request, callback) => { protocol.interceptFileProtocol('file', (request, callback) => {
let url = request.url.substring(8); // 移除 'file:///' // 使用 fileURLToPath 正确解码 URL处理空格和特殊字符
let filePath: string;
try {
filePath = fileURLToPath(request.url);
} catch (e) {
// 如果解码失败,回退到原来的方法
filePath = decodeURIComponent(request.url.substring(8));
}
// 检查是否是 icon 或 image 资源请求 // 检查是否是 icon 或 image 资源请求
if (url.includes('/icon/') || url.includes('/image/')) { if (filePath.includes('/icon/') || filePath.includes('\\icon\\') ||
const match = url.match(/\/(icon|image)\/([^?#]+)/); filePath.includes('/image/') || filePath.includes('\\image\\')) {
const match = filePath.match(/[/\\](icon|image)[/\\]([^?#]+)/);
if (match) { if (match) {
const [, type, filename] = match; const [, type, filename] = match;
const publicPath = join(process.resourcesPath, 'app.asar.unpacked', 'public', type, filename); const publicPath = join(process.resourcesPath, 'app.asar.unpacked', 'public', type, filename);
@@ -323,7 +329,7 @@ app.whenReady().then(() => {
} }
} }
callback({ path: url }); callback({ path: filePath });
}); });
} }
@@ -370,9 +376,9 @@ app.whenReady().then(() => {
} }
} }
//666 //666
setTimeout(() => { // setTimeout(() => {
openAppIfNotOpened(); // openAppIfNotOpened();
}, 100); // }, 100);
app.on('activate', () => { app.on('activate', () => {
if (mainWindow && !mainWindow.isDestroyed()) { if (mainWindow && !mainWindow.isDestroyed()) {

View File

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

View File

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

View File

@@ -0,0 +1,9 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
export {}
declare global {
}

View File

@@ -0,0 +1,31 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
export {}
declare module 'vue' {
export interface GlobalComponents {
ElButton: typeof import('element-plus/es')['ElButton']
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
ElDialog: typeof import('element-plus/es')['ElDialog']
ElDropdown: typeof import('element-plus/es')['ElDropdown']
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
ElIcon: typeof import('element-plus/es')['ElIcon']
ElImage: typeof import('element-plus/es')['ElImage']
ElInput: typeof import('element-plus/es')['ElInput']
ElOption: typeof import('element-plus/es')['ElOption']
ElPagination: typeof import('element-plus/es')['ElPagination']
ElProgress: typeof import('element-plus/es')['ElProgress']
ElRadio: typeof import('element-plus/es')['ElRadio']
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
ElSelect: typeof import('element-plus/es')['ElSelect']
ElTable: typeof import('element-plus/es')['ElTable']
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
ElTag: typeof import('element-plus/es')['ElTag']
}
}

View File

@@ -249,7 +249,7 @@ function handleExportData() {
</div> </div>
<div class="banner-content"> <div class="banner-content">
<div class="banner-title">数据筛查中...</div> <div class="banner-title">数据筛查中...</div>
<div class="banner-desc">确保结果准确耐心等待保持后台运行...</div> <div class="banner-desc">避免进程中断勿退出应用保持后台运行...</div>
</div> </div>
<div class="banner-actions"> <div class="banner-actions">
<el-button size="default" @click="handleCancelTask">取消</el-button> <el-button size="default" @click="handleCancelTask">取消</el-button>
@@ -278,7 +278,7 @@ function handleExportData() {
</div> </div>
<div class="banner-content"> <div class="banner-content">
<div class="banner-title">{{ trademarkPanelRef.queryStatus === 'done' ? '筛查已完成' : '数据筛查失败' }}</div> <div class="banner-title">{{ trademarkPanelRef.queryStatus === 'done' ? '筛查已完成' : '数据筛查失败' }}</div>
<div class="banner-desc">{{ trademarkPanelRef.queryStatus === 'done' ? '点击"导出数据"按钮,可导出为 Excel 表格文。' : (trademarkPanelRef.errorMessage || '请稍后重试') }}</div> <div class="banner-desc">{{ trademarkPanelRef.queryStatus === 'done' ? '点击右侧“导出数据按钮,可导出为 Excel 表格文档。如过滤结果为 0请排查选品表格内容是否正确产品所属地区与商标查询地区是否不一致如不一致可能导致筛查结果偏差或错误。' : (trademarkPanelRef.errorMessage || '请稍后重试') }}</div>
</div> </div>
<div class="banner-actions"> <div class="banner-actions">
<el-button size="default" @click="handleNewTask">新建任务</el-button> <el-button size="default" @click="handleNewTask">新建任务</el-button>
@@ -328,7 +328,7 @@ function handleExportData() {
<div class="task-title-row"> <div class="task-title-row">
<div class="task-info"> <div class="task-info">
<div class="task-name">{{ trademarkPanelRef?.taskProgress?.product?.label || '未注册/TM商标筛查' }}</div> <div class="task-name">{{ trademarkPanelRef?.taskProgress?.product?.label || '未注册/TM商标筛查' }}</div>
<div class="task-desc">{{ trademarkPanelRef?.taskProgress?.product?.desc || '筛查未注册商标或TM标的产品' }}<span v-if="(trademarkPanelRef?.taskProgress?.product?.total || 0) > 0"> (已完成)</span></div> <div class="task-desc">{{ trademarkPanelRef?.taskProgress?.product?.desc || '筛查未注册商标或TM标的产品' }}<span v-if="trademarkPanelRef?.isProductTaskRealData && (trademarkPanelRef?.taskProgress?.product?.current || 0) >= trademarkPanelRef.taskProgress.product.total"> (已完成)</span></div>
</div> </div>
</div> </div>
<div class="task-progress-wrapper"> <div class="task-progress-wrapper">
@@ -340,15 +340,15 @@ function handleExportData() {
<div class="task-stats"> <div class="task-stats">
<div class="task-stat"> <div class="task-stat">
<span class="stat-label">查询数量</span> <span class="stat-label">查询数量</span>
<span class="stat-value">{{ (trademarkPanelRef?.taskProgress?.product?.total || 0) > 0 ? trademarkPanelRef.taskProgress.product.total : '-' }}</span> <span class="stat-value">{{ trademarkPanelRef?.isProductTaskRealData ? trademarkPanelRef.taskProgress.product.total : '-' }}</span>
</div> </div>
<div class="task-stat highlight"> <div class="task-stat highlight">
<span class="stat-label">未注册/TM标</span> <span class="stat-label">未注册/TM标</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> <span class="stat-value">{{ trademarkPanelRef?.isProductTaskRealData ? trademarkPanelRef.taskProgress.product.completed : '-' }}</span>
</div> </div>
<div class="task-stat"> <div class="task-stat">
<span class="stat-label">已过滤</span> <span class="stat-label">已过滤</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> <span class="stat-value">{{ trademarkPanelRef?.isProductTaskRealData ? (trademarkPanelRef.taskProgress.product.total - trademarkPanelRef.taskProgress.product.completed) : '-' }}</span>
</div> </div>
</div> </div>
</div> </div>
@@ -357,7 +357,7 @@ function handleExportData() {
<div class="task-title-row"> <div class="task-title-row">
<div class="task-info"> <div class="task-info">
<div class="task-name">{{ trademarkPanelRef?.taskProgress?.brand?.label || '品牌商标筛查' }}</div> <div class="task-name">{{ trademarkPanelRef?.taskProgress?.brand?.label || '品牌商标筛查' }}</div>
<div class="task-desc">{{ trademarkPanelRef?.taskProgress?.brand?.desc || '筛查未注册商标的品牌' }}<span v-if="(trademarkPanelRef?.taskProgress?.brand?.total || 0) > 0 && (trademarkPanelRef?.taskProgress?.brand?.current || 0) >= trademarkPanelRef.taskProgress.brand.total"> (已完成)</span></div> <div class="task-desc">{{ trademarkPanelRef?.taskProgress?.brand?.desc || '筛查未注册商标的品牌' }}<span v-if="trademarkPanelRef?.isBrandTaskRealData && (trademarkPanelRef?.taskProgress?.brand?.current || 0) >= trademarkPanelRef.taskProgress.brand.total"> (已完成)</span></div>
</div> </div>
</div> </div>
<div class="task-progress-wrapper"> <div class="task-progress-wrapper">
@@ -369,15 +369,15 @@ function handleExportData() {
<div class="task-stats"> <div class="task-stats">
<div class="task-stat"> <div class="task-stat">
<span class="stat-label">查询数量</span> <span class="stat-label">查询数量</span>
<span class="stat-value">{{ (trademarkPanelRef?.taskProgress?.brand?.total || 0) === 0 ? '-' : trademarkPanelRef.taskProgress.brand.total }}</span> <span class="stat-value">{{ trademarkPanelRef?.isBrandTaskRealData ? trademarkPanelRef.taskProgress.brand.total : '-' }}</span>
</div> </div>
<div class="task-stat highlight"> <div class="task-stat highlight">
<span class="stat-label">未注册</span> <span class="stat-label">未注册</span>
<span class="stat-value">{{ ((trademarkPanelRef?.taskProgress?.brand?.current || 0) >= (trademarkPanelRef?.taskProgress?.brand?.total || 1)) ? (trademarkPanelRef?.taskProgress?.brand?.completed || 0) : '-' }}</span> <span class="stat-value">{{ trademarkPanelRef?.isBrandTaskRealData ? trademarkPanelRef.taskProgress.brand.completed : '-' }}</span>
</div> </div>
<div class="task-stat"> <div class="task-stat">
<span class="stat-label">已注册</span> <span class="stat-label">已注册</span>
<span class="stat-value">{{ ((trademarkPanelRef?.taskProgress?.brand?.current || 0) >= (trademarkPanelRef?.taskProgress?.brand?.total || 1)) ? ((trademarkPanelRef?.taskProgress?.brand?.total || 0) - (trademarkPanelRef?.taskProgress?.brand?.completed || 0)) : '-' }}</span> <span class="stat-value">{{ trademarkPanelRef?.isBrandTaskRealData ? (trademarkPanelRef.taskProgress.brand.total - trademarkPanelRef.taskProgress.brand.completed) : '-' }}</span>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -4,6 +4,7 @@ import { ElMessage } from 'element-plus'
import { handlePlatformFileExport } from '../../utils/settings' import { handlePlatformFileExport } from '../../utils/settings'
import { getUsernameFromToken } from '../../utils/token' import { getUsernameFromToken } from '../../utils/token'
import { markApi } from '../../api/mark' import { markApi } from '../../api/mark'
import { useFileDrop } from '../../composables/useFileDrop'
const TrialExpiredDialog = defineAsyncComponent(() => import('../common/TrialExpiredDialog.vue')) const TrialExpiredDialog = defineAsyncComponent(() => import('../common/TrialExpiredDialog.vue'))
const refreshVipStatus = inject<() => Promise<boolean>>('refreshVipStatus') const refreshVipStatus = inject<() => Promise<boolean>>('refreshVipStatus')
@@ -31,6 +32,10 @@ const totalSteps = ref(0)
let brandProgressTimer: any = null let brandProgressTimer: any = null
const brandTaskId = ref('') const brandTaskId = ref('')
// 真实数据标记(区分临时进度值和真实统计数据)
const isProductTaskRealData = ref(false)
const isBrandTaskRealData = ref(false)
// 三个任务的进度数据 // 三个任务的进度数据
const taskProgress = ref({ const taskProgress = ref({
product: { total: 0, current: 0, completed: 0, label: '产品商标筛查', desc: '筛查未注册商标或TM标的商品' }, product: { total: 0, current: 0, completed: 0, label: '产品商标筛查', desc: '筛查未注册商标或TM标的商品' },
@@ -107,6 +112,49 @@ function showMessage(message: string, type: 'success' | 'warning' | 'error' | 'i
ElMessage({ message, type }) ElMessage({ message, type })
} }
// 拖拽上传
async function processTrademarkFile(file: File) {
uploadLoading.value = true
try {
// 根据选中的查询类型确定需要的表头
const requiredHeaders: string[] = []
if (queryTypes.value.includes('product')) {
requiredHeaders.push('商品主图')
}
if (queryTypes.value.includes('brand')) {
requiredHeaders.push('品牌')
}
// 验证表头
if (requiredHeaders.length > 0) {
const validateResult = await markApi.validateHeaders(file, requiredHeaders)
if (validateResult.code !== 200 && validateResult.code !== 0) {
showMessage(validateResult.msg || '表头验证失败', 'error')
return
}
}
trademarkFileName.value = file.name
trademarkFile.value = file
queryStatus.value = 'idle'
trademarkData.value = []
trademarkFullData.value = []
trademarkHeaders.value = []
emit('updateData', [])
} catch (error: any) {
showMessage('表头验证失败: ' + error.message, 'error')
} finally {
uploadLoading.value = false
}
}
const { dragActive, onDragEnter, onDragOver, onDragLeave, onDrop } = useFileDrop({
accept: /\.xlsx?$/i,
onFile: processTrademarkFile,
onError: (msg) => showMessage(msg, 'warning')
})
function removeTrademarkFile() { function removeTrademarkFile() {
trademarkFileName.value = '' trademarkFileName.value = ''
trademarkFile.value = null trademarkFile.value = null
@@ -196,41 +244,8 @@ async function handleTrademarkUpload(e: Event) {
return return
} }
uploadLoading.value = true await processTrademarkFile(file)
try {
// 根据选中的查询类型确定需要的表头
const requiredHeaders: string[] = []
if (queryTypes.value.includes('product')) {
requiredHeaders.push('商品主图')
}
if (queryTypes.value.includes('brand')) {
requiredHeaders.push('品牌')
}
// 验证表头
if (requiredHeaders.length > 0) {
const validateResult = await markApi.validateHeaders(file, requiredHeaders)
if (validateResult.code !== 200 && validateResult.code !== 0) {
showMessage(validateResult.msg || '表头验证失败', 'error')
input.value = '' input.value = ''
return
}
}
trademarkFileName.value = file.name
trademarkFile.value = file
queryStatus.value = 'idle'
trademarkData.value = []
trademarkFullData.value = []
trademarkHeaders.value = []
emit('updateData', [])
} catch (error: any) {
showMessage('表头验证失败: ' + error.message, 'error')
} finally {
uploadLoading.value = false
input.value = ''
}
} }
async function startTrademarkQuery() { async function startTrademarkQuery() {
@@ -268,6 +283,10 @@ async function startTrademarkQuery() {
taskProgress.value.platform.current = 0 taskProgress.value.platform.current = 0
taskProgress.value.platform.completed = 0 taskProgress.value.platform.completed = 0
// 重置真实数据标记
isProductTaskRealData.value = false
isBrandTaskRealData.value = false
// 通知父组件更新数据 // 通知父组件更新数据
emit('updateData', []) emit('updateData', [])
@@ -297,7 +316,7 @@ async function startTrademarkQuery() {
// 轮询检查任务状态 // 轮询检查任务状态
const pollTask = async () => { const pollTask = async () => {
const maxWaitTime = 60000 const maxWaitTime = 60000
const pollInterval = 3000 const pollInterval = 2000
const startTime = Date.now() const startTime = Date.now()
let taskResult: any = null let taskResult: any = null
@@ -307,7 +326,16 @@ async function startTrademarkQuery() {
return null return null
} }
await new Promise(resolve => setTimeout(resolve, pollInterval)) // 分段等待每500ms检查一次取消状态
const waitSegments = pollInterval / 500
for (let i = 0; i < waitSegments; i++) {
if (!trademarkLoading.value) {
clearInterval(progressTimer)
return null
}
await new Promise(resolve => setTimeout(resolve, 500))
}
if (!trademarkLoading.value) { if (!trademarkLoading.value) {
clearInterval(progressTimer) clearInterval(progressTimer)
return null return null
@@ -321,8 +349,14 @@ async function startTrademarkQuery() {
return taskResult return taskResult
} }
} }
} catch (err) { } catch (err: any) {
// 继续等待 const errorMsg = err.message || ''
if (errorMsg.includes('表格没有数据') || errorMsg.includes('没有数据') ||
errorMsg.includes('任务处理失败') || errorMsg.includes('任务失败')) {
clearInterval(progressTimer)
throw err
}
// 其他错误(网络错误等)继续等待
} }
} }
@@ -333,7 +367,12 @@ async function startTrademarkQuery() {
try { try {
productResult = await pollTask() productResult = await pollTask()
if (!productResult || (productResult.code !== 200 && productResult.code !== 0)) { // 如果返回null说明用户取消了直接返回
if (!productResult) {
return
}
if (productResult.code !== 200 && productResult.code !== 0) {
throw new Error('获取任务超时或失败,请重试') throw new Error('获取任务超时或失败,请重试')
} }
@@ -345,6 +384,7 @@ async function startTrademarkQuery() {
taskData.total = productResult.data.original?.total || 0 taskData.total = productResult.data.original?.total || 0
taskData.current = taskData.total taskData.current = taskData.total
taskData.completed = productResult.data.filtered.length taskData.completed = productResult.data.filtered.length
isProductTaskRealData.value = true
} finally { } finally {
clearInterval(progressTimer) clearInterval(progressTimer)
} }
@@ -440,6 +480,7 @@ async function startTrademarkQuery() {
brandData.total = brandResult.data.checked || brandResult.data.total || brandData.total brandData.total = brandResult.data.checked || brandResult.data.total || brandData.total
brandData.current = brandData.total brandData.current = brandData.total
brandData.completed = brandResult.data.unregistered || 0 brandData.completed = brandResult.data.unregistered || 0
isBrandTaskRealData.value = true
// 提取未注册品牌列表 // 提取未注册品牌列表
const unregisteredBrands = brandResult.data.data.map((item: any) => item.brand).filter(Boolean) const unregisteredBrands = brandResult.data.data.map((item: any) => item.brand).filter(Boolean)
@@ -455,6 +496,7 @@ async function startTrademarkQuery() {
// 更新统计:显示过滤出的实际行数(而不是品牌数) // 更新统计:显示过滤出的实际行数(而不是品牌数)
brandData.completed = filterResult.data.filteredRows.length brandData.completed = filterResult.data.filteredRows.length
isBrandTaskRealData.value = true
// 将品牌筛查结果作为展示数据 // 将品牌筛查结果作为展示数据
const brandItems = filterResult.data.filteredRows.map((row: any) => ({ const brandItems = filterResult.data.filteredRows.map((row: any) => ({
@@ -495,33 +537,50 @@ async function startTrademarkQuery() {
// 保存会话 // 保存会话
await saveSession() await saveSession()
} catch (error: any) { } catch (error: any) {
const hasProductData = taskProgress.value.product.total > 0 const hasProductData = isProductTaskRealData.value && taskProgress.value.product.total > 0
const hasBrandData = isBrandTaskRealData.value && taskProgress.value.brand.total > 0
// 优化错误信息 - 只显示友好提示 // 优化错误信息 - 只显示友好提示
let msg = error.message || '' let msg = error.message || ''
if (msg.includes('网络') || msg.includes('network')) { let friendlyMsg = ''
if (msg.includes('网络') || msg.includes('network') || msg.includes('Network')) {
queryStatus.value = 'networkError' queryStatus.value = 'networkError'
errorMessage.value = '网络不可用,请检查你的网络或代理设置' friendlyMsg = '网络连接失败,请检查网络或代理设置'
} else if (msg.includes('超时') || msg.includes('timeout')) { } else if (msg.includes('超时') || msg.includes('timeout') || msg.includes('Timeout')) {
queryStatus.value = 'error' queryStatus.value = 'error'
errorMessage.value = '数据库维护中,请稍后重试' friendlyMsg = '查询超时,请稍后重试'
} else if (msg.includes('403') || msg.includes('风控')) { } else if (msg.includes('403') || msg.includes('风控')) {
queryStatus.value = 'error' queryStatus.value = 'error'
errorMessage.value = '网站风控限制,请稍后重试' friendlyMsg = '访问受限,请稍后重试'
} else if (msg.includes('表格没有数据') || msg.includes('没有数据')) {
queryStatus.value = 'error'
friendlyMsg = '表格没有有效数据,请检查文件内容'
} else if (msg.includes('创建任务失败') || msg.includes('提取品牌')) {
queryStatus.value = 'error'
friendlyMsg = '数据处理失败,请检查文件格式是否正确'
} else { } else {
queryStatus.value = 'error' queryStatus.value = 'error'
errorMessage.value = '数据库维护中,请稍后重试' friendlyMsg = '查询失败,请稍后重试'
} }
// 仅在第1步失败时清空数据 // 根据失败阶段确定错误提示
if (!hasProductData) { if (hasProductData && !hasBrandData && needBrandCheck) {
// 产品筛查完成,品牌筛查失败
errorMessage.value = `品牌筛查失败:${friendlyMsg}。产品筛查结果已保留`
// 保留产品数据
emit('updateData', trademarkData.value)
} else if (!hasProductData) {
errorMessage.value = `产品筛查失败:${friendlyMsg}`
trademarkData.value = [] trademarkData.value = []
trademarkFullData.value = [] trademarkFullData.value = []
trademarkHeaders.value = [] trademarkHeaders.value = []
emit('updateData', []) emit('updateData', [])
} else { } else {
errorMessage.value = friendlyMsg
emit('updateData', trademarkData.value) emit('updateData', trademarkData.value)
} }
showMessage(errorMessage.value, 'error') showMessage(errorMessage.value, 'error')
} finally { } finally {
// 清除定时器 // 清除定时器
@@ -646,6 +705,10 @@ function resetToIdle() {
taskProgress.value.platform.current = 0 taskProgress.value.platform.current = 0
taskProgress.value.platform.completed = 0 taskProgress.value.platform.completed = 0
// 重置真实数据标记
isProductTaskRealData.value = false
isBrandTaskRealData.value = false
// 清空localStorage中的会话数据 // 清空localStorage中的会话数据
try { try {
const username = getUsernameFromToken() const username = getUsernameFromToken()
@@ -666,7 +729,9 @@ defineExpose({
resetToIdle, resetToIdle,
stopTrademarkQuery, stopTrademarkQuery,
startTrademarkQuery, startTrademarkQuery,
exportTrademarkData exportTrademarkData,
isProductTaskRealData,
isBrandTaskRealData
}) })
</script> </script>
@@ -679,7 +744,15 @@ defineExpose({
<div class="step-card"> <div class="step-card">
<div class="step-header"><div class="title">导入Excel表格</div></div> <div class="step-header"><div class="title">导入Excel表格</div></div>
<div class="desc">产品筛查需导入卖家精灵选品表格并勾选"导出主图"品牌筛查Excel需包含"品牌"</div> <div class="desc">产品筛查需导入卖家精灵选品表格并勾选"导出主图"品牌筛查Excel需包含"品牌"</div>
<div class="dropzone" :class="{ uploading: uploadLoading }" @click="!uploadLoading && openTrademarkUpload()"> <div
class="dropzone"
:class="{ uploading: uploadLoading, 'drag-active': dragActive }"
@click="!uploadLoading && openTrademarkUpload()"
@dragenter="onDragEnter"
@dragover="onDragOver"
@dragleave="onDragLeave"
@drop="onDrop"
>
<div v-if="!uploadLoading" class="dz-icon">📤</div> <div v-if="!uploadLoading" class="dz-icon">📤</div>
<div v-else class="dz-icon spinner"></div> <div v-else class="dz-icon spinner"></div>
<div class="dz-text">{{ uploadLoading ? '正在验证表头...' : '点击或将文件拖拽到这里上传' }}</div> <div class="dz-text">{{ uploadLoading ? '正在验证表头...' : '点击或将文件拖拽到这里上传' }}</div>
@@ -845,6 +918,7 @@ defineExpose({
.dropzone:hover { background: #f6fbff; border-color: #409EFF; } .dropzone:hover { background: #f6fbff; border-color: #409EFF; }
.dropzone.uploading { cursor: not-allowed; opacity: 0.7; } .dropzone.uploading { cursor: not-allowed; opacity: 0.7; }
.dropzone.uploading:hover { background: #fafafa; border-color: #c0c4cc; } .dropzone.uploading:hover { background: #fafafa; border-color: #c0c4cc; }
.dropzone.drag-active { background: #e6f4ff; border-color: #1677FF; }
.dz-icon { font-size: 20px; margin-bottom: 6px; color: #909399; } .dz-icon { font-size: 20px; margin-bottom: 6px; color: #909399; }
.dz-text { color: #303133; font-size: 13px; margin-bottom: 2px; } .dz-text { color: #303133; font-size: 13px; margin-bottom: 2px; }
.dz-sub { color: #909399; font-size: 12px; } .dz-sub { color: #909399; font-size: 12px; }

View File

@@ -10,7 +10,7 @@
</parent> </parent>
<groupId>com.tashow.erp</groupId> <groupId>com.tashow.erp</groupId>
<artifactId>erp_client_sb</artifactId> <artifactId>erp_client_sb</artifactId>
<version>2.6.0</version> <version>2.6.2</version>
<name>erp_client_sb</name> <name>erp_client_sb</name>
<description>erp客户端</description> <description>erp客户端</description>
<properties> <properties>

View File

@@ -58,10 +58,10 @@ public class TrademarkController {
String taskId = (String) request.get("taskId"); String taskId = (String) request.get("taskId");
try { try {
// 保持与前端传入数量一致,不去重(允许重复品牌以匹配产品数量)
List<String> list = brands.stream() List<String> list = brands.stream()
.filter(b -> b != null && !b.trim().isEmpty()) .filter(b -> b != null && !b.trim().isEmpty())
.map(String::trim) .map(String::trim)
.distinct()
.collect(Collectors.toList()); .collect(Collectors.toList());
long start = System.currentTimeMillis(); long start = System.currentTimeMillis();
@@ -122,8 +122,8 @@ public class TrademarkController {
} }
long t = (System.currentTimeMillis() - start) / 1000; long t = (System.currentTimeMillis() - start) / 1000;
int checkedCount = allResults.size(); int checkedCount = list.size();
int failedCount = list.size() - checkedCount; int failedCount = 0;
Map<String, Object> res = new HashMap<>(); Map<String, Object> res = new HashMap<>();
res.put("total", list.size()); res.put("total", list.size());

View File

@@ -163,8 +163,17 @@ public class FangzhouApiServiceImpl implements IFangzhouApiService {
for (int i = 0; i < maxRetries; i++) { for (int i = 0; i < maxRetries; i++) {
JsonNode json = callApi("TaskPageList", data, token); JsonNode json = callApi("TaskPageList", data, token);
JsonNode dNode = json.get("D").get("items").get(0); JsonNode dNode = json.get("D").get("items").get(0);
String downloadUrl = dNode.get("download_url").asText();
// 检查任务状态
int state = dNode.get("state").asInt();
if (state == -1) {
String remark = dNode.has("remark") ? dNode.get("remark").asText() : "任务处理失败";
logger.error("任务处理失败: {}", remark);
throw new RuntimeException(remark);
}
// 检查下载链接
String downloadUrl = dNode.get("download_url").asText();
if (downloadUrl != null && !downloadUrl.isEmpty()) { if (downloadUrl != null && !downloadUrl.isEmpty()) {
return dNode; return dNode;
} }

View File

@@ -57,6 +57,8 @@ public class MarkServiceImpl implements IMarkService {
accountData.put("password", password); accountData.put("password", password);
accountData.put("token", token); accountData.put("token", token);
redisCache.setCacheMap(CacheConstants.MARK_ACCOUNT_KEY, accountData); redisCache.setCacheMap(CacheConstants.MARK_ACCOUNT_KEY, accountData);
// 设置 6 天后过期
redisCache.expire(CacheConstants.MARK_ACCOUNT_KEY, 6, java.util.concurrent.TimeUnit.DAYS);
return token; return token;
} }
throw new RuntimeException("注册失败: " + json.get("M").asText()); throw new RuntimeException("注册失败: " + json.get("M").asText());
@@ -84,7 +86,10 @@ public class MarkServiceImpl implements IMarkService {
String result = restTemplate.postForObject("https://api.fangzhoujingxuan.com/App", requestEntity, String.class); String result = restTemplate.postForObject("https://api.fangzhoujingxuan.com/App", requestEntity, String.class);
JsonNode json = objectMapper.readTree( result); JsonNode json = objectMapper.readTree( result);
String token = json.get("D").get("key").asText(); String token = json.get("D").get("key").asText();
if( token !=null)redisCache.setCacheMapValue(CacheConstants.MARK_ACCOUNT_KEY, "token",token); if( token !=null){
redisCache.setCacheMapValue(CacheConstants.MARK_ACCOUNT_KEY, "token",token);
redisCache.expire(CacheConstants.MARK_ACCOUNT_KEY, 6, java.util.concurrent.TimeUnit.DAYS);
}
return token; return token;
} catch (JsonProcessingException e) { } catch (JsonProcessingException e) {
throw new RuntimeException(e); throw new RuntimeException(e);