feat(trademark): 实现商标筛查功能并优化相关配置

- 新增商标筛查进度展示界面与交互逻辑
- 实现产品、品牌及平台跟卖许可的分项任务进度追踪
- 添加商标数据导出与任务重试、取消功能
- 调整Redis连接池配置以提升并发性能
- 禁用ChromeDriver预加载,改为按需启动以节省资源- 支持品牌商标远程筛查接口调用与结果解析
- 增加Hutool工具库依赖用于简化IO与Excel处理- 更新USPTO商标查询脚本实现自动化检测
- 修改Ruoyi后台Redis依赖版本并添加集群心跳配置- 切换本地开发环境API地址指向内网测试服务器
This commit is contained in:
2025-11-04 15:39:15 +08:00
parent c9874f1786
commit a62d7b6147
22 changed files with 2063 additions and 168 deletions

View File

@@ -211,7 +211,7 @@ function startSpringBoot() {
}
}
startSpringBoot();
// startSpringBoot();
function stopSpringBoot() {
if (!springProcess) return;
try {
@@ -348,9 +348,9 @@ app.whenReady().then(() => {
}
}
// setTimeout(() => {
// openAppIfNotOpened();
// }, 100);
setTimeout(() => {
openAppIfNotOpened();
}, 100);
app.on('activate', () => {
if (mainWindow && !mainWindow.isDestroyed()) {

View File

@@ -1,13 +1,13 @@
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')) {
if (path.startsWith('/monitor/') || path.startsWith('/system/') || path.startsWith('/tool/banma') || path.startsWith('/tool/genmai') || path.startsWith('/tool/mark')) {
return CONFIG.RUOYI_BASE;
}
return CONFIG.CLIENT_BASE;

View File

@@ -0,0 +1,21 @@
import { http } from './http'
export const markApi = {
// 新建任务
newTask(file: File) {
const formData = new FormData()
formData.append('file', file)
return http.upload<{ code: number, data: any, msg: string }>('/tool/mark/newTask', formData)
},
// 获取任务列表及筛选数据
getTask() {
return http.get<{ code: number, data: { original: any, filtered: any[] }, msg: string }>('/tool/mark/task')
},
// 品牌商标筛查
brandCheck(brands: string[]) {
return http.post<{ code: number, data: { total: number, filtered: number, passed: number, data: any[] }, msg: string }>('/tool/mark/brandCheck', brands)
}
}

View File

@@ -62,6 +62,26 @@ function handleSizeChange(size: number) {
function handleCurrentChange(page: number) {
currentPage.value = page
}
function handleNewTask() {
if (trademarkPanelRef.value && trademarkPanelRef.value.resetToIdle) {
trademarkPanelRef.value.resetToIdle()
}
trademarkData.value = []
currentPage.value = 1
}
function handleCancelTask() {
if (trademarkPanelRef.value && typeof trademarkPanelRef.value.stopTrademarkQuery === 'function') {
trademarkPanelRef.value.stopTrademarkQuery()
}
}
function handleRetryTask() {
if (trademarkPanelRef.value && typeof trademarkPanelRef.value.startTrademarkQuery === 'function') {
trademarkPanelRef.value.startTrademarkQuery()
}
}
</script>
<template>
@@ -84,8 +104,8 @@ function handleCurrentChange(page: number) {
</div>
<div class="body-layout">
<!-- 左侧步骤栏 -->
<aside :class="['steps-sidebar', currentTab === 'trademark' ? 'wide' : 'narrow']">
<!-- 左侧步骤栏 (商标筛查有数据时隐藏) -->
<aside v-show="!(currentTab === 'trademark' && (trademarkPanelRef?.queryStatus === 'inProgress' || trademarkPanelRef?.queryStatus === 'done' || trademarkPanelRef?.queryStatus === 'error' || trademarkPanelRef?.queryStatus === 'networkError'))" :class="['steps-sidebar', currentTab === 'trademark' ? 'wide' : 'narrow']">
<div class="steps-title">操作流程</div>
<!-- ASIN查询面板 -->
@@ -180,48 +200,168 @@ function handleCurrentChange(page: number) {
</tbody>
</table>
<!-- 商标筛查表格 -->
<table v-if="currentTab === 'trademark'" class="table table-trademark">
<thead>
<tr>
<th>商标名称</th>
<th>状态</th>
<th>类别</th>
<th>权利人</th>
<th>到期日期</th>
<th>相似度</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, idx) in paginatedData" :key="idx">
<td><span class="trademark-name">{{ row.name }}</span></td>
<td>
<span :class="{
'status-registered': row.status === '已注册',
'status-risk': row.status === '近似风险',
'status-available': row.status === '可注册'
}">{{ row.status }}</span>
</td>
<td>{{ row.class }}</td>
<td class="truncate-cell">{{ row.owner }}</td>
<td>{{ row.expireDate }}</td>
<td>
<span v-if="row.similarity > 0" :class="{ 'similarity-high': row.similarity >= 80 }">
{{ row.similarity }}%
</span>
<span v-else>-</span>
</td>
</tr>
</tbody>
</table>
<!-- 商标筛查进度显示 -->
<div v-if="currentTab === 'trademark' && (trademarkPanelRef?.queryStatus === 'inProgress' || trademarkPanelRef?.queryStatus === 'done' || trademarkPanelRef?.queryStatus === 'error' || trademarkPanelRef?.queryStatus === 'networkError')" class="trademark-progress-container">
<!-- 进行中状态横幅 -->
<div v-if="trademarkPanelRef?.queryStatus === 'inProgress'" class="status-banner progress-banner">
<div class="banner-icon">
<img src="/icon/wait.png" alt="筛查中" class="icon-image" />
</div>
<div v-if="paginatedData.length === 0" class="empty-abs">
<div class="banner-content">
<div class="banner-title">数据筛查中...</div>
<div class="banner-desc">为确保结果准确请耐心等待保持后台运行...</div>
</div>
<div class="banner-actions">
<el-button size="default" @click="handleCancelTask">取消</el-button>
</div>
</div>
<!-- 完成/失败状态横幅样式统一 -->
<div v-if="trademarkPanelRef?.queryStatus === 'done' || trademarkPanelRef?.queryStatus === 'error' || trademarkPanelRef?.queryStatus === 'networkError'" class="status-banner done-banner">
<div class="banner-icon">
<img src="/icon/done1.png" alt="完成" class="icon-image" />
</div>
<div class="banner-content">
<div class="banner-title">筛查已完成</div>
<div class="banner-desc">点击"导出数据"按钮可导出为 Excel 表格文件如出现异常请检查账号地区设置</div>
</div>
<div class="banner-actions">
<el-button size="default" @click="handleNewTask">新建任务</el-button>
<el-button type="primary" size="default">导出数据</el-button>
</div>
</div>
<!-- 统一任务卡片容器 -->
<div class="unified-task-card">
<!-- 三个任务区块 -->
<div class="task-section">
<!-- 左侧状态列 -->
<div class="status-column">
<!-- 任务1产品商标筛查 -->
<div class="status-item">
<img v-if="trademarkPanelRef?.queryStatus === 'error' || trademarkPanelRef?.queryStatus === 'networkError'" src="/icon/error.png" alt="筛查失败" class="status-indicator-icon" />
<img v-else-if="(trademarkPanelRef?.taskProgress?.product?.current || 0) >= (trademarkPanelRef?.taskProgress?.product?.total || 1) && (trademarkPanelRef?.taskProgress?.product?.total || 0) > 0" src="/icon/done.png" alt="采集完成" class="status-indicator-icon" />
<img v-else-if="trademarkPanelRef?.queryStatus === 'inProgress'" src="/icon/inProgress.png" alt="采集中" class="status-indicator-icon" />
<img v-else src="/icon/acquisition.png" alt="待采集" class="status-indicator-icon" />
</div>
<div class="status-connector"></div>
<!-- 任务2品牌商标筛查 -->
<div class="status-item">
<img v-if="(trademarkPanelRef?.taskProgress?.brand?.current || 0) >= (trademarkPanelRef?.taskProgress?.brand?.total || 1)" src="/icon/done.png" alt="采集完成" class="status-indicator-icon" />
<img v-else-if="(trademarkPanelRef?.taskProgress?.brand?.current || 0) > 0" src="/icon/inProgress.png" alt="采集中" class="status-indicator-icon" />
<img v-else src="/icon/acquisition.png" alt="待采集" class="status-indicator-icon" />
</div>
<div class="status-connector"></div>
<!-- 任务3跟卖许可筛查 -->
<div class="status-item">
<img v-if="(trademarkPanelRef?.taskProgress?.platform?.current || 0) >= (trademarkPanelRef?.taskProgress?.platform?.total || 1)" src="/icon/done.png" alt="采集完成" class="status-indicator-icon" />
<img v-else-if="(trademarkPanelRef?.taskProgress?.platform?.current || 0) > 0" src="/icon/inProgress.png" alt="采集中" class="status-indicator-icon" />
<img v-else src="/icon/acquisition.png" alt="待采集" class="status-indicator-icon" />
</div>
</div>
<!-- 任务内容列 -->
<div class="tasks-content">
<div class="task-item">
<div class="task-title-row">
<div class="task-info">
<div class="task-name">{{ trademarkPanelRef?.taskProgress?.product?.label || '未注册/TM商标筛查' }}</div>
<div class="task-desc">{{ trademarkPanelRef?.taskProgress?.product?.desc || '筛查未注册商标或TM标的产品' }}<span v-if="trademarkPanelRef?.queryStatus !== 'inProgress'"> (已完成)</span></div>
</div>
</div>
<div class="task-progress-wrapper">
<div class="task-progress-bar">
<div class="task-progress-fill" :style="{ width: ((trademarkPanelRef?.taskProgress?.product?.total || 0) > 0 ? ((trademarkPanelRef?.taskProgress?.product?.current || 0) / trademarkPanelRef.taskProgress.product.total * 100) : 0) + '%' }"></div>
</div>
<div class="task-percentage">{{ (trademarkPanelRef?.taskProgress?.product?.total || 0) > 0 ? Math.round(((trademarkPanelRef?.taskProgress?.product?.current || 0) / trademarkPanelRef.taskProgress.product.total) * 100) : 0 }}%</div>
</div>
<div class="task-stats">
<div class="task-stat">
<span class="stat-label">查询数量</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) > 0 ? (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) > 0 ? ((trademarkPanelRef?.taskProgress?.product?.total || 0) - (trademarkPanelRef?.taskProgress?.product?.completed || 0)) : '-' }}</span>
</div>
</div>
</div>
<div class="task-item">
<div class="task-title-row">
<div class="task-info">
<div class="task-name">{{ trademarkPanelRef?.taskProgress?.brand?.label || '品牌商标筛查' }}</div>
<div class="task-desc">{{ trademarkPanelRef?.taskProgress?.brand?.desc || '筛查未注册商标的品牌' }}<span v-if="trademarkPanelRef?.queryStatus !== 'inProgress'"> (已完成)</span></div>
</div>
</div>
<div class="task-progress-wrapper">
<div class="task-progress-bar">
<div class="task-progress-fill" :style="{ width: ((trademarkPanelRef?.taskProgress?.brand?.total || 0) > 0 ? ((trademarkPanelRef?.taskProgress?.brand?.current || 0) / trademarkPanelRef.taskProgress.brand.total * 100) : 0) + '%' }"></div>
</div>
<div class="task-percentage">{{ (trademarkPanelRef?.taskProgress?.brand?.total || 0) > 0 ? Math.round(((trademarkPanelRef?.taskProgress?.brand?.current || 0) / trademarkPanelRef.taskProgress.brand.total) * 100) : 0 }}%</div>
</div>
<div class="task-stats">
<div class="task-stat">
<span class="stat-label">查询数量</span>
<span class="stat-value">{{ (trademarkPanelRef?.taskProgress?.brand?.total || 0) === 0 ? '-' : trademarkPanelRef.taskProgress.brand.total }}</span>
</div>
<div class="task-stat highlight">
<span class="stat-label">未注册</span>
<span class="stat-value">{{ (trademarkPanelRef?.taskProgress?.brand?.total || 0) > 0 ? (trademarkPanelRef?.taskProgress?.brand?.completed || 0) : '-' }}</span>
</div>
<div class="task-stat">
<span class="stat-label">已注册</span>
<span class="stat-value">{{ (trademarkPanelRef?.taskProgress?.brand?.total || 0) > 0 ? ((trademarkPanelRef?.taskProgress?.brand?.total || 0) - (trademarkPanelRef?.taskProgress?.brand?.completed || 0)) : '-' }}</span>
</div>
</div>
</div>
<div class="task-item">
<div class="task-title-row">
<div class="task-info">
<div class="task-name">{{ trademarkPanelRef?.taskProgress?.platform?.label || '亚马逊跟卖许可筛查' }}</div>
<div class="task-desc">{{ trademarkPanelRef?.taskProgress?.platform?.desc || '筛查亚马逊许可跟卖的商品' }}<span v-if="trademarkPanelRef?.queryStatus !== 'inProgress'"> (未开始)</span></div>
</div>
</div>
<div class="task-progress-wrapper">
<div class="task-progress-bar">
<div class="task-progress-fill" :style="{ width: ((trademarkPanelRef?.taskProgress?.platform?.total || 0) > 0 ? ((trademarkPanelRef?.taskProgress?.platform?.current || 0) / trademarkPanelRef.taskProgress.platform.total * 100) : 0) + '%' }"></div>
</div>
<div class="task-percentage">{{ (trademarkPanelRef?.taskProgress?.platform?.total || 0) > 0 ? Math.round(((trademarkPanelRef?.taskProgress?.platform?.current || 0) / trademarkPanelRef.taskProgress.platform.total) * 100) : 0 }}%</div>
</div>
<div class="task-stats">
<div class="task-stat">
<span class="stat-label">查询数量</span>
<span class="stat-value">{{ (trademarkPanelRef?.taskProgress?.platform?.total || 0) === 0 ? '-' : trademarkPanelRef.taskProgress.platform.total }}</span>
</div>
<div class="task-stat highlight">
<span class="stat-label">可跟卖</span>
<span class="stat-value">{{ (trademarkPanelRef?.taskProgress?.platform?.total || 0) > 0 ? (trademarkPanelRef?.taskProgress?.platform?.completed || 0) : '-' }}</span>
</div>
<div class="task-stat">
<span class="stat-label">已过滤</span>
<span class="stat-value">{{ (trademarkPanelRef?.taskProgress?.platform?.total || 0) > 0 ? ((trademarkPanelRef?.taskProgress?.platform?.total || 0) - (trademarkPanelRef?.taskProgress?.platform?.completed || 0)) : '-' }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div v-if="paginatedData.length === 0 && !(currentTab === 'trademark' && (trademarkPanelRef?.queryStatus === 'inProgress' || trademarkPanelRef?.queryStatus === 'done' || trademarkPanelRef?.queryStatus === 'error' || trademarkPanelRef?.queryStatus === 'networkError'))" class="empty-abs">
<!-- 商标筛查状态显示 -->
<div v-if="currentTab === 'trademark' && trademarkPanelRef?.queryStatus" class="empty-container">
<div v-if="currentTab === 'trademark' && trademarkPanelRef?.queryStatus && trademarkPanelRef.queryStatus !== 'inProgress' && trademarkPanelRef.queryStatus !== 'done' && trademarkPanelRef.queryStatus !== 'error' && trademarkPanelRef.queryStatus !== 'networkError'" class="empty-container">
<img
:src="trademarkPanelRef.statusConfig[trademarkPanelRef.queryStatus].icon"
:alt="trademarkPanelRef.statusConfig[trademarkPanelRef.queryStatus].title"
:class="['status-image', { 'status-loading': trademarkPanelRef.queryStatus === 'inProgress' }]"
class="status-image"
/>
<div class="empty-title">{{ trademarkPanelRef.statusConfig[trademarkPanelRef.queryStatus].title }}</div>
<div v-if="trademarkPanelRef.statusConfig[trademarkPanelRef.queryStatus].desc" class="empty-desc">
@@ -240,7 +380,7 @@ function handleCurrentChange(page: number) {
</div>
</div>
</div>
<div v-if="currentTab !== 'genmai'" class="pagination-fixed">
<div v-if="currentTab !== 'genmai' && currentTab !== 'trademark'" class="pagination-fixed">
<el-pagination
:current-page="currentPage"
:page-sizes="[15,30,50,100]"
@@ -618,6 +758,253 @@ function handleCurrentChange(page: number) {
text-decoration: underline;
}
/* 商标筛查进度样式 */
.trademark-progress-container {
padding: 24px;
width: 100%;
height: 100%;
box-sizing: border-box;
overflow-y: auto;
display: flex;
flex-direction: column;
align-items: center;
}
/* 状态横幅 */
.status-banner {
box-sizing: border-box;
display: flex;
flex-direction: row;
align-items: center;
padding: 16px;
gap: 16px;
width: 70%;
max-width: 900px;
min-height: 90px;
background: #FFFFFF;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 16px;
flex: none;
margin-bottom: 16px;
}
.status-banner.progress-banner {
box-shadow: 0px 8px 20px rgba(22, 119, 255, 0.2);
}
.status-banner.done-banner {
box-shadow: 0px 8px 20px rgba(103, 194, 58, 0.2);
}
.status-banner.error-banner {
box-shadow: 0px 8px 20px rgba(245, 108, 108, 0.2);
}
.banner-icon {
flex-shrink: 0;
width: 64px;
height: 64px;
display: flex;
align-items: center;
justify-content: center;
background: transparent;
border-radius: 50%;
}
.done-banner .banner-icon {
background: transparent;
}
.icon-image {
width: 48px;
height: 48px;
filter: none;
}
.banner-content {
flex: 1;
min-width: 0;
text-align: left;
}
.banner-title {
font-size: 16px;
font-weight: 600;
color: #303133;
margin-bottom: 4px;
}
.banner-desc {
font-size: 13px;
color: #606266;
line-height: 1.5;
}
.banner-actions {
flex-shrink: 0;
display: flex;
gap: 12px;
}
.banner-actions .el-button {
min-width: 100px;
}
/* 统一任务卡片容器 */
.unified-task-card {
width: 70%;
max-width: 900px;
background: #FFFFFF;
border: 1px solid rgba(0, 0, 0, 0.06);
border-radius: 12px;
box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.08);
overflow: hidden;
}
/* 任务区块 */
.task-section {
display: flex;
flex-direction: row;
padding: 20px;
gap: 20px;
}
/* 左侧状态列 */
.status-column {
display: flex;
flex-direction: column;
align-items: center;
gap: 0;
flex-shrink: 0;
}
.status-item {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
flex-shrink: 0;
}
.status-indicator-icon {
width: 24px;
height: 24px;
object-fit: contain;
}
.status-indicator-icon[src*="inProgress"] {
animation: spin 1.5s linear infinite;
}
.status-connector {
width: 2px;
flex: 1;
background: #e5e7eb;
margin: 6px 0;
min-height: 50px;
}
/* 任务内容列 */
.tasks-content {
flex: 1;
display: flex;
flex-direction: column;
gap: 16px;
min-width: 0;
}
.task-item {
width: 100%;
padding: 0;
padding-bottom: 16px;
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
}
.task-item:first-child {
padding-top: 0;
}
.task-item:last-child {
padding-bottom: 0;
border-bottom: none;
}
/* 竖线分隔(换行后不需要) */
.vertical-divider {
display: none;
}
/* 任务标题行 */
.task-title-row {
display: flex;
align-items: flex-start;
margin-bottom: 12px;
}
.task-info {
flex: 1;
min-width: 0;
text-align: left;
}
.task-name {
font-size: 13px;
font-weight: 600;
color: #303133;
margin-bottom: 2px;
line-height: 1.4;
}
.task-desc {
font-size: 11px;
color: #909399;
line-height: 1.4;
}
/* 进度条 */
.task-progress-wrapper {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 10px;
}
.task-progress-bar {
flex: 1;
height: 4px;
background: rgba(22, 119, 255, 0.1);
border-radius: 999px;
overflow: hidden;
}
.task-progress-fill {
height: 100%;
background: linear-gradient(90deg, #1677FF 0%, #4096FF 100%);
border-radius: 999px;
transition: width 0.3s ease;
}
.task-percentage {
flex-shrink: 0;
font-size: 13px;
font-weight: 600;
color: #1677FF;
min-width: 40px;
text-align: right;
}
/* 统计数据 */
.task-stats {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
text-align: left;
}
.task-stat {
flex: 1;
display: flex;
flex-direction: column;
gap: 2px;
}
.task-stat.highlight {
border-left: 1px solid rgba(0, 0, 0, 0.06);
border-right: 1px solid rgba(0, 0, 0, 0.06);
padding: 0 8px;
}
.stat-label {
font-size: 10px;
color: #909399;
}
.stat-value {
font-size: 16px;
font-weight: 600;
color: #303133;
}
.task-stat.highlight .stat-label {
color: #67c23a;
}
.task-stat.highlight .stat-value {
color: #67c23a;
}
/* 响应式设计 */
@media (max-width: 1200px) {
.steps-sidebar.narrow {
@@ -640,6 +1027,10 @@ function handleCurrentChange(page: number) {
.genmai-info-boxes {
width: 95%;
}
.status-banner,
.unified-task-card {
width: 85%;
}
}
@media (max-width: 768px) {
@@ -655,6 +1046,35 @@ function handleCurrentChange(page: number) {
width: 100px;
font-size: 13px;
}
.status-banner,
.unified-task-card {
width: 95%;
}
.status-banner {
min-height: auto;
padding: 14px;
}
.banner-icon {
width: 40px;
height: 40px;
}
.icon-image {
width: 24px;
height: 24px;
}
.banner-title {
font-size: 14px;
}
.banner-desc {
font-size: 12px;
}
.banner-actions {
flex-direction: column;
width: 100%;
}
.banner-actions .el-button {
width: 100%;
}
}
</style>

View File

@@ -3,6 +3,7 @@ import { ref, inject, defineAsyncComponent } from 'vue'
import { ElMessage } from 'element-plus'
import { handlePlatformFileExport } from '../../utils/settings'
import { getUsernameFromToken } from '../../utils/token'
import { markApi } from '../../api/mark'
const TrialExpiredDialog = defineAsyncComponent(() => import('../common/TrialExpiredDialog.vue'))
const refreshVipStatus = inject<() => Promise<boolean>>('refreshVipStatus')
@@ -17,12 +18,21 @@ const emit = defineEmits<{
const trademarkData = ref<any[]>([])
const trademarkFileName = ref('')
const trademarkFile = ref<File | null>(null)
const trademarkUpload = ref<HTMLInputElement | null>(null)
const trademarkLoading = ref(false)
const trademarkProgress = ref(0)
const exportLoading = ref(false)
const currentStep = ref(0)
const totalSteps = ref(0)
let brandProgressTimer: any = null
// 三个任务的进度数据
const taskProgress = ref({
product: { total: 0, current: 0, completed: 0, label: '产品商标筛查', desc: '筛查未注册商标或TM标的商品' },
brand: { total: 0, current: 0, completed: 0, label: '品牌商标筛查', desc: '筛查未注册商标的品牌' },
platform: { total: 0, current: 0, completed: 0, label: '跟卖许可筛查', desc: '筛查亚马逊许可跟卖的商品' }
})
// 查询状态: idle-待开始, inProgress-进行中, done-已完成, error-错误, networkError-网络错误, cancel-已取消
const queryStatus = ref<'idle' | 'inProgress' | 'done' | 'error' | 'networkError' | 'cancel'>('idle')
@@ -39,14 +49,14 @@ const statusConfig = {
desc: '请耐心等待,正在为您筛查商标信息'
},
done: {
icon: '/icon/done.png',
icon: '/icon/done1.png',
title: '筛查完成',
desc: '已成功完成所有商标筛查任务'
},
error: {
icon: '/icon/error.png',
title: '查失败',
desc: '查过程中出现错误,请重试'
title: '查失败',
desc: '查过程中出现错误,请重新尝试'
},
networkError: {
icon: '/icon/networkErrors.png',
@@ -61,19 +71,15 @@ const statusConfig = {
}
// 选择的账号(模拟数据)
const selectedAccount = ref('chensri3.com')
const selectedAccount = ref(1)
const accountOptions = [
{ label: 'chensri3.com', value: 'chensri3.com', status: '美国' },
{ label: 'zhangikcpi.com', value: 'zhangikcpi.com', status: '加拿大' },
{ label: 'hhhhsoutlook...', value: 'hhhhsoutlook', status: '日本' }
{ id: 1, name: 'chensri3.com', username: 'chensri3.com', status: 1 }
]
// 地区选择
const region = ref('美国')
const regionOptions = [
{ label: '美国', value: '美国', flag: '🇺🇸' },
{ label: '日本', value: '日本', flag: '🇯🇵' },
{ label: '加拿大', value: '加拿大', flag: '🇨🇦' }
{ label: '美国', value: '美国', flag: '🇺🇸' }
]
// 查询类型多选
@@ -103,6 +109,7 @@ async function handleTrademarkUpload(e: Event) {
}
trademarkFileName.value = file.name
trademarkFile.value = file
queryStatus.value = 'idle' // 重置状态
trademarkData.value = []
emit('updateData', [])
@@ -111,7 +118,7 @@ async function handleTrademarkUpload(e: Event) {
}
async function startTrademarkQuery() {
if (!trademarkFileName.value) {
if (!trademarkFileName.value || !trademarkFile.value) {
showMessage('请先导入商标列表', 'warning')
return
}
@@ -129,30 +136,231 @@ async function startTrademarkQuery() {
trademarkData.value = []
queryStatus.value = 'inProgress'
const mockData = [
{ name: 'SONY', status: '已注册', class: '9类-电子产品', owner: 'Sony Corporation', expireDate: '2028-12-31', similarity: 0 },
{ name: 'SUMSUNG', status: '近似风险', class: '9类-电子产品', owner: '待查询', expireDate: '-', similarity: 85 },
{ name: 'NIKE', status: '已注册', class: '25类-服装鞋帽', owner: 'Nike, Inc.', expireDate: '2029-06-15', similarity: 0 },
{ name: 'ADIBAS', status: '近似风险', class: '25类-服装鞋帽', owner: '待查询', expireDate: '-', similarity: 92 },
{ name: 'MyBrand', status: '可注册', class: '未查询', owner: '-', expireDate: '-', similarity: 0 }
]
// 完全重置任务进度包括total
taskProgress.value.product.total = 0
taskProgress.value.product.current = 0
taskProgress.value.product.completed = 0
taskProgress.value.brand.total = 0
taskProgress.value.brand.current = 0
taskProgress.value.brand.completed = 0
taskProgress.value.platform.total = 0
taskProgress.value.platform.current = 0
taskProgress.value.platform.completed = 0
totalSteps.value = mockData.length
// 通知父组件更新数据
emit('updateData', [])
try {
for (let i = 0; i < mockData.length; i++) {
if (!trademarkLoading.value) break // 被取消
let productResult: any = null
let brandList: string[] = []
// 判断是否需要执行产品商标筛查
const needProductCheck = queryTypes.value.includes('product')
const needBrandCheck = queryTypes.value.includes('brand')
if (needProductCheck) {
// 步骤1: 产品商标筛查 - 调用新建任务接口
showMessage('正在上传文件...', 'info')
const createResult = await markApi.newTask(trademarkFile.value)
currentStep.value = i + 1
await new Promise(resolve => setTimeout(resolve, 800))
trademarkData.value.push(mockData[i])
trademarkProgress.value = Math.round(((i + 1) / mockData.length) * 100)
if (createResult.code !== 200 && createResult.code !== 0) {
throw new Error(createResult.msg || '创建任务失败')
}
showMessage('文件上传成功,正在处理...', 'success')
// 步骤2: 轮询检查任务状态最多等待60秒
const taskData = taskProgress.value.product
const maxWaitTime = 60000 // 最多等60秒
const pollInterval = 3000 // 每3秒检查一次
const startTime = Date.now()
let taskResult: any = null
while (Date.now() - startTime < maxWaitTime) {
if (!trademarkLoading.value) return
// 等待一段时间
await new Promise(resolve => setTimeout(resolve, pollInterval))
if (!trademarkLoading.value) return
// 尝试获取任务结果
try {
taskResult = await markApi.getTask()
if (taskResult.code === 200 || taskResult.code === 0) {
// 检查是否有 download_url有则说明任务完成
if (taskResult.data.original?.download_url) {
break
}
}
} catch (err) {
// 继续等待
console.log('等待任务处理中...', err)
}
}
if (!trademarkLoading.value) return
// 步骤3: 检查是否成功获取到结果
if (!taskResult || (taskResult.code !== 200 && taskResult.code !== 0)) {
throw new Error('获取任务超时或失败,请重试')
}
if (!taskResult.data.original?.download_url) {
throw new Error('任务处理超时,请稍后重试')
}
productResult = taskResult
// 从后端获取真实的统计数据
taskData.total = taskResult.data.original?.total || 0
taskData.completed = taskResult.data.filtered.length
taskData.current = taskData.total
// 映射后端数据到前端格式
trademarkData.value = taskResult.data.filtered.map((item: any) => ({
name: item['品牌'] || '',
status: item['商标类型'] || '',
class: '',
owner: '',
expireDate: item['注册时间'] || '',
similarity: 0,
// 保留原始数据供后续使用
asin: item['ASIN'],
productImage: item['商品主图']
}))
// 如果需要品牌筛查,从产品结果中提取品牌列表
if (needBrandCheck) {
brandList = taskResult.data.filtered
.map((item: any) => item['品牌'])
.filter((brand: string) => brand && brand.trim())
}
showMessage(`产品筛查完成,共查询 ${taskData.total} 条,筛查出 ${taskData.completed} 条未注册/TM标`, 'success')
}
// 品牌商标筛查
if (needBrandCheck) {
if (!trademarkLoading.value) return
// 如果没有执行产品筛查,需要先上传文件获取品牌列表
if (!needProductCheck) {
showMessage('正在上传文件提取品牌列表...', 'info')
// 调用新建任务接口获取处理后的数据
const createResult = await markApi.newTask(trademarkFile.value)
if (createResult.code !== 200 && createResult.code !== 0) {
throw new Error(createResult.msg || '创建任务失败')
}
// 轮询获取任务结果
const maxWaitTime = 60000
const pollInterval = 3000
const startTime = Date.now()
let taskResult: any = null
while (Date.now() - startTime < maxWaitTime) {
if (!trademarkLoading.value) return
await new Promise(resolve => setTimeout(resolve, pollInterval))
if (!trademarkLoading.value) return
try {
taskResult = await markApi.getTask()
if (taskResult.code === 200 || taskResult.code === 0) {
if (taskResult.data.original?.download_url) {
break
}
}
} catch (err) {
console.log('等待任务处理中...', err)
}
}
if (!trademarkLoading.value) return
if (!taskResult || (taskResult.code !== 200 && taskResult.code !== 0)) {
throw new Error('获取任务超时或失败,请重试')
}
if (!taskResult.data.original?.download_url) {
throw new Error('任务处理超时,请稍后重试')
}
// 从结果中提取品牌列表包括TM和未注册的品牌
brandList = taskResult.data.filtered
.map((item: any) => item['品牌'])
.filter((brand: string) => brand && brand.trim())
showMessage(`品牌列表提取成功,共 ${brandList.length} 个品牌`, 'success')
}
if (brandList.length === 0) {
showMessage('没有品牌需要筛查', 'warning')
} else {
const brandData = taskProgress.value.brand
brandData.total = brandList.length
brandData.current = 0
brandData.completed = 0
showMessage(`开始品牌商标筛查,共 ${brandList.length} 个品牌...`, 'info')
// 模拟进度动画每秒增加20个
const batchSize = 20
brandProgressTimer = setInterval(() => {
if (brandData.current < brandList.length * 0.95) {
brandData.current = Math.min(brandData.current + batchSize, brandList.length * 0.95)
}
}, 1000)
// 调用品牌筛查接口(浏览器内并发,速度快)
const brandResult = await markApi.brandCheck(brandList)
if (brandProgressTimer) clearInterval(brandProgressTimer)
if (!trademarkLoading.value) return
if (brandResult.code === 200 || brandResult.code === 0) {
// 完成显示100%
brandData.current = brandData.total
brandData.completed = brandResult.data.filtered
// 将品牌筛查结果追加到展示数据中
const brandItems = brandResult.data.data.map((item: any) => ({
name: item.brand || '',
status: item.status || '未注册',
class: '',
owner: '',
expireDate: '',
similarity: 0,
isBrand: true // 标记为品牌数据
}))
trademarkData.value = [...trademarkData.value, ...brandItems]
showMessage(`品牌筛查完成,共查询 ${brandData.total} 个品牌,筛查出 ${brandData.completed} 个未注册品牌`, 'success')
} else {
throw new Error(brandResult.msg || '品牌筛查失败')
}
}
}
if (trademarkLoading.value) {
queryStatus.value = 'done'
emit('updateData', trademarkData.value)
showMessage('商标筛查完成', 'success')
let summaryMsg = '筛查完成'
if (needProductCheck) {
const taskData = taskProgress.value.product
summaryMsg += `,产品:${taskData.completed}/${taskData.total}`
}
if (needBrandCheck && brandList.length > 0) {
const brandData = taskProgress.value.brand
summaryMsg += `,品牌:${brandData.completed}/${brandData.total}`
}
showMessage(summaryMsg, 'success')
}
} catch (error: any) {
if (error.message && error.message.includes('网络')) {
@@ -163,7 +371,16 @@ async function startTrademarkQuery() {
errorMessage.value = error.message || '查询失败'
}
showMessage(errorMessage.value, 'error')
// 失败时清空数据,不显示侧边栏
trademarkData.value = []
emit('updateData', [])
} finally {
// 清除定时器
if (brandProgressTimer) {
clearInterval(brandProgressTimer)
brandProgressTimer = null
}
trademarkLoading.value = false
currentStep.value = 0
totalSteps.value = 0
@@ -171,6 +388,11 @@ async function startTrademarkQuery() {
}
function stopTrademarkQuery() {
// 清除进度动画定时器
if (brandProgressTimer) {
clearInterval(brandProgressTimer)
brandProgressTimer = null
}
trademarkLoading.value = false
queryStatus.value = 'cancel'
currentStep.value = 0
@@ -239,12 +461,28 @@ function downloadTrademarkTemplate() {
URL.revokeObjectURL(url)
}
function resetToIdle() {
queryStatus.value = 'idle'
trademarkData.value = []
trademarkFileName.value = ''
trademarkFile.value = null
taskProgress.value.product.current = 0
taskProgress.value.product.completed = 0
taskProgress.value.brand.current = 0
taskProgress.value.brand.completed = 0
taskProgress.value.platform.current = 0
taskProgress.value.platform.completed = 0
}
defineExpose({
trademarkLoading,
trademarkProgress,
trademarkData,
queryStatus,
statusConfig
statusConfig,
taskProgress,
resetToIdle,
stopTrademarkQuery
})
</script>
@@ -278,19 +516,26 @@ defineExpose({
<div class="step-header"><div class="title">选择亚马逊商家账号</div></div>
<div class="desc">请确保账号地区与专利地区一致不一致可能导致结果不准确或失败</div>
<el-select v-model="selectedAccount" placeholder="选择账号" size="small" style="width: 100%">
<el-option v-for="opt in accountOptions" :key="opt.value" :label="opt.label" :value="opt.value">
<div class="account-option">
<span>{{ opt.label }}</span>
<span class="account-status">{{ opt.status }}</span>
<span class="account-check"></span>
<el-scrollbar :class="['account-list', { 'scroll-limit': accountOptions.length > 3 }]">
<div>
<div
v-for="acc in accountOptions"
:key="acc.id"
:class="['acct-item', 'disabled', { selected: selectedAccount === acc.id }]"
>
<span class="acct-row">
<span :class="['status-dot', acc.status === 1 ? 'on' : 'off']"></span>
<img class="avatar" src="/image/user.png" alt="avatar" />
<span class="acct-text">{{ acc.name || acc.username }}</span>
<span v-if="selectedAccount === acc.id" class="acct-check"></span>
</span>
</div>
</el-option>
</el-select>
</div>
</el-scrollbar>
<div class="step-actions btn-row">
<el-button size="small" class="w50">添加账号</el-button>
<el-button size="small" class="w50 btn-blue">账号管理</el-button>
<el-button size="small" class="w50" disabled>添加账号</el-button>
<el-button size="small" class="w50 btn-blue" disabled>账号管理</el-button>
</div>
</div>
</div>
@@ -301,7 +546,7 @@ defineExpose({
<div class="step-card">
<div class="step-header"><div class="title">选择产品品牌地区</div></div>
<div class="desc">暂时仅支持美国品牌商标筛查后续将开放更多地区敬请期待</div>
<el-select v-model="region" placeholder="选择地区" size="small" style="width: 100%">
<el-select v-model="region" placeholder="选择地区" size="small" style="width: 100%" disabled>
<el-option v-for="opt in regionOptions" :key="opt.value" :label="opt.label" :value="opt.value">
<span style="margin-right:6px">{{ opt.flag }}</span>{{ opt.label }}
</el-option>
@@ -337,7 +582,7 @@ defineExpose({
</div>
</div>
<div :class="['query-card', { active: queryTypes.includes('platform') }]" @click="toggleQueryType('platform')">
<div :class="['query-card', 'query-disabled', { active: queryTypes.includes('platform') }]">
<div class="query-check">
<div class="check-icon" v-if="queryTypes.includes('platform')"></div>
</div>
@@ -355,9 +600,12 @@ defineExpose({
<div class="bottom-action">
<div class="action-header">
<span class="step-indicator">{{ trademarkLoading ? currentStep : 1 }}/4</span>
<el-button class="start-btn" type="primary" :disabled="!trademarkFileName || trademarkLoading" :loading="trademarkLoading" @click="startTrademarkQuery">
<el-button v-if="!trademarkLoading" class="start-btn" type="primary" :disabled="!trademarkFileName || queryTypes.length === 0" @click="startTrademarkQuery">
开始筛查
</el-button>
<el-button v-else class="start-btn stop-btn" @click="stopTrademarkQuery">
停止筛查
</el-button>
</div>
</div>
@@ -391,7 +639,7 @@ defineExpose({
.title { font-size: 14px; font-weight: 600; color: #303133; text-align: left; }
.desc { font-size: 12px; color: #909399; margin-bottom: 10px; text-align: left; line-height: 1.5; }
.links { display: flex; align-items: center; gap: 6px; margin-bottom: 8px; }
.link { color: #409EFF; cursor: pointer; font-size: 12px; }
.link { color: #909399; cursor: pointer; font-size: 12px; }
.file-chip {
display: flex;
@@ -402,19 +650,22 @@ defineExpose({
border-radius: 4px;
font-size: 12px;
color: #606266;
margin-top: 6px;
margin-top: 6px;
min-width: 0;
}
.file-chip .dot {
width: 6px;
height: 6px;
background: #409EFF;
border-radius: 50%;
display: inline-block;
flex-shrink: 0;
}
.file-chip .name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
white-space: nowrap;
flex: 1;
min-width: 0;
}
.dropzone {
@@ -459,6 +710,11 @@ defineExpose({
border-color: #1677FF;
background: #f0f9ff;
}
.query-card.query-disabled {
cursor: not-allowed;
opacity: 0.5;
pointer-events: none;
}
.query-check {
width: 16px;
height: 16px;
@@ -468,15 +724,15 @@ defineExpose({
align-items: center;
justify-content: center;
flex-shrink: 0;
background: #ffffff;
background: transparent;
transition: all 0.2s ease;
}
.query-card.active .query-check {
border-color: #1677FF;
background: #1677FF;
background: transparent;
}
.check-icon {
color: #ffffff;
color: #1677FF;
font-size: 10px;
font-weight: bold;
line-height: 1;
@@ -534,32 +790,90 @@ defineExpose({
}
.start-btn {
flex: 1;
height: 38px;
height: 32px;
padding: 0px 16px;
background: #1677FF;
border-color: #1677FF;
font-size: 14px;
font-weight: 500;
border-radius: 6px;
}
.stop-btn {
background: #f56c6c;
border-color: #f56c6c;
}
.stop-btn:hover {
background: #f78989;
border-color: #f78989;
}
.btn-blue { background: #1677FF; border-color: #1677FF; color: #fff; }
.btn-blue:disabled { background: #a6c8ff; border-color: #a6c8ff; color: #fff; }
.account-option {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
.account-list {
height: auto;
background: #fff;
border: 1px solid #ebeef5;
border-radius: 4px;
margin-bottom: 8px;
padding: 4px;
}
.account-status {
margin-left: auto;
font-size: 12px;
color: #909399;
.scroll-limit { max-height: 140px; }
.avatar { width: 18px; height: 18px; border-radius: 50%; }
.acct-row {
display: grid;
grid-template-columns: 6px 18px 1fr auto;
align-items: center;
gap: 6px;
width: 100%;
}
.account-check {
color: #1677FF;
font-size: 14px;
.acct-text {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
min-width: 0;
font-size: 12px;
color: #303133;
}
.status-dot {
width: 6px;
height: 6px;
border-radius: 50%;
display: inline-block;
}
.status-dot.on { background: #22c55e; }
.status-dot.off { background: #f87171; }
.acct-item {
padding: 6px 8px;
border-radius: 6px;
margin-bottom: 4px;
}
.acct-item:last-child {
margin-bottom: 0;
}
.acct-item.selected {
background: #eef5ff;
box-shadow: inset 0 0 0 1px #d6e4ff;
}
.acct-item.disabled {
cursor: not-allowed;
opacity: 0.6;
}
.acct-check {
display: inline-flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
border-radius: 50%;
background: transparent;
color: #111;
font-size: 12px;
}
.account-list :deep(.el-scrollbar__view) {
padding: 0;
}
.account-list::-webkit-scrollbar { width: 0; height: 0; }
.trademark-panel :deep(.el-select),
.trademark-panel :deep(.el-input__wrapper) {

Binary file not shown.

After

Width:  |  Height:  |  Size: 533 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 894 B