- 将原有复杂逻辑拆分为独立组件:AsinQueryPanel、GenmaiSpiritPanel、TrademarkCheckPanel - 新增商标批量筛查功能,支持商标状态、类别、权利人等信息查询 - 优化UI布局,改进标签页样式和响应式设计 - 重构数据处理逻辑,使用计算属性优化性能- 完善分页功能,支持不同tab的数据展示 - 移除冗余代码,提高组件可维护性 - 添加跟卖精灵功能说明和注意事项展示-优化空状态和加载状态的用户体验
376 lines
13 KiB
Vue
376 lines
13 KiB
Vue
<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>
|
||
|