feat(amazon):重构亚马逊仪表板组件并新增商标筛查功能

- 将原有复杂逻辑拆分为独立组件:AsinQueryPanel、GenmaiSpiritPanel、TrademarkCheckPanel
- 新增商标批量筛查功能,支持商标状态、类别、权利人等信息查询
- 优化UI布局,改进标签页样式和响应式设计
- 重构数据处理逻辑,使用计算属性优化性能- 完善分页功能,支持不同tab的数据展示
- 移除冗余代码,提高组件可维护性
- 添加跟卖精灵功能说明和注意事项展示-优化空状态和加载状态的用户体验
This commit is contained in:
2025-10-31 11:30:19 +08:00
parent 87a4a2fed0
commit c9874f1786
16 changed files with 1571 additions and 480 deletions

View File

@@ -0,0 +1,575 @@
<script setup lang="ts">
import { ref, inject, defineAsyncComponent } from 'vue'
import { ElMessage } from 'element-plus'
import { handlePlatformFileExport } from '../../utils/settings'
import { getUsernameFromToken } from '../../utils/token'
const TrialExpiredDialog = defineAsyncComponent(() => import('../common/TrialExpiredDialog.vue'))
const refreshVipStatus = inject<() => Promise<boolean>>('refreshVipStatus')
const props = defineProps<{
isVip: boolean
}>()
const emit = defineEmits<{
updateData: [data: any[]]
}>()
const trademarkData = ref<any[]>([])
const trademarkFileName = ref('')
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)
// 查询状态: idle-待开始, inProgress-进行中, done-已完成, error-错误, networkError-网络错误, cancel-已取消
const queryStatus = ref<'idle' | 'inProgress' | 'done' | 'error' | 'networkError' | 'cancel'>('idle')
const errorMessage = ref('')
const statusConfig = {
idle: {
icon: '/image/img.png',
title: '暂无数据,请按左侧流程操作',
desc: ''
},
inProgress: {
icon: '/icon/wait.png',
title: '正在查询中...',
desc: '请耐心等待,正在为您筛查商标信息'
},
done: {
icon: '/icon/done.png',
title: '筛查完成',
desc: '已成功完成所有商标筛查任务'
},
error: {
icon: '/icon/error.png',
title: '查询失败',
desc: '查询过程中出现错误,请重试'
},
networkError: {
icon: '/icon/networkErrors.png',
title: '网络连接失败',
desc: '网络异常,请检查网络连接后重试'
},
cancel: {
icon: '/icon/cancel.png',
title: '已取消查询',
desc: '您已取消本次查询任务'
}
}
// 选择的账号(模拟数据)
const selectedAccount = ref('chensri3.com')
const accountOptions = [
{ label: 'chensri3.com', value: 'chensri3.com', status: '美国' },
{ label: 'zhangikcpi.com', value: 'zhangikcpi.com', status: '加拿大' },
{ label: 'hhhhsoutlook...', value: 'hhhhsoutlook', status: '日本' }
]
// 地区选择
const region = ref('美国')
const regionOptions = [
{ label: '美国', value: '美国', flag: '🇺🇸' },
{ label: '日本', value: '日本', flag: '🇯🇵' },
{ label: '加拿大', value: '加拿大', flag: '🇨🇦' }
]
// 查询类型多选
const queryTypes = ref<string[]>(['product'])
const showTrialExpiredDialog = ref(false)
const trialExpiredType = ref<'device' | 'account' | 'both' | 'subscribe'>('account')
const vipStatus = inject<any>('vipStatus')
function showMessage(message: string, type: 'success' | 'warning' | 'error' | 'info' = 'info') {
ElMessage({ message, type })
}
function openTrademarkUpload() {
trademarkUpload.value?.click()
}
async function handleTrademarkUpload(e: Event) {
const input = e.target as HTMLInputElement
const file = input.files?.[0]
if (!file) return
const ok = /\.xlsx?$/.test(file.name)
if (!ok) {
showMessage('仅支持 .xlsx/.xls 文件', 'warning')
return
}
trademarkFileName.value = file.name
queryStatus.value = 'idle' // 重置状态
trademarkData.value = []
emit('updateData', [])
showMessage(`文件已准备:${file.name}`, 'success')
input.value = ''
}
async function startTrademarkQuery() {
if (!trademarkFileName.value) {
showMessage('请先导入商标列表', 'warning')
return
}
if (refreshVipStatus) await refreshVipStatus()
if (!props.isVip) {
if (vipStatus) trialExpiredType.value = vipStatus.value.expiredType
showTrialExpiredDialog.value = true
return
}
trademarkLoading.value = true
trademarkProgress.value = 0
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 }
]
totalSteps.value = mockData.length
try {
for (let i = 0; i < mockData.length; i++) {
if (!trademarkLoading.value) break // 被取消
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 (trademarkLoading.value) {
queryStatus.value = 'done'
emit('updateData', trademarkData.value)
showMessage('商标筛查完成', 'success')
}
} catch (error: any) {
if (error.message && error.message.includes('网络')) {
queryStatus.value = 'networkError'
errorMessage.value = '网络连接失败'
} else {
queryStatus.value = 'error'
errorMessage.value = error.message || '查询失败'
}
showMessage(errorMessage.value, 'error')
} finally {
trademarkLoading.value = false
currentStep.value = 0
totalSteps.value = 0
}
}
function stopTrademarkQuery() {
trademarkLoading.value = false
queryStatus.value = 'cancel'
currentStep.value = 0
totalSteps.value = 0
showMessage('已停止筛查', 'info')
}
async function exportTrademarkData() {
if (!trademarkData.value.length) {
showMessage('没有数据可供导出', 'warning')
return
}
exportLoading.value = true
let html = `<table>
<tr><th>商标名称</th><th>状态</th><th>类别</th><th>权利人</th><th>到期日期</th><th>相似度</th></tr>`
trademarkData.value.forEach(item => {
html += `<tr>
<td>${item.name || ''}</td>
<td>${item.status || ''}</td>
<td>${item.class || ''}</td>
<td>${item.owner || ''}</td>
<td>${item.expireDate || ''}</td>
<td>${item.similarity ? item.similarity + '%' : '-'}</td>
</tr>`
})
html += '</table>'
const blob = new Blob([html], { type: 'application/vnd.ms-excel' })
const fileName = `商标筛查结果_${new Date().toISOString().slice(0, 10)}.xls`
const username = getUsernameFromToken()
const success = await handlePlatformFileExport('amazon', blob, fileName, username)
if (success) {
showMessage('Excel文件导出成功', 'success')
}
exportLoading.value = false
}
function toggleQueryType(type: string) {
const index = queryTypes.value.indexOf(type)
if (index > -1) {
queryTypes.value.splice(index, 1)
} else {
queryTypes.value.push(type)
}
}
function viewTrademarkExample() {
showMessage('商标列表应包含一列商标名称', 'info')
}
function downloadTrademarkTemplate() {
const html = '<table><tr><th>商标名称</th></tr><tr><td>SONY</td></tr><tr><td>NIKE</td></tr><tr><td>MyBrand</td></tr></table>'
const blob = new Blob([html], { type: 'application/vnd.ms-excel' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = '商标列表模板.xls'
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
URL.revokeObjectURL(url)
}
defineExpose({
trademarkLoading,
trademarkProgress,
trademarkData,
queryStatus,
statusConfig
})
</script>
<template>
<div class="trademark-panel">
<div class="steps-flow">
<!-- 1. 导入签署精灵账品批格 -->
<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="dropzone" @click="openTrademarkUpload">
<div class="dz-icon">📤</div>
<div class="dz-text">点击或将文件拖拽到这里上传</div>
<div class="dz-sub">支持 .xls .xlsx</div>
</div>
<input ref="trademarkUpload" style="display:none" type="file" accept=".xls,.xlsx" @change="handleTrademarkUpload" :disabled="trademarkLoading" />
<div v-if="trademarkFileName" class="file-chip">
<span class="dot"></span>
<span class="name">{{ trademarkFileName }}</span>
</div>
</div>
</div>
<!-- 2. 选择亚马逊商标地区 -->
<div class="flow-item">
<div class="step-index">2</div>
<div class="step-card">
<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>
</div>
</el-option>
</el-select>
<div class="step-actions btn-row">
<el-button size="small" class="w50">添加账号</el-button>
<el-button size="small" class="w50 btn-blue">账号管理</el-button>
</div>
</div>
</div>
<!-- 3. 选择产品地理区域 -->
<div class="flow-item">
<div class="step-index">3</div>
<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-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>
</el-select>
</div>
</div>
<!-- 4. 选择需查询的可多选 -->
<div class="flow-item">
<div class="step-index">4</div>
<div class="step-card">
<div class="step-header"><div class="title">选择需查询的可多选</div></div>
<div class="desc">默认查询违规产品可多选</div>
<div class="query-options">
<div :class="['query-card', { active: queryTypes.includes('product') }]" @click="toggleQueryType('product')">
<div class="query-check">
<div class="check-icon" v-if="queryTypes.includes('product')"></div>
</div>
<div class="query-content">
<div class="query-title">产品商标筛查</div>
<div class="query-desc">筛查未注册商标或TM标的商品</div>
</div>
</div>
<div :class="['query-card', { active: queryTypes.includes('brand') }]" @click="toggleQueryType('brand')">
<div class="query-check">
<div class="check-icon" v-if="queryTypes.includes('brand')"></div>
</div>
<div class="query-content">
<div class="query-title">品牌商标筛查</div>
<div class="query-desc">筛查未注册商标的品牌</div>
</div>
</div>
<div :class="['query-card', { active: queryTypes.includes('platform') }]" @click="toggleQueryType('platform')">
<div class="query-check">
<div class="check-icon" v-if="queryTypes.includes('platform')"></div>
</div>
<div class="query-content">
<div class="query-title">跟卖许可筛查</div>
<div class="query-desc">筛查亚马逊许可跟卖的商品</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 底部开始查询按钮 -->
<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>
</div>
</div>
<TrialExpiredDialog v-model="showTrialExpiredDialog" :expired-type="trialExpiredType" />
</div>
</template>
<style scoped>
.trademark-panel {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
overflow: hidden;
}
.steps-flow {
position: relative;
flex: 1;
min-height: 0;
overflow-y: auto;
scrollbar-width: none;
}
.steps-flow::-webkit-scrollbar {
display: none;
}
.steps-flow:before { content: ''; position: absolute; left: 13px; top: 26px; bottom: 0; width: 2px; background: rgba(229, 231, 235, 0.6); }
.flow-item { position: relative; display: grid; grid-template-columns: 28px 1fr; gap: 12px; padding: 10px 0; }
.flow-item .step-index { position: static; width: 28px; height: 28px; line-height: 28px; text-align: center; border-radius: 50%; background: #1677FF; color: #fff; font-size: 14px; font-weight: 600; margin-top: 2px; }
.step-card { border: none; border-radius: 0; padding: 0; background: transparent; }
.step-header { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
.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; }
.file-chip {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 8px;
background: #f5f7fa;
border-radius: 4px;
font-size: 12px;
color: #606266;
margin-top: 6px;
}
.file-chip .dot {
width: 6px;
height: 6px;
background: #409EFF;
border-radius: 50%;
display: inline-block;
}
.file-chip .name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.dropzone {
border: 1px dashed #c0c4cc;
border-radius: 8px;
padding: 20px 16px;
text-align: center;
cursor: pointer;
background: #fafafa;
transition: all 0.2s ease;
}
.dropzone:hover { background: #f6fbff; border-color: #409EFF; }
.dz-icon { font-size: 20px; margin-bottom: 6px; color: #909399; }
.dz-text { color: #303133; font-size: 13px; margin-bottom: 2px; }
.dz-sub { color: #909399; font-size: 12px; }
.query-options {
display: flex;
flex-direction: column;
gap: 8px;
}
.query-card {
display: flex;
flex-direction: row;
align-items: center;
padding: 8px 12px;
gap: 10px;
width: 100%;
min-height: 46px;
border-radius: 6px;
border: 1px solid #e5e7eb;
background: #ffffff;
cursor: pointer;
transition: all 0.2s ease;
box-sizing: border-box;
}
.query-card:hover {
border-color: #1677FF;
background: #f7fbff;
}
.query-card.active {
border-color: #1677FF;
background: #f0f9ff;
}
.query-check {
width: 16px;
height: 16px;
border-radius: 50%;
border: 1.5px solid #d9d9d9;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
background: #ffffff;
transition: all 0.2s ease;
}
.query-card.active .query-check {
border-color: #1677FF;
background: #1677FF;
}
.check-icon {
color: #ffffff;
font-size: 10px;
font-weight: bold;
line-height: 1;
}
.query-content {
flex: 1;
display: flex;
flex-direction: column;
gap: 2px;
align-items: flex-start;
min-width: 0;
}
.query-title {
font-size: 13px;
font-weight: 600;
color: #303133;
line-height: 1.3;
text-align: left;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: 100%;
}
.query-desc {
font-size: 11px;
color: #909399;
line-height: 1.3;
text-align: left;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: 100%;
}
.step-actions { margin-top: 8px; }
.btn-row { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.w50 { width: 100%; }
.bottom-action {
flex-shrink: 0;
padding: 16px 0 0 0;
border-top: 2px solid #e5e7eb;
}
.action-header {
display: flex;
align-items: center;
gap: 10px;
}
.step-indicator {
font-size: 15px;
color: #303133;
font-weight: 600;
min-width: 36px;
text-align: left;
}
.start-btn {
flex: 1;
height: 38px;
background: #1677FF;
border-color: #1677FF;
font-size: 14px;
font-weight: 500;
border-radius: 6px;
}
.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-status {
margin-left: auto;
font-size: 12px;
color: #909399;
}
.account-check {
color: #1677FF;
font-size: 14px;
}
.trademark-panel :deep(.el-select),
.trademark-panel :deep(.el-input__wrapper) {
width: 100%;
}
.trademark-panel :deep(.el-select .el-input__wrapper) {
border-radius: 6px;
}
.trademark-panel :deep(.el-button) {
border-radius: 6px;
}
</style>