-优化错误提示信息,区分网络错误、超时和风控场景 - 改进任务进度计算逻辑,支持更准确的完成状态判断 - 调整品牌提取逻辑,从Excel中直接读取品牌列数据 - 增强后端403错误检测和代理自动切换机制 - 更新前端组件样式和交互逻辑以匹配新状态 -修复部分条件判断逻辑以提升稳定性- 调整文件上传大小限制至50MB以支持更大文件- 优化Excel解析工具类,支持自动识别表头行位置
887 lines
27 KiB
Vue
887 lines
27 KiB
Vue
<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'
|
||
import { markApi } from '../../api/mark'
|
||
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 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')
|
||
const errorMessage = ref('')
|
||
const statusConfig = {
|
||
idle: {
|
||
icon: '/image/img.png',
|
||
title: '暂无数据,请按左侧流程操作',
|
||
desc: ''
|
||
},
|
||
inProgress: {
|
||
icon: '/icon/wait.png',
|
||
title: '正在查询中...',
|
||
desc: '请耐心等待,正在为您筛查商标信息'
|
||
},
|
||
done: {
|
||
icon: '/icon/done1.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(1)
|
||
const accountOptions = [
|
||
{ id: 1, name: 'chensri3.com', username: 'chensri3.com', status: 1 }
|
||
]
|
||
|
||
// 地区选择
|
||
const region = ref('美国')
|
||
const regionOptions = [
|
||
{ 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
|
||
trademarkFile.value = file
|
||
queryStatus.value = 'idle' // 重置状态
|
||
trademarkData.value = []
|
||
emit('updateData', [])
|
||
showMessage(`文件已准备:${file.name}`, 'success')
|
||
input.value = ''
|
||
}
|
||
|
||
async function startTrademarkQuery() {
|
||
if (!trademarkFileName.value || !trademarkFile.value) {
|
||
showMessage('请先导入商标列表', 'warning')
|
||
return
|
||
}
|
||
|
||
if (refreshVipStatus) await refreshVipStatus()
|
||
|
||
if (!props.isVip) {
|
||
if (vipStatus) trialExpiredType.value = vipStatus.value.expiredType
|
||
showTrialExpiredDialog.value = true
|
||
return
|
||
}
|
||
|
||
const needProductCheck = queryTypes.value.includes('product')
|
||
const needBrandCheck = queryTypes.value.includes('brand')
|
||
|
||
trademarkLoading.value = true
|
||
trademarkProgress.value = 0
|
||
trademarkData.value = []
|
||
queryStatus.value = 'inProgress'
|
||
|
||
// 重置任务进度
|
||
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
|
||
|
||
// 通知父组件更新数据
|
||
emit('updateData', [])
|
||
|
||
try {
|
||
let productResult: any = null
|
||
let brandList: string[] = []
|
||
|
||
if (needProductCheck) {
|
||
// 步骤1: 产品商标筛查 - 调用新建任务接口
|
||
showMessage('正在上传文件...', 'info')
|
||
const createResult = await markApi.newTask(trademarkFile.value)
|
||
|
||
if (createResult.code !== 200 && createResult.code !== 0) {
|
||
throw new Error(createResult.msg || '创建任务失败')
|
||
}
|
||
|
||
showMessage('文件上传成功,正在处理...', 'success')
|
||
|
||
const taskData = taskProgress.value.product
|
||
taskData.total = 100 // 设置临时总数以显示进度动画
|
||
taskData.current = 5 // 立即显示初始进度
|
||
|
||
// 启动进度动画(5-95%)
|
||
const progressTimer = setInterval(() => {
|
||
if (taskData.current < 95) {
|
||
taskData.current = Math.min(taskData.current + 3, 95)
|
||
}
|
||
}, 500)
|
||
|
||
// 轮询检查任务状态
|
||
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) {
|
||
clearInterval(progressTimer)
|
||
return null
|
||
}
|
||
|
||
await new Promise(resolve => setTimeout(resolve, pollInterval))
|
||
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) {
|
||
clearInterval(progressTimer)
|
||
return taskResult
|
||
}
|
||
}
|
||
} catch (err) {
|
||
// 继续等待
|
||
}
|
||
}
|
||
|
||
clearInterval(progressTimer)
|
||
return taskResult
|
||
}
|
||
|
||
try {
|
||
productResult = await pollTask()
|
||
if (!trademarkLoading.value) return
|
||
|
||
if (!productResult || (productResult.code !== 200 && productResult.code !== 0)) {
|
||
throw new Error('获取任务超时或失败,请重试')
|
||
}
|
||
|
||
if (!productResult.data.original?.download_url) {
|
||
throw new Error('任务处理超时,请稍后重试')
|
||
}
|
||
|
||
// 设置真实统计数据
|
||
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')
|
||
}
|
||
|
||
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')
|
||
|
||
// 模拟进度动画
|
||
brandProgressTimer = setInterval(() => {
|
||
if (brandData.current < 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)
|
||
|
||
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)
|
||
|
||
let summaryMsg = '筛查完成'
|
||
if (needProductCheck) summaryMsg += `,产品:${taskProgress.value.product.completed}/${taskProgress.value.product.total}`
|
||
if (needBrandCheck && brandList.length > 0) summaryMsg += `,品牌:${taskProgress.value.brand.completed}/${taskProgress.value.brand.total}`
|
||
showMessage(summaryMsg, 'success')
|
||
}
|
||
} catch (error: any) {
|
||
const hasProductData = taskProgress.value.product.total > 0
|
||
|
||
// 优化错误信息 - 只显示友好提示
|
||
let msg = error.message || ''
|
||
if (msg.includes('网络') || msg.includes('network')) {
|
||
queryStatus.value = 'networkError'
|
||
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 = '数据库维护中,请稍后重试'
|
||
}
|
||
|
||
// 仅在第1步失败时清空数据
|
||
if (!hasProductData) {
|
||
trademarkData.value = []
|
||
emit('updateData', [])
|
||
} else {
|
||
emit('updateData', trademarkData.value)
|
||
}
|
||
showMessage(errorMessage.value, 'error')
|
||
} finally {
|
||
// 清除定时器
|
||
if (brandProgressTimer) {
|
||
clearInterval(brandProgressTimer)
|
||
brandProgressTimer = null
|
||
}
|
||
trademarkLoading.value = false
|
||
currentStep.value = 0
|
||
totalSteps.value = 0
|
||
}
|
||
}
|
||
|
||
function stopTrademarkQuery() {
|
||
// 清除进度动画定时器
|
||
if (brandProgressTimer) {
|
||
clearInterval(brandProgressTimer)
|
||
brandProgressTimer = null
|
||
}
|
||
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)
|
||
}
|
||
|
||
function resetToIdle() {
|
||
queryStatus.value = 'idle'
|
||
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
|
||
}
|
||
|
||
defineExpose({
|
||
trademarkLoading,
|
||
trademarkProgress,
|
||
trademarkData,
|
||
queryStatus,
|
||
statusConfig,
|
||
taskProgress,
|
||
errorMessage,
|
||
resetToIdle,
|
||
stopTrademarkQuery,
|
||
startTrademarkQuery
|
||
})
|
||
</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">导入Excel表格</div></div>
|
||
<div class="desc">产品筛查:需导入卖家精灵选品表格,并勾选"导出主图";品牌筛查:Excel需包含"品牌"列</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-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>
|
||
</div>
|
||
</el-scrollbar>
|
||
|
||
<div class="step-actions btn-row">
|
||
<el-button size="small" class="w50" disabled>添加账号</el-button>
|
||
<el-button size="small" class="w50 btn-blue" disabled>账号管理</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%" 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>
|
||
</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', 'query-disabled', { active: queryTypes.includes('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 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>
|
||
|
||
<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: #909399; 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;
|
||
min-width: 0;
|
||
}
|
||
.file-chip .dot {
|
||
width: 6px;
|
||
height: 6px;
|
||
background: #409EFF;
|
||
border-radius: 50%;
|
||
flex-shrink: 0;
|
||
}
|
||
.file-chip .name {
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
.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-card.query-disabled {
|
||
cursor: not-allowed;
|
||
opacity: 0.5;
|
||
pointer-events: none;
|
||
}
|
||
.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: transparent;
|
||
transition: all 0.2s ease;
|
||
}
|
||
.query-card.active .query-check {
|
||
border-color: #1677FF;
|
||
background: transparent;
|
||
}
|
||
.check-icon {
|
||
color: #1677FF;
|
||
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: 32px;
|
||
padding: 0px 16px;
|
||
background: #1677FF;
|
||
border-color: #1677FF;
|
||
font-size: 14px;
|
||
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;
|
||
}
|
||
.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-list {
|
||
height: auto;
|
||
background: #fff;
|
||
border: 1px solid #ebeef5;
|
||
border-radius: 4px;
|
||
margin-bottom: 8px;
|
||
padding: 4px;
|
||
}
|
||
.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%;
|
||
}
|
||
.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) {
|
||
width: 100%;
|
||
}
|
||
.trademark-panel :deep(.el-select .el-input__wrapper) {
|
||
border-radius: 6px;
|
||
}
|
||
.trademark-panel :deep(.el-button) {
|
||
border-radius: 6px;
|
||
}
|
||
</style>
|
||
|