feat(amazon):重构亚马逊仪表板组件并新增商标筛查功能
- 将原有复杂逻辑拆分为独立组件:AsinQueryPanel、GenmaiSpiritPanel、TrademarkCheckPanel - 新增商标批量筛查功能,支持商标状态、类别、权利人等信息查询 - 优化UI布局,改进标签页样式和响应式设计 - 重构数据处理逻辑,使用计算属性优化性能- 完善分页功能,支持不同tab的数据展示 - 移除冗余代码,提高组件可维护性 - 添加跟卖精灵功能说明和注意事项展示-优化空状态和加载状态的用户体验
@@ -0,0 +1,375 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, inject, onMounted, defineAsyncComponent } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { amazonApi } from '../../api/amazon'
|
||||
import { handlePlatformFileExport } from '../../utils/settings'
|
||||
import { getUsernameFromToken } from '../../utils/token'
|
||||
import { useFileDrop } from '../../composables/useFileDrop'
|
||||
|
||||
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 loading = ref(false)
|
||||
const tableLoading = ref(false)
|
||||
const progressPercentage = ref(0)
|
||||
const progressVisible = ref(false)
|
||||
const localProductData = ref<any[]>([])
|
||||
const currentAsin = ref('')
|
||||
let abortController: AbortController | null = null
|
||||
|
||||
const region = ref('JP')
|
||||
const regionOptions = [
|
||||
{ label: '日本 (Japan)', value: 'JP', flag: '🇯🇵' },
|
||||
{ label: '美国 (USA)', value: 'US', flag: '🇺🇸' },
|
||||
]
|
||||
const pendingAsins = ref<string[]>([])
|
||||
const selectedFileName = ref('')
|
||||
const amazonUpload = ref<HTMLInputElement | null>(null)
|
||||
const exportLoading = ref(false)
|
||||
const amazonExampleVisible = ref(false)
|
||||
|
||||
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 })
|
||||
}
|
||||
|
||||
async function processExcelFile(file: File) {
|
||||
try {
|
||||
loading.value = true
|
||||
progressPercentage.value = 0
|
||||
progressVisible.value = false
|
||||
|
||||
const response = await amazonApi.importAsinFromExcel(file)
|
||||
const asinList = response.data.asinList
|
||||
|
||||
if (!asinList || asinList.length === 0) {
|
||||
showMessage('文件中未找到有效的ASIN数据', 'warning')
|
||||
return
|
||||
}
|
||||
pendingAsins.value = asinList
|
||||
selectedFileName.value = file.name
|
||||
} catch (error: any) {
|
||||
showMessage(error.message || '处理文件失败', 'error')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleExcelUpload(e: Event) {
|
||||
const input = e.target as HTMLInputElement
|
||||
const file = input.files?.[0]
|
||||
if (!file) return
|
||||
await processExcelFile(file)
|
||||
input.value = ''
|
||||
}
|
||||
|
||||
const { dragActive, onDragEnter, onDragOver, onDragLeave, onDrop } = useFileDrop({
|
||||
accept: /\.xlsx?$/i,
|
||||
onFile: processExcelFile,
|
||||
onError: (msg) => showMessage(msg, 'warning')
|
||||
})
|
||||
|
||||
async function batchGetProductInfo(asinList: string[]) {
|
||||
if (refreshVipStatus) await refreshVipStatus()
|
||||
if (!props.isVip) {
|
||||
if (vipStatus) trialExpiredType.value = vipStatus.value.expiredType
|
||||
showTrialExpiredDialog.value = true
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
currentAsin.value = '正在处理...'
|
||||
progressPercentage.value = 0
|
||||
|
||||
const batchId = `BATCH_${Date.now()}`
|
||||
const batchSize = 2
|
||||
const totalBatches = Math.ceil(asinList.length / batchSize)
|
||||
let processedCount = 0
|
||||
|
||||
for (let i = 0; i < totalBatches && loading.value; i++) {
|
||||
const start = i * batchSize
|
||||
const end = Math.min(start + batchSize, asinList.length)
|
||||
const batchAsins = asinList.slice(start, end)
|
||||
|
||||
currentAsin.value = `正在处理第${i + 1}/${totalBatches}批 (${batchAsins.join(', ')})`
|
||||
|
||||
try {
|
||||
const result = await amazonApi.getProductsBatch(batchAsins, batchId, region.value, abortController?.signal)
|
||||
|
||||
if (result?.data?.products?.length > 0) {
|
||||
localProductData.value.push(...result.data.products)
|
||||
// 立即更新父组件数据,实时显示
|
||||
emit('updateData', [...localProductData.value])
|
||||
if (tableLoading.value) tableLoading.value = false
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error.name === 'AbortError') break
|
||||
console.error(`批次${i + 1}失败:`, error)
|
||||
}
|
||||
|
||||
processedCount += batchAsins.length
|
||||
progressPercentage.value = Math.round((processedCount / asinList.length) * 100)
|
||||
|
||||
if (i < totalBatches - 1 && loading.value) {
|
||||
await new Promise(resolve => setTimeout(resolve, 1000 + Math.random() * 1500))
|
||||
}
|
||||
}
|
||||
|
||||
progressPercentage.value = 100
|
||||
currentAsin.value = '处理完成'
|
||||
} catch (error: any) {
|
||||
if (error.name !== 'AbortError') {
|
||||
showMessage(error.message || '批量获取产品信息失败', 'error')
|
||||
currentAsin.value = '处理失败'
|
||||
}
|
||||
} finally {
|
||||
tableLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function startQueuedFetch() {
|
||||
if (!pendingAsins.value.length) {
|
||||
showMessage('请先导入ASIN列表', 'warning')
|
||||
return
|
||||
}
|
||||
|
||||
// 开始采集前先清空数据
|
||||
localProductData.value = []
|
||||
emit('updateData', [])
|
||||
|
||||
abortController = new AbortController()
|
||||
loading.value = true
|
||||
progressVisible.value = true
|
||||
tableLoading.value = true
|
||||
try {
|
||||
await batchGetProductInfo(pendingAsins.value)
|
||||
} finally {
|
||||
tableLoading.value = false
|
||||
loading.value = false
|
||||
abortController = null
|
||||
}
|
||||
}
|
||||
|
||||
async function exportToExcel() {
|
||||
if (!localProductData.value.length) {
|
||||
showMessage('没有数据可供导出', 'warning')
|
||||
return
|
||||
}
|
||||
|
||||
exportLoading.value = true
|
||||
|
||||
let html = `<table>
|
||||
<tr><th>ASIN</th><th>卖家/配送方</th><th>当前售价</th></tr>`
|
||||
|
||||
localProductData.value.forEach(product => {
|
||||
const sellerText = getSellerShipperText(product)
|
||||
html += `<tr>
|
||||
<td>${product.asin || ''}</td>
|
||||
<td>${sellerText}</td>
|
||||
<td>${product.price || '无货'}</td>
|
||||
</tr>`
|
||||
})
|
||||
html += '</table>'
|
||||
|
||||
const blob = new Blob([html], { type: 'application/vnd.ms-excel' })
|
||||
const fileName = `Amazon产品数据_${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 getSellerShipperText(product: any) {
|
||||
let text = product.seller || '无货'
|
||||
if (product.shipper && product.shipper !== product.seller) {
|
||||
text += (text && text !== '无货' ? ' / ' : '') + product.shipper
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
function stopFetch() {
|
||||
abortController?.abort()
|
||||
abortController = null
|
||||
loading.value = false
|
||||
currentAsin.value = '已停止'
|
||||
showMessage('已停止获取产品数据', 'info')
|
||||
}
|
||||
|
||||
function openAmazonUpload() {
|
||||
amazonUpload.value?.click()
|
||||
}
|
||||
|
||||
function viewAmazonExample() {
|
||||
amazonExampleVisible.value = true
|
||||
}
|
||||
|
||||
function downloadAmazonTemplate() {
|
||||
const html = '<table><tr><th>ASIN</th></tr><tr><td>B0XXXXXXX1</td></tr><tr><td>B0XXXXXXX2</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 = 'amazon_asin_template.xls'
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
// 组件挂载时加载缓存数据
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const resp = await amazonApi.getLatestProducts()
|
||||
if (resp.data?.products && resp.data.products.length > 0) {
|
||||
localProductData.value = resp.data.products
|
||||
emit('updateData', resp.data.products)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载缓存数据失败:', error)
|
||||
}
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
loading,
|
||||
progressVisible,
|
||||
progressPercentage,
|
||||
localProductData
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="asin-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">导入ASIN</div></div>
|
||||
<div class="desc">仅支持包含 ASIN 列的 Excel 文档</div>
|
||||
<div class="links">
|
||||
<a class="link" @click.prevent="viewAmazonExample">点击查看示例</a>
|
||||
<span class="sep">|</span>
|
||||
<a class="link" @click.prevent="downloadAmazonTemplate">点击下载模板</a>
|
||||
</div>
|
||||
<div class="dropzone" :class="{ active: dragActive }" @dragenter="onDragEnter" @dragover="onDragOver" @dragleave="onDragLeave" @drop="onDrop" @click="openAmazonUpload">
|
||||
<div class="dz-el-icon">📤</div>
|
||||
<div class="dz-text">点击或将文件拖拽到这里上传</div>
|
||||
<div class="dz-sub">支持 .xls .xlsx</div>
|
||||
</div>
|
||||
<input ref="amazonUpload" style="display:none" type="file" accept=".xls,.xlsx" @change="handleExcelUpload" :disabled="loading" />
|
||||
<div v-if="selectedFileName" class="file-chip">
|
||||
<span class="dot"></span>
|
||||
<span class="name">{{ selectedFileName }}</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="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>
|
||||
<!-- 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">导入表格后,点击下方按钮开始获取ASIN数据</div>
|
||||
<div class="action-buttons column">
|
||||
<el-button size="small" class="w100 btn-blue" :disabled="!pendingAsins.length || loading" @click="startQueuedFetch">{{ loading ? '处理中...' : '获取数据' }}</el-button>
|
||||
<el-button size="small" class="w100" :disabled="!loading" @click="stopFetch">停止获取</el-button>
|
||||
</div>
|
||||
</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="action-buttons column">
|
||||
<el-button size="small" class="w100 btn-blue" :disabled="!localProductData.length || loading || exportLoading" :loading="exportLoading" @click="exportToExcel">{{ exportLoading ? '导出中...' : '导出Excel' }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-dialog v-model="amazonExampleVisible" title="示例 - ASIN文档格式" width="480px">
|
||||
<div>
|
||||
<div style="margin:8px 0;color:#606266;font-size:13px;">Excel 示例:</div>
|
||||
<el-table :data="[{asin:'B0XXXXXXX1'},{asin:'B0XXXXXXX2'}]" size="small" border>
|
||||
<el-table-column prop="asin" label="ASIN" />
|
||||
</el-table>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button type="primary" class="btn-blue" @click="amazonExampleVisible = false">我知道了</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<TrialExpiredDialog v-model="showTrialExpiredDialog" :expired-type="trialExpiredType" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.asin-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;
|
||||
}
|
||||
.asin-panel .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; }
|
||||
.sep { color: #dcdfe6; }
|
||||
.dropzone { border: 1px dashed #c0c4cc; border-radius: 6px; padding: 16px; text-align: center; cursor: pointer; background: #fafafa; }
|
||||
.dropzone:hover { background: #f6fbff; border-color: #409EFF; }
|
||||
.dz-el-icon { font-size: 18px; margin-bottom: 4px; color: #909399; }
|
||||
.dz-text { color: #303133; font-size: 13px; }
|
||||
.dz-sub { color: #909399; 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; }
|
||||
.action-buttons.column { display: flex; flex-direction: column; gap: 8px; }
|
||||
.btn-blue { background: #1677FF; border-color: #1677FF; color: #fff; }
|
||||
.btn-blue:disabled { background: #a6c8ff; border-color: #a6c8ff; color: #fff; }
|
||||
.w100 { width: 100%; }
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, inject, onMounted, defineAsyncComponent } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { systemApi } from '../../api/system'
|
||||
import { genmaiApi, type GenmaiAccount } from '../../api/genmai'
|
||||
import { getUsernameFromToken } from '../../utils/token'
|
||||
|
||||
const AccountManager = defineAsyncComponent(() => import('../common/AccountManager.vue'))
|
||||
|
||||
const props = defineProps<{
|
||||
isVip: boolean
|
||||
}>()
|
||||
|
||||
const genmaiLoading = ref(false)
|
||||
const genmaiAccounts = ref<GenmaiAccount[]>([])
|
||||
const selectedGenmaiAccountId = ref<number | null>(null)
|
||||
const showAccountManager = ref(false)
|
||||
const accountManagerRef = ref<any>(null)
|
||||
|
||||
function showMessage(message: string, type: 'success' | 'warning' | 'error' | 'info' = 'info') {
|
||||
ElMessage({ message, type })
|
||||
}
|
||||
|
||||
async function openGenmaiSpirit() {
|
||||
if (!genmaiAccounts.value.length) {
|
||||
showAccountManager.value = true
|
||||
return
|
||||
}
|
||||
genmaiLoading.value = true
|
||||
try {
|
||||
await systemApi.openGenmaiSpirit(selectedGenmaiAccountId.value)
|
||||
showMessage('跟卖精灵已打开', 'success')
|
||||
} finally {
|
||||
genmaiLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadGenmaiAccounts() {
|
||||
try {
|
||||
const res = await genmaiApi.getAccounts(getUsernameFromToken())
|
||||
genmaiAccounts.value = (res as any)?.data ?? []
|
||||
if (genmaiAccounts.value[0]) selectedGenmaiAccountId.value = genmaiAccounts.value[0].id
|
||||
} catch {}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await loadGenmaiAccounts()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="genmai-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">请选择需启动的跟卖精灵账号</div>
|
||||
<template v-if="genmaiAccounts.length">
|
||||
<el-scrollbar :class="['account-list', { 'scroll-limit': genmaiAccounts.length > 3 }]">
|
||||
<div>
|
||||
<div
|
||||
v-for="acc in genmaiAccounts"
|
||||
:key="acc.id"
|
||||
:class="['acct-item', { selected: selectedGenmaiAccountId === acc.id }]"
|
||||
@click="selectedGenmaiAccountId = 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="selectedGenmaiAccountId === acc.id" class="acct-check">✔️</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="placeholder-box">
|
||||
<img class="placeholder-img" src="/icon/image.png" alt="add-account" />
|
||||
<div class="placeholder-tip">请添加跟卖精灵账号</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="step-actions btn-row">
|
||||
<el-button size="small" class="w50" @click="showAccountManager = true">添加账号</el-button>
|
||||
<el-button size="small" class="w50 btn-blue" @click="showAccountManager = true">账号管理</el-button>
|
||||
</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">请确保设备已安装Chrome浏览器,否则服务将无法启动。打开跟卖精灵将关闭Chrome浏览器进程。</div>
|
||||
<div class="action-buttons column">
|
||||
<el-button
|
||||
size="small"
|
||||
class="w100 btn-blue"
|
||||
:disabled="genmaiLoading || !genmaiAccounts.length"
|
||||
@click="openGenmaiSpirit"
|
||||
>
|
||||
<span v-if="!genmaiLoading">启动服务</span>
|
||||
<span v-else><span class="inline-spinner">⟳</span> 启动中...</span>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AccountManager
|
||||
ref="accountManagerRef"
|
||||
v-model="showAccountManager"
|
||||
platform="genmai"
|
||||
@refresh="loadGenmaiAccounts"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.genmai-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;
|
||||
}
|
||||
.genmai-panel .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; }
|
||||
|
||||
.account-list { height: auto; }
|
||||
.scroll-limit { max-height: 140px; }
|
||||
.placeholder-box { display: flex; align-items: center; justify-content: center; flex-direction: column; height: 100px; background: #fff; border: 1px solid #ebeef5; border-radius: 4px; margin-bottom: 8px; }
|
||||
.placeholder-img { width: 80px; opacity: 0.9; }
|
||||
.placeholder-tip { margin-top: 6px; font-size: 12px; color: #a8abb2; }
|
||||
.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; }
|
||||
.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; cursor: pointer; margin-bottom: 4px; }
|
||||
.acct-item.selected { background: #eef5ff; box-shadow: inset 0 0 0 1px #d6e4ff; }
|
||||
.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::-webkit-scrollbar { width: 0; height: 0; }
|
||||
|
||||
.step-actions { margin-top: 8px; display: flex; gap: 8px; }
|
||||
.btn-row { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
|
||||
.w50 { width: 100%; }
|
||||
.action-buttons.column { display: flex; flex-direction: column; gap: 8px; }
|
||||
.btn-blue { background: #1677FF; border-color: #1677FF; color: #fff; }
|
||||
.btn-blue:disabled { background: #a6c8ff; border-color: #a6c8ff; color: #fff; }
|
||||
.w100 { width: 100%; }
|
||||
.inline-spinner { display: inline-block; animation: spin 1s linear infinite; }
|
||||
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
|
||||
</style>
|
||||
|
||||
@@ -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>
|
||||
|
||||
BIN
electron-vue-template/src/renderer/public/icon/anjldk.png
Normal file
|
After Width: | Height: | Size: 638 B |
BIN
electron-vue-template/src/renderer/public/icon/asin.png
Normal file
|
After Width: | Height: | Size: 378 B |
BIN
electron-vue-template/src/renderer/public/icon/cancel.png
Normal file
|
After Width: | Height: | Size: 913 B |
BIN
electron-vue-template/src/renderer/public/icon/done.png
Normal file
|
After Width: | Height: | Size: 731 B |
BIN
electron-vue-template/src/renderer/public/icon/error.png
Normal file
|
After Width: | Height: | Size: 870 B |
BIN
electron-vue-template/src/renderer/public/icon/inProgress.png
Normal file
|
After Width: | Height: | Size: 968 B |
BIN
electron-vue-template/src/renderer/public/icon/networkErrors.png
Normal file
|
After Width: | Height: | Size: 628 B |
BIN
electron-vue-template/src/renderer/public/icon/plsb.png
Normal file
|
After Width: | Height: | Size: 894 B |
BIN
electron-vue-template/src/renderer/public/icon/wait.png
Normal file
|
After Width: | Height: | Size: 533 B |
BIN
electron-vue-template/src/renderer/public/icon/wlymx.png
Normal file
|
After Width: | Height: | Size: 384 B |
BIN
electron-vue-template/src/renderer/public/image/img.png
Normal file
|
After Width: | Height: | Size: 67 KiB |
BIN
electron-vue-template/src/renderer/public/image/img_1.png
Normal file
|
After Width: | Height: | Size: 60 KiB |