Files
erp_sb/electron-vue-template/src/renderer/components/amazon/AmazonDashboard.vue
zhangzijienbplus c9874f1786 feat(amazon):重构亚马逊仪表板组件并新增商标筛查功能
- 将原有复杂逻辑拆分为独立组件:AsinQueryPanel、GenmaiSpiritPanel、TrademarkCheckPanel
- 新增商标批量筛查功能,支持商标状态、类别、权利人等信息查询
- 优化UI布局,改进标签页样式和响应式设计
- 重构数据处理逻辑,使用计算属性优化性能- 完善分页功能,支持不同tab的数据展示
- 移除冗余代码,提高组件可维护性
- 添加跟卖精灵功能说明和注意事项展示-优化空状态和加载状态的用户体验
2025-10-31 11:30:19 +08:00

666 lines
24 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

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, computed } from 'vue'
import AsinQueryPanel from './AsinQueryPanel.vue'
import GenmaiSpiritPanel from './GenmaiSpiritPanel.vue'
import TrademarkCheckPanel from './TrademarkCheckPanel.vue'
const props = defineProps<{
isVip: boolean
}>()
const currentTab = ref<'asin' | 'genmai' | 'trademark'>('asin')
const asinPanelRef = ref<any>(null)
const trademarkPanelRef = ref<any>(null)
const currentPage = ref(1)
const pageSize = ref(15)
const localProductData = ref<any[]>([])
const trademarkData = ref<any[]>([])
const paginatedData = computed(() => {
const start = (currentPage.value - 1) * pageSize.value
const end = start + pageSize.value
if (currentTab.value === 'trademark') {
return trademarkData.value.slice(start, end)
}
return localProductData.value.slice(start, end)
})
const progressVisible = computed(() => {
return asinPanelRef.value?.progressVisible || false
})
const progressPercentage = computed(() => {
if (currentTab.value === 'trademark') {
return trademarkPanelRef.value?.trademarkProgress || 0
}
return asinPanelRef.value?.progressPercentage || 0
})
const loading = computed(() => {
if (currentTab.value === 'asin') {
return asinPanelRef.value?.loading || false
} else if (currentTab.value === 'trademark') {
return trademarkPanelRef.value?.trademarkLoading || false
}
return false
})
function handleAsinDataUpdate(data: any[]) {
localProductData.value = data
}
function handleTrademarkDataUpdate(data: any[]) {
trademarkData.value = data
}
function isOutOfStock(product: any) {
const sellerEmpty = !product?.seller || product.seller === '无货'
const priceEmpty = !product?.price || product.price === '无货'
return sellerEmpty || priceEmpty
}
function handleSizeChange(size: number) {
pageSize.value = size
currentPage.value = 1
}
function handleCurrentChange(page: number) {
currentPage.value = page
}
</script>
<template>
<div class="amazon-root">
<div class="main-container">
<!-- 顶部标签栏 -->
<div class="top-tabs">
<div :class="['tab-item', { active: currentTab === 'asin' }]" @click="currentTab = 'asin'">
<img src="/icon/asin.png" alt="ASIN查询" class="tab-icon" />
<span class="tab-text">ASIN查询</span>
</div>
<div :class="['tab-item', { active: currentTab === 'genmai' }]" @click="currentTab = 'genmai'">
<img src="/icon/anjldk.png" alt="跟卖精灵" class="tab-icon" />
<span class="tab-text">跟卖精灵</span>
</div>
<div :class="['tab-item', { active: currentTab === 'trademark' }]" @click="currentTab = 'trademark'">
<img src="/icon/plsb.png" alt="批量筛查商标" class="tab-icon" />
<span class="tab-text">批量筛查商标</span>
</div>
</div>
<div class="body-layout">
<!-- 左侧步骤栏 -->
<aside :class="['steps-sidebar', currentTab === 'trademark' ? 'wide' : 'narrow']">
<div class="steps-title">操作流程</div>
<!-- ASIN查询面板 -->
<AsinQueryPanel
v-if="currentTab === 'asin'"
ref="asinPanelRef"
:is-vip="isVip"
@update-data="handleAsinDataUpdate"
/>
<!-- 跟卖精灵面板 -->
<GenmaiSpiritPanel
v-if="currentTab === 'genmai'"
:is-vip="isVip"
/>
<!-- 商标筛查面板 -->
<TrademarkCheckPanel
v-if="currentTab === 'trademark'"
ref="trademarkPanelRef"
:is-vip="isVip"
@update-data="handleTrademarkDataUpdate"
/>
</aside>
<!-- 右侧主区域 -->
<section class="content-panel">
<!-- 数据显示区域 -->
<div class="table-container">
<div class="table-section">
<!-- 表格上方进度条 -->
<div v-if="progressVisible || (currentTab === 'trademark' && progressPercentage > 0)" class="progress-head">
<div class="progress-section">
<div class="progress-box">
<div class="progress-container">
<div class="progress-bar">
<div class="progress-fill" :style="{ width: progressPercentage + '%' }"></div>
</div>
<div class="progress-text">{{ progressPercentage }}%</div>
</div>
</div>
</div>
</div>
<div class="table-wrapper">
<!-- 跟卖精灵内容 -->
<div v-if="currentTab === 'genmai'" class="genmai-content">
<div class="genmai-image-wrapper">
<img src="/image/img_1.png" alt="跟卖精灵提示" class="genmai-image" draggable="false" />
</div>
<div class="genmai-info-boxes">
<div class="info-box">
<div class="info-title">功能说明</div>
<div class="info-text">通过此功能可以实现多设备使用同一个跟卖精灵账号</div>
</div>
<div class="info-box">
<div class="info-title">需要注意</div>
<div class="info-text">必须安装Chrome浏览器启动此服务会清除Chrome全部进程</div>
</div>
<div class="info-box">
<div class="info-title">收费标准</div>
<div class="info-text">目前免费试用3天付费会员无使用限制<a href="#" class="info-link">点击付费会员</a></div>
</div>
</div>
</div>
<!-- ASIN查询表格 -->
<table v-if="currentTab === 'asin'" class="table">
<thead>
<tr>
<th>ASIN</th>
<th>卖家/配送方</th>
<th>当前售价</th>
</tr>
</thead>
<tbody>
<tr v-for="row in paginatedData" :key="row.asin">
<td><span :class="{ 'asin-out': isOutOfStock(row) }">{{ row.asin }}</span></td>
<td>
<div class="seller-info">
<span class="seller">{{ row.seller || '无货' }}</span>
<span v-if="row.shipper && row.shipper !== row.seller" class="shipper">/ {{ row.shipper }}</span>
</div>
</td>
<td>
<span class="price">{{ row.price || '无货' }}</span>
</td>
</tr>
</tbody>
</table>
<!-- 商标筛查表格 -->
<table v-if="currentTab === 'trademark'" class="table table-trademark">
<thead>
<tr>
<th>商标名称</th>
<th>状态</th>
<th>类别</th>
<th>权利人</th>
<th>到期日期</th>
<th>相似度</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, idx) in paginatedData" :key="idx">
<td><span class="trademark-name">{{ row.name }}</span></td>
<td>
<span :class="{
'status-registered': row.status === '已注册',
'status-risk': row.status === '近似风险',
'status-available': row.status === '可注册'
}">{{ row.status }}</span>
</td>
<td>{{ row.class }}</td>
<td class="truncate-cell">{{ row.owner }}</td>
<td>{{ row.expireDate }}</td>
<td>
<span v-if="row.similarity > 0" :class="{ 'similarity-high': row.similarity >= 80 }">
{{ row.similarity }}%
</span>
<span v-else>-</span>
</td>
</tr>
</tbody>
</table>
</div>
<div v-if="paginatedData.length === 0" class="empty-abs">
<!-- 商标筛查状态显示 -->
<div v-if="currentTab === 'trademark' && trademarkPanelRef?.queryStatus" class="empty-container">
<img
:src="trademarkPanelRef.statusConfig[trademarkPanelRef.queryStatus].icon"
:alt="trademarkPanelRef.statusConfig[trademarkPanelRef.queryStatus].title"
:class="['status-image', { 'status-loading': trademarkPanelRef.queryStatus === 'inProgress' }]"
/>
<div class="empty-title">{{ trademarkPanelRef.statusConfig[trademarkPanelRef.queryStatus].title }}</div>
<div v-if="trademarkPanelRef.statusConfig[trademarkPanelRef.queryStatus].desc" class="empty-desc">
{{ trademarkPanelRef.statusConfig[trademarkPanelRef.queryStatus].desc }}
</div>
</div>
<!-- 其他tab的加载状态 -->
<div v-else-if="loading" class="empty-container">
<div class="spinner"></div>
<div>加载中...</div>
</div>
<!-- 默认空状态 -->
<div v-else class="empty-container">
<img src="/image/img.png" alt="暂无数据" class="empty-image" />
<div class="empty-text">暂无数据请按左侧流程操作</div>
</div>
</div>
</div>
<div v-if="currentTab !== 'genmai'" class="pagination-fixed">
<el-pagination
:current-page="currentPage"
:page-sizes="[15,30,50,100]"
:page-size="pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="currentTab === 'trademark' ? trademarkData.length : localProductData.length"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</div>
</section>
</div>
</div>
</div>
</template>
<style scoped>
.amazon-root {
position: absolute;
inset: 0;
background: #fff;
box-sizing: border-box;
overflow: hidden;
}
.main-container {
height: 100%;
display: flex;
flex-direction: column;
padding: 12px;
box-sizing: border-box;
overflow: hidden;
min-height: 0;
}
/* 顶部标签栏 */
.top-tabs {
display: flex;
gap: 8px;
margin-bottom: 12px;
flex-shrink: 0;
}
.tab-item {
box-sizing: border-box;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
padding: 0px 12px;
gap: 8px;
width: 140px;
height: 40px;
min-height: 40px;
max-height: 40px;
background: #FFFFFF;
border: 1px solid #e5e7eb;
border-radius: 8px;
flex: none;
flex-shrink: 0;
cursor: pointer;
transition: all 0.2s ease;
color: #606266;
font-size: 14px;
font-weight: 400;
}
.tab-item:hover {
background: #f0f9ff;
border-color: #1677FF;
color: #1677FF;
}
.tab-item.active {
background: #FFFFFF;
border: 1px solid #1677FF;
color: #1677FF;
cursor: default;
}
.tab-icon { width: 20px; height: 20px; flex-shrink: 0; object-fit: contain; }
.tab-text {
line-height: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
min-width: 0;
}
/* 账号列表样式(和斑马一致) */
.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; }
.body-layout {
display: flex;
gap: 12px;
flex: 1;
overflow: hidden;
min-height: 0;
}
.steps-sidebar {
background: #fff;
border: 1px solid #ebeef5;
border-radius: 6px;
padding: 12px;
height: 100%;
flex-shrink: 0;
display: flex;
flex-direction: column;
box-sizing: border-box;
transition: width 0.3s ease;
}
.steps-sidebar.narrow {
width: 240px;
}
.steps-sidebar.wide {
width: 280px;
}
.steps-title {
font-size: 14px;
font-weight: 600;
color: #303133;
text-align: left;
flex-shrink: 0;
margin-bottom: 8px;
}
.steps-flow { position: relative; }
.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; }
.flow-item:after { display: none; }
.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; }
.mini-hint { font-size: 12px; color: #909399; margin-top: 6px; }
.links { display: flex; align-items: center; gap: 6px; margin-bottom: 8px; }
.link { color: #409EFF; cursor: pointer; font-size: 12px; }
.sep { color: #dcdfe6; }
.content-panel {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0;
min-height: 0;
overflow: hidden;
}
.left-controls { margin-top: 10px; display: flex; flex-direction: column; gap: 10px; }
.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-icon { font-size: 20px; margin-bottom: 6px; }
.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; }
.single-input.left { display: flex; gap: 8px; }
.action-buttons.column { display: flex; flex-direction: column; gap: 8px; }
.step-actions { margin-top: 8px; display: flex; gap: 8px; }
.btn-row { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.w50 { width: 100%; }
.form-row { margin-bottom: 10px; }
.label { display: block; font-size: 12px; color: #606266; margin-bottom: 6px; }
/* 统一左侧控件宽度与主色 */
.steps-sidebar :deep(.el-date-editor),
.steps-sidebar :deep(.el-range-editor.el-input__wrapper),
.steps-sidebar :deep(.el-input),
.steps-sidebar :deep(.el-input__wrapper),
.steps-sidebar :deep(.el-select) { width: 100%; box-sizing: border-box; }
.btn-blue { background: #1677FF; border-color: #1677FF; color: #fff; }
.btn-blue:disabled { background: #a6c8ff; border-color: #a6c8ff; color: #fff; }
.w100 { width: 100%; }
.steps-sidebar :deep(.el-button + .el-button) { margin-left: 0; }
.import-section { margin-bottom: 10px; flex-shrink: 0; }
.import-controls { display: flex; align-items: flex-end; gap: 20px; flex-wrap: wrap; margin-bottom: 8px; }
.single-input { display: flex; align-items: center; gap: 8px; }
.text { width: 180px; height: 32px; padding: 0 10px; border: 1px solid #dcdfe6; border-radius: 4px; font-size: 14px; outline: none; transition: border-color 0.2s ease; }
.text:focus { border-color: #409EFF; }
.text:disabled { background: #f5f7fa; color: #c0c4cc; }
.action-buttons { display: flex; gap: 10px; flex-wrap: wrap; }
.progress-section { margin: 0px 12px 0px 12px; }
.progress-head { margin-bottom: 8px; }
.progress-box { padding: 4px 0; }
.progress-container { display: flex; align-items: center; gap: 8px; }
.progress-bar { flex: 1; height: 6px; background: #e3eeff; border-radius: 999px; overflow: hidden; }
.progress-fill { height: 100%; background: #1677FF; border-radius: 999px; transition: width 0.3s ease; }
.progress-text { font-size: 13px; color: #1677FF; font-weight: 500; min-width: 44px; text-align: right; }
.current-status { font-size: 12px; color: #606266; padding-left: 2px; }
.table-container {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
overflow: hidden;
}
.table-section {
flex: 1;
overflow: hidden;
position: relative;
background: #fff;
border: 1px solid #ebeef5;
border-radius: 4px;
display: flex;
flex-direction: column;
min-height: 0;
}
.table-wrapper {
flex: 1;
min-height: 0;
overflow: auto;
scrollbar-width: thin;
scrollbar-color: #c0c4cc transparent;
}
.table-wrapper::-webkit-scrollbar { width: 6px; height: 6px; }
.table-wrapper::-webkit-scrollbar-track { background: transparent; }
.table-wrapper::-webkit-scrollbar-thumb { background: #c0c4cc; border-radius: 3px; }
.table-wrapper:hover::-webkit-scrollbar-thumb { background: #a8abb2; }
.table { width: 100%; border-collapse: collapse; font-size: 13px; table-layout: fixed; }
.table th { background: #f5f7fa; color: #909399; font-weight: 600; padding: 12px 8px; border-bottom: 2px solid #ebeef5; text-align: center; }
.table td { padding: 10px 8px; border-bottom: 1px solid #f0f0f0; vertical-align: middle; text-align: center; }
.table tbody tr:hover { background: #f9f9f9; }
.table th:nth-child(1), .table td:nth-child(1) { width: 33.33%; }
.table th:nth-child(2), .table td:nth-child(2) { width: 33.33%; }
.table th:nth-child(3), .table td:nth-child(3) { width: 33.33%; }
.asin-out { color: #f56c6c; font-weight: 600; }
.seller-info { display: flex; align-items: center; gap: 4px; justify-content: center; }
.seller { color: #303133; font-weight: 500; }
.shipper { color: #909399; font-size: 12px; }
.price { color: #e6a23c; font-weight: 600; }
.table-loading { position: absolute; inset: 0; background: rgba(255, 255, 255, 0.95); display: flex; flex-direction: column; justify-content: center; align-items: center; font-size: 14px; color: #606266; }
.spinner { font-size: 24px; animation: spin 1s linear infinite; margin-bottom: 8px; }
.inline-spinner { display: inline-block; animation: spin 1s linear infinite; }
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
.pagination-fixed { flex-shrink: 0; padding: 8px 12px 0 12px; background: #fff; display: flex; justify-content: flex-end; }
.pagination-fixed :deep(.el-pager li.is-active) { border: 1px solid #1677FF; border-radius: 4px; color: #1677FF; background: #fff; }
.empty-tip { text-align: center; color: #909399; padding: 16px 0; }
.import-section[draggable], .import-section.drag-active { border: 1px dashed #409EFF; border-radius: 6px; }
.empty-container { text-align: center; }
.empty-icon { font-size: 48px; margin-bottom: 12px; opacity: 0.6; }
.empty-image { max-width: 200px; width: 100%; height: auto; margin-bottom: 16px; opacity: 0.8; }
.empty-text { font-size: 14px; color: #909399; }
.empty-abs { position: absolute; left: 0; right: 0; top: 48px; bottom: 0; display: flex; align-items: center; justify-content: center; }
/* 状态图标样式 */
.status-image {
max-width: 160px;
width: 100%;
height: auto;
margin-bottom: 20px;
opacity: 0.9;
user-select: none;
-webkit-user-drag: none;
pointer-events: none;
}
.status-loading {
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { opacity: 0.6; }
50% { opacity: 0.9; }
}
.empty-title {
font-size: 16px;
font-weight: 600;
color: #303133;
margin-bottom: 8px;
}
.empty-desc {
font-size: 14px;
color: #909399;
line-height: 1.5;
}
/* 商标筛查相关样式 */
.table-trademark th:nth-child(1), .table-trademark td:nth-child(1) { width: 15%; }
.table-trademark th:nth-child(2), .table-trademark td:nth-child(2) { width: 12%; }
.table-trademark th:nth-child(3), .table-trademark td:nth-child(3) { width: 18%; }
.table-trademark th:nth-child(4), .table-trademark td:nth-child(4) { width: 25%; }
.table-trademark th:nth-child(5), .table-trademark td:nth-child(5) { width: 15%; }
.table-trademark th:nth-child(6), .table-trademark td:nth-child(6) { width: 15%; }
.trademark-name { font-weight: 600; color: #303133; }
.truncate-cell { max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.status-registered { color: #67c23a; font-weight: 500; }
.status-risk { color: #f56c6c; font-weight: 500; }
.status-available { color: #409EFF; font-weight: 500; }
.similarity-high { color: #f56c6c; font-weight: 600; }
.dz-el-icon { font-size: 18px; margin-bottom: 4px; color: #909399; }
/* 跟卖精灵内容样式 */
.genmai-content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
height: 100%;
}
.genmai-image-wrapper {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
}
.genmai-image {
max-width: 100%;
max-height: 370px;
width: auto;
height: auto;
object-fit: contain;
user-select: none;
-webkit-user-drag: none;
pointer-events: none;
}
.genmai-info-boxes {
box-sizing: border-box;
display: flex;
text-align: left;
flex-direction: row;
align-items: flex-start;
padding: 8px 0px;
gap: 10px;
width: 90%;
max-width: 872px;
height: 200rpx;
background: rgba(0, 0, 0, 0.02);
border: 1px solid rgba(0, 0, 0, 0.06);
border-radius: 8px;
flex: none;
order: 0;
flex-grow: 0;
}
.info-box {
flex: 1;
border: none;
background: transparent;
display: flex;
flex-direction: column;
gap: 8px;
padding: 0 16px;
}
.info-box:not(:last-child) {
border-right: 1px solid rgba(0, 0, 0, 0.06);
}
.info-title {
font-size: 14px;
font-weight: 600;
color: #303133;
line-height: 1.4;
}
.info-text {
font-size: 13px;
color: #606266;
line-height: 1.6;
}
.info-link {
color: #1677FF;
text-decoration: none;
}
.info-link:hover {
text-decoration: underline;
}
/* 响应式设计 */
@media (max-width: 1200px) {
.steps-sidebar.narrow {
width: 220px;
}
/* wide保持280px不变 */
.tab-item {
width: 120px;
padding: 0px 8px;
}
}
@media (max-width: 1024px) {
.steps-sidebar.narrow {
width: 200px;
}
.steps-sidebar.wide {
width: 260px;
}
.genmai-info-boxes {
width: 95%;
}
}
@media (max-width: 768px) {
.body-layout {
flex-direction: column;
}
.steps-sidebar.narrow,
.steps-sidebar.wide {
width: 100%;
max-height: 300px;
}
.tab-item {
width: 100px;
font-size: 13px;
}
}
</style>
<script lang="ts">
export default {
name: 'AmazonDashboard',
}
</script>