feat(trademark): 支持商标筛查任务取消状态及优化错误处理- 新增商标筛查取消状态的UI展示和处理逻辑

-优化错误提示信息,区分网络错误、超时和风控场景
- 改进任务进度计算逻辑,支持更准确的完成状态判断
- 调整品牌提取逻辑,从Excel中直接读取品牌列数据
- 增强后端403错误检测和代理自动切换机制
- 更新前端组件样式和交互逻辑以匹配新状态
-修复部分条件判断逻辑以提升稳定性- 调整文件上传大小限制至50MB以支持更大文件- 优化Excel解析工具类,支持自动识别表头行位置
This commit is contained in:
2025-11-05 10:16:14 +08:00
parent a62d7b6147
commit 4e2ce48934
9 changed files with 264 additions and 179 deletions

View File

@@ -131,12 +131,15 @@ async function startTrademarkQuery() {
return
}
const needProductCheck = queryTypes.value.includes('product')
const needBrandCheck = queryTypes.value.includes('brand')
trademarkLoading.value = true
trademarkProgress.value = 0
trademarkData.value = []
queryStatus.value = 'inProgress'
// 完全重置任务进度包括total
// 重置任务进度
taskProgress.value.product.total = 0
taskProgress.value.product.current = 0
taskProgress.value.product.completed = 0
@@ -154,10 +157,6 @@ async function startTrademarkQuery() {
let productResult: any = null
let brandList: string[] = []
// 判断是否需要执行产品商标筛查
const needProductCheck = queryTypes.value.includes('product')
const needBrandCheck = queryTypes.value.includes('brand')
if (needProductCheck) {
// 步骤1: 产品商标筛查 - 调用新建任务接口
showMessage('正在上传文件...', 'info')
@@ -169,132 +168,113 @@ async function startTrademarkQuery() {
showMessage('文件上传成功,正在处理...', 'success')
// 步骤2: 轮询检查任务状态最多等待60秒
const taskData = taskProgress.value.product
const maxWaitTime = 60000 // 最多等60秒
const pollInterval = 3000 // 每3秒检查一次
const startTime = Date.now()
taskData.total = 100 // 设置临时总数以显示进度动画
taskData.current = 5 // 立即显示初始进度
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)
// 启动进度动画(5-95%)
const progressTimer = setInterval(() => {
if (taskData.current < 95) {
taskData.current = Math.min(taskData.current + 3, 95)
}
}
}, 500)
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 pollTask = async () => {
const maxWaitTime = 60000
const pollInterval = 3000
const startTime = Date.now()
let taskResult: any = null
while (Date.now() - startTime < maxWaitTime) {
if (!trademarkLoading.value) return
if (!trademarkLoading.value) {
clearInterval(progressTimer)
return null
}
await new Promise(resolve => setTimeout(resolve, pollInterval))
if (!trademarkLoading.value) return
if (!trademarkLoading.value) {
clearInterval(progressTimer)
return null
}
try {
taskResult = await markApi.getTask()
if (taskResult.code === 200 || taskResult.code === 0) {
if (taskResult.data.original?.download_url) {
break
clearInterval(progressTimer)
return taskResult
}
}
} catch (err) {
console.log('等待任务处理中...', err)
// 继续等待
}
}
clearInterval(progressTimer)
return taskResult
}
try {
productResult = await pollTask()
if (!trademarkLoading.value) return
if (!taskResult || (taskResult.code !== 200 && taskResult.code !== 0)) {
if (!productResult || (productResult.code !== 200 && productResult.code !== 0)) {
throw new Error('获取任务超时或失败,请重试')
}
if (!taskResult.data.original?.download_url) {
if (!productResult.data.original?.download_url) {
throw new Error('任务处理超时,请稍后重试')
}
// 从结果中提取品牌列表包括TM和未注册的品牌
brandList = taskResult.data.filtered
// 设置真实统计数据
taskData.total = productResult.data.original?.total || 0
taskData.current = taskData.total
taskData.completed = productResult.data.filtered.length
} finally {
clearInterval(progressTimer)
}
// 映射后端数据到前端格式
trademarkData.value = productResult.data.filtered.map((item: any) => ({
name: item['品牌'] || '',
status: item['商标类型'] || '',
class: '',
owner: '',
expireDate: item['注册时间'] || '',
similarity: 0,
asin: item['ASIN'],
productImage: item['商品主图']
}))
// 如果需要品牌筛查,从产品结果中提取品牌列表
if (needBrandCheck) {
brandList = productResult.data.filtered
.map((item: any) => item['品牌'])
.filter((brand: string) => brand && brand.trim())
}
showMessage(`产品筛查完成,共 ${taskData.total} 条,筛查出 ${taskData.completed}`, 'success')
}
// 品牌商标筛查
if (needBrandCheck) {
if (!trademarkLoading.value) return
// 如果没有执行产品筛查需要先从Excel提取品牌列表
if (!needProductCheck) {
showMessage('正在从Excel提取品牌列表...', 'info')
const extractResult = await markApi.extractBrands(trademarkFile.value)
if (extractResult.code !== 200 && extractResult.code !== 0) {
throw new Error(extractResult.msg || '提取品牌列表失败')
}
if (!extractResult.data.brands || extractResult.data.brands.length === 0) {
throw new Error('未能从文件中提取到品牌数据请确保Excel包含"品牌"列')
}
brandList = extractResult.data.brands
showMessage(`品牌列表提取成功,共 ${brandList.length} 个品牌`, 'success')
}
@@ -308,15 +288,13 @@ async function startTrademarkQuery() {
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)
brandData.current = Math.min(brandData.current + 20, brandList.length * 0.95)
}
}, 1000)
// 调用品牌筛查接口(浏览器内并发,速度快)
const brandResult = await markApi.brandCheck(brandList)
if (brandProgressTimer) clearInterval(brandProgressTimer)
@@ -352,29 +330,37 @@ async function startTrademarkQuery() {
emit('updateData', trademarkData.value)
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}`
}
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')
}
} catch (error: any) {
if (error.message && error.message.includes('网络')) {
const hasProductData = taskProgress.value.product.total > 0
// 优化错误信息 - 只显示友好提示
let msg = error.message || ''
if (msg.includes('网络') || msg.includes('network')) {
queryStatus.value = 'networkError'
errorMessage.value = '网络连接失败'
errorMessage.value = '网络不可用,请检查你的网络或代理设置'
} else if (msg.includes('超时') || msg.includes('timeout')) {
queryStatus.value = 'error'
errorMessage.value = '数据库维护中,请稍后重试'
} else if (msg.includes('403') || msg.includes('风控')) {
queryStatus.value = 'error'
errorMessage.value = '网站风控限制,请稍后重试'
} else {
queryStatus.value = 'error'
errorMessage.value = error.message || '查询失败'
errorMessage.value = '数据库维护中,请稍后重试'
}
// 仅在第1步失败时清空数据
if (!hasProductData) {
trademarkData.value = []
emit('updateData', [])
} else {
emit('updateData', trademarkData.value)
}
showMessage(errorMessage.value, 'error')
// 失败时清空数据,不显示侧边栏
trademarkData.value = []
emit('updateData', [])
} finally {
// 清除定时器
if (brandProgressTimer) {
@@ -466,10 +452,13 @@ function resetToIdle() {
trademarkData.value = []
trademarkFileName.value = ''
trademarkFile.value = null
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
}
@@ -481,8 +470,10 @@ defineExpose({
queryStatus,
statusConfig,
taskProgress,
errorMessage,
resetToIdle,
stopTrademarkQuery
stopTrademarkQuery,
startTrademarkQuery
})
</script>
@@ -493,8 +484,8 @@ defineExpose({
<div class="flow-item">
<div class="step-index">1</div>
<div class="step-card">
<div class="step-header"><div class="title">导入卖家精灵选品表格</div></div>
<div class="desc">在卖家精灵导出文档时必须要勾选导出主图具体操作请<span class="link" @click.prevent="viewTrademarkExample">点击查看</span></div>
<div class="step-header"><div class="title">导入Excel表格</div></div>
<div class="desc">产品筛查需导入卖家精灵选品表格并勾选"导出主图"品牌筛查Excel需包含"品牌"</div>
<div class="dropzone" @click="openTrademarkUpload">
<div class="dz-icon">📤</div>
<div class="dz-text">点击或将文件拖拽到这里上传</div>
@@ -798,6 +789,12 @@ defineExpose({
font-weight: 500;
border-radius: 6px;
}
.start-btn:disabled {
background: #d9d9d9;
border-color: #d9d9d9;
color: #ffffff;
cursor: not-allowed;
}
.stop-btn {
background: #f56c6c;
border-color: #f56c6c;