Initial commit
This commit is contained in:
@@ -0,0 +1,393 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { amazonApi } from '../../api/amazon'
|
||||
|
||||
// 响应式状态
|
||||
const loading = ref(false) // 主加载状态
|
||||
const tableLoading = ref(false) // 表格加载状态
|
||||
const progressPercentage = ref(0) // 进度百分比
|
||||
const localProductData = ref<any[]>([]) // 本地产品数据
|
||||
const singleAsin = ref('') // 单个ASIN输入
|
||||
const currentAsin = ref('') // 当前处理的ASIN
|
||||
const genmaiLoading = ref(false) // Genmai Spirit加载状态
|
||||
|
||||
// 分页配置
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(15)
|
||||
const totalPages = computed(() => Math.max(1, Math.ceil((localProductData.value.length || 0) / pageSize.value)))
|
||||
const amazonUpload = ref<HTMLInputElement | null>(null)
|
||||
const dragActive = ref(false)
|
||||
|
||||
// 计算属性 - 当前页数据
|
||||
const paginatedData = computed(() => {
|
||||
const start = (currentPage.value - 1) * pageSize.value
|
||||
const end = start + pageSize.value
|
||||
return localProductData.value.slice(start, end)
|
||||
})
|
||||
|
||||
// 通用消息提示
|
||||
function showMessage(message: string, type: 'success' | 'warning' | 'error' | 'info' = 'info') {
|
||||
alert(`[${type.toUpperCase()}] ${message}`)
|
||||
}
|
||||
|
||||
// Excel文件上传处理 - 主要业务逻辑入口
|
||||
async function processExcelFile(file: File) {
|
||||
try {
|
||||
loading.value = true
|
||||
tableLoading.value = true
|
||||
localProductData.value = []
|
||||
progressPercentage.value = 0
|
||||
|
||||
const response = await amazonApi.importAsinFromExcel(file)
|
||||
const asinList = response.data.asinList
|
||||
|
||||
if (!asinList || asinList.length === 0) {
|
||||
showMessage('文件中未找到有效的ASIN数据', 'warning')
|
||||
return
|
||||
}
|
||||
|
||||
showMessage(`成功解析 ${asinList.length} 个ASIN`, 'success')
|
||||
await batchGetProductInfo(asinList)
|
||||
} catch (error: any) {
|
||||
showMessage(error.message || '处理文件失败', 'error')
|
||||
} finally {
|
||||
loading.value = false
|
||||
tableLoading.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 = ''
|
||||
}
|
||||
|
||||
function onDragOver(e: DragEvent) { e.preventDefault(); dragActive.value = true }
|
||||
function onDragLeave() { dragActive.value = false }
|
||||
async function onDrop(e: DragEvent) {
|
||||
e.preventDefault()
|
||||
dragActive.value = false
|
||||
const file = e.dataTransfer?.files?.[0]
|
||||
if (!file) return
|
||||
const ok = /(\.csv|\.txt|\.xls|\.xlsx)$/i.test(file.name)
|
||||
if (!ok) return showMessage('仅支持 .csv/.txt/.xls/.xlsx 文件', 'warning')
|
||||
await processExcelFile(file)
|
||||
}
|
||||
|
||||
// 批量获取产品信息 - 核心数据处理逻辑
|
||||
async function batchGetProductInfo(asinList: string[]) {
|
||||
try {
|
||||
currentAsin.value = '正在处理...'
|
||||
progressPercentage.value = 0
|
||||
localProductData.value = []
|
||||
|
||||
const batchId = `BATCH_${Date.now()}`
|
||||
const batchSize = 2 // 每批处理2个ASIN
|
||||
const totalBatches = Math.ceil(asinList.length / batchSize)
|
||||
let processedCount = 0
|
||||
let failedCount = 0
|
||||
|
||||
// 分批处理ASIN列表
|
||||
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)
|
||||
|
||||
if (result?.data?.products?.length > 0) {
|
||||
localProductData.value.push(...result.data.products)
|
||||
if (tableLoading.value) tableLoading.value = false // 首次数据到达后隐藏表格加载
|
||||
}
|
||||
|
||||
// 统计失败数量
|
||||
const expectedCount = batchAsins.length
|
||||
const actualCount = result?.data?.products?.length || 0
|
||||
failedCount += Math.max(0, expectedCount - actualCount)
|
||||
|
||||
} catch (error) {
|
||||
failedCount += batchAsins.length
|
||||
console.error(`批次${i + 1}失败:`, error)
|
||||
}
|
||||
|
||||
// 更新进度
|
||||
processedCount += batchAsins.length
|
||||
progressPercentage.value = Math.round((processedCount / asinList.length) * 100)
|
||||
|
||||
// 批次间延迟,避免API频率限制
|
||||
if (i < totalBatches - 1 && loading.value) {
|
||||
await new Promise(resolve => setTimeout(resolve, 1000 + Math.random() * 1500))
|
||||
}
|
||||
}
|
||||
|
||||
// 处理完成状态更新
|
||||
progressPercentage.value = 100
|
||||
currentAsin.value = '处理完成'
|
||||
|
||||
// 结果提示
|
||||
if (failedCount > 0) {
|
||||
showMessage(`采集完成!共 ${asinList.length} 个ASIN,成功 ${asinList.length - failedCount} 个,失败 ${failedCount} 个`, 'warning')
|
||||
} else {
|
||||
showMessage(`采集完成!成功获取 ${asinList.length} 个产品信息`, 'success')
|
||||
}
|
||||
|
||||
} catch (error: any) {
|
||||
showMessage(error.message || '批量获取产品信息失败', 'error')
|
||||
currentAsin.value = '处理失败'
|
||||
} finally {
|
||||
tableLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 单个ASIN查询
|
||||
async function searchSingleAsin() {
|
||||
const asin = singleAsin.value.trim()
|
||||
if (!asin) return
|
||||
|
||||
localProductData.value = []
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
const resp = await amazonApi.getProductsBatch([asin], `SINGLE_${Date.now()}`)
|
||||
if (resp?.data?.products?.length > 0) {
|
||||
localProductData.value = resp.data.products
|
||||
showMessage('查询成功', 'success')
|
||||
singleAsin.value = ''
|
||||
} else {
|
||||
showMessage('未找到商品信息', 'warning')
|
||||
}
|
||||
} catch (e: any) {
|
||||
showMessage(e?.message || '查询失败', 'error')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 导出Excel数据
|
||||
async function exportToExcel() {
|
||||
if (!localProductData.value.length) {
|
||||
showMessage('没有数据可供导出', 'warning')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
loading.value = true
|
||||
showMessage('正在生成Excel文件,请稍候...', 'info')
|
||||
|
||||
// 数据格式化 - 只保留核心字段
|
||||
const exportData = localProductData.value.map(product => ({
|
||||
asin: product.asin || '',
|
||||
seller_shipper: getSellerShipperText(product),
|
||||
price: product.price || '无货'
|
||||
}))
|
||||
|
||||
await amazonApi.exportToExcel(exportData, {
|
||||
filename: `Amazon产品数据_${new Date().toISOString().slice(0, 10)}.xlsx`
|
||||
})
|
||||
|
||||
showMessage('Excel文件导出成功', 'success')
|
||||
} catch (error: any) {
|
||||
showMessage(error.message || '导出Excel失败', 'error')
|
||||
} finally {
|
||||
loading.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() {
|
||||
loading.value = false
|
||||
currentAsin.value = '已停止'
|
||||
showMessage('已停止获取产品数据', 'info')
|
||||
}
|
||||
|
||||
// 打开Genmai Spirit工具
|
||||
async function openGenmaiSpirit() {
|
||||
genmaiLoading.value = true
|
||||
try {
|
||||
await amazonApi.openGenmaiSpirit()
|
||||
} catch (error: any) {
|
||||
showMessage(error.message || '打开跟卖精灵失败', 'error')
|
||||
} finally {
|
||||
genmaiLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 分页处理
|
||||
function handleSizeChange(size: number) {
|
||||
pageSize.value = size
|
||||
currentPage.value = 1
|
||||
}
|
||||
|
||||
function handleCurrentChange(page: number) {
|
||||
currentPage.value = page
|
||||
}
|
||||
|
||||
// 使用 Element Plus 的 jumper,不再需要手动跳转函数
|
||||
|
||||
function openAmazonUpload() {
|
||||
amazonUpload.value?.click()
|
||||
}
|
||||
// 组件挂载时获取最新数据
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const resp = await amazonApi.getLatestProducts()
|
||||
localProductData.value = resp.data?.products || []
|
||||
} catch {
|
||||
// 静默处理初始化失败
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="amazon-root">
|
||||
<div class="main-container">
|
||||
|
||||
<!-- 文件导入和操作区域 -->
|
||||
<div class="import-section" @dragover.prevent="onDragOver" @dragleave="onDragLeave" @drop="onDrop" :class="{ 'drag-active': dragActive }">
|
||||
<div class="import-controls">
|
||||
<!-- 文件上传按钮 -->
|
||||
<el-button type="primary" :disabled="loading" @click="openAmazonUpload">
|
||||
📂 {{ loading ? '处理中...' : '导入ASIN列表' }}
|
||||
</el-button>
|
||||
<input ref="amazonUpload" style="display:none" type="file" accept=".csv,.txt,.xls,.xlsx" @change="handleExcelUpload" :disabled="loading" />
|
||||
|
||||
<!-- 单个ASIN输入 -->
|
||||
<div class="single-input">
|
||||
<input class="text" v-model="singleAsin" placeholder="输入单个ASIN" :disabled="loading" @keyup.enter="searchSingleAsin" />
|
||||
<el-button type="info" :disabled="!singleAsin || loading" @click="searchSingleAsin">查询</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮组 -->
|
||||
<div class="action-buttons">
|
||||
<el-button type="danger" :disabled="!loading" @click="stopFetch">停止获取</el-button>
|
||||
<el-button type="success" :disabled="!localProductData.length || loading" @click="exportToExcel">导出Excel</el-button>
|
||||
<el-button type="warning" :loading="genmaiLoading" @click="openGenmaiSpirit">{{ genmaiLoading ? '启动中...' : '跟卖精灵' }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 进度条显示 -->
|
||||
<div class="progress-section" v-if="loading">
|
||||
<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 class="current-status" v-if="currentAsin">{{ currentAsin }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 数据显示区域 -->
|
||||
<div class="table-container">
|
||||
<!-- 数据表格(无数据时也显示表头) -->
|
||||
<div class="table-section">
|
||||
<div class="table-wrapper">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="130">ASIN</th>
|
||||
<th>卖家/配送方</th>
|
||||
<th width="120">当前售价</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-if="paginatedData.length === 0">
|
||||
<td colspan="3" class="empty-tip">暂无数据,请导入ASIN列表</td>
|
||||
</tr>
|
||||
<tr v-else v-for="row in paginatedData" :key="row.asin">
|
||||
<td>{{ row.asin }}</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>
|
||||
</div>
|
||||
|
||||
<!-- 表格加载遮罩 -->
|
||||
<div v-if="tableLoading" class="table-loading">
|
||||
<div class="spinner">⟳</div>
|
||||
<div>加载中...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分页器 -->
|
||||
<div class="pagination-fixed" >
|
||||
<el-pagination
|
||||
background
|
||||
:current-page="currentPage"
|
||||
:page-sizes="[15,30,50,100]"
|
||||
:page-size="pageSize"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="localProductData.length"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.amazon-root { position: absolute; inset: 0; background: #f5f5f5; padding: 12px; box-sizing: border-box; }
|
||||
.main-container { background: #fff; border-radius: 4px; padding: 15px; box-shadow: 0 2px 4px rgba(0,0,0,0.05); height: 100%; display: flex; flex-direction: column; }
|
||||
.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: 15px 0 10px 0; }
|
||||
.progress-box { padding: 8px 0; }
|
||||
.progress-container { display: flex; align-items: center; position: relative; padding-right: 50px; margin-bottom: 8px; }
|
||||
.progress-bar { flex: 1; height: 6px; background: #ebeef5; border-radius: 3px; overflow: hidden; }
|
||||
.progress-fill { height: 100%; background: linear-gradient(90deg, #409EFF, #66b1ff); border-radius: 3px; transition: width 0.3s ease; }
|
||||
.progress-text { position: absolute; right: 0; font-size: 13px; color: #409EFF; font-weight: 500; }
|
||||
.current-status { font-size: 12px; color: #606266; padding-left: 2px; }
|
||||
.table-container { display: flex; flex-direction: column; flex: 1; min-height: 400px; overflow: hidden; }
|
||||
.table-section { flex: 1; overflow: hidden; position: relative; background: #fff; border: 1px solid #ebeef5; border-radius: 4px; }
|
||||
.table-wrapper { height: 100%; overflow: auto; }
|
||||
.table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
||||
.table th { background: #f5f7fa; color: #909399; font-weight: 600; padding: 12px 8px; border-bottom: 2px solid #ebeef5; text-align: left; }
|
||||
.table td { padding: 10px 8px; border-bottom: 1px solid #f0f0f0; vertical-align: middle; }
|
||||
.table tbody tr:hover { background: #f9f9f9; }
|
||||
.seller-info { display: flex; align-items: center; gap: 4px; }
|
||||
.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; }
|
||||
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
|
||||
.pagination-fixed { flex-shrink: 0; padding: 8px 12px; background: #f9f9f9; border-radius: 4px; display: flex; justify-content: center; border-top: 1px solid #ebeef5; margin-top: 8px; }
|
||||
.empty-tip { text-align: center; color: #909399; padding: 16px 0; }
|
||||
.import-section[draggable], .import-section.drag-active { border: 1px dashed #409EFF; border-radius: 6px; }
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'AmazonDashboard',
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user