Files
erp_sb/electron-vue-template/src/renderer/components/amazon/TrademarkCheckPanel.vue
zhangzijienbplus 4e2ce48934 feat(trademark): 支持商标筛查任务取消状态及优化错误处理- 新增商标筛查取消状态的UI展示和处理逻辑
-优化错误提示信息,区分网络错误、超时和风控场景
- 改进任务进度计算逻辑,支持更准确的完成状态判断
- 调整品牌提取逻辑,从Excel中直接读取品牌列数据
- 增强后端403错误检测和代理自动切换机制
- 更新前端组件样式和交互逻辑以匹配新状态
-修复部分条件判断逻辑以提升稳定性- 调整文件上传大小限制至50MB以支持更大文件- 优化Excel解析工具类,支持自动识别表头行位置
2025-11-05 10:16:14 +08:00

887 lines
27 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>