Files
erp_sb/electron-vue-template/src/renderer/components/amazon/AmazonDashboard.vue
zhangzijienbplus 0be60bc103 feat(genmai): 集成跟卖精灵账号管理系统跟
- 新增卖精灵账号管理功能,支持多账号切换
- 实现账号增删改查接口与前端交互逻辑
-优化打开跟卖精灵流程,增加账号选择界面
- 添加账号权限限制与订阅升级提醒
- 完善后端账号实体类及数据库映射
- 更新系统控制器以支持指定账号启动功能- 调整HTTP请求路径适配新工具模块路由- 升级客户端版本至2.5.3并优化代码结构
2025-10-27 09:13:00 +08:00

699 lines
30 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, computed, onMounted, defineAsyncComponent, inject } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { amazonApi } from '../../api/amazon'
import { systemApi } from '../../api/system'
import { genmaiApi, type GenmaiAccount } from '../../api/genmai'
import { handlePlatformFileExport } from '../../utils/settings'
import { getUsernameFromToken } from '../../utils/token'
import { useFileDrop } from '../../composables/useFileDrop'
const TrialExpiredDialog = defineAsyncComponent(() => import('../common/TrialExpiredDialog.vue'))
const AccountManager = defineAsyncComponent(() => import('../common/AccountManager.vue'))
const refreshVipStatus = inject<() => Promise<boolean>>('refreshVipStatus')
// 接收VIP状态
const props = defineProps<{
isVip: boolean
}>()
// 响应式状态
const loading = ref(false) // 主加载状态
const tableLoading = ref(false) // 表格加载状态
const progressPercentage = ref(0) // 进度百分比
const progressVisible = ref(false) // 进度条是否显示(完成后仍保留)
const localProductData = ref<any[]>([]) // 本地产品数据
const currentAsin = ref('') // 当前处理的ASIN
const genmaiLoading = ref(false) // Genmai Spirit加载状态
const genmaiAccounts = ref<GenmaiAccount[]>([]) // 跟卖精灵账号列表
const selectedGenmaiAccountId = ref<number | null>(null) // 选中的跟卖精灵账号ID
const currentTab = ref<'asin' | 'genmai'>('asin') // 当前选中的tab
let abortController: AbortController | null = null // 请求取消控制器
// 分页配置
const currentPage = ref(1)
const pageSize = ref(15)
const amazonUpload = ref<HTMLInputElement | null>(null)
// 试用期过期弹框
const showTrialExpiredDialog = ref(false)
const trialExpiredType = ref<'device' | 'account' | 'both' | 'subscribe'>('account')
// 账号管理弹框
const showAccountManager = ref(false)
const accountManagerRef = ref<any>(null)
const vipStatus = inject<any>('vipStatus')
// 计算属性 - 当前页数据
const paginatedData = computed(() => {
const start = (currentPage.value - 1) * pageSize.value
const end = start + pageSize.value
return localProductData.value.slice(start, end)
})
// 左侧:网站地区 & 待采集队列
const region = ref('JP')
const regionOptions = [
{ label: '日本 (Japan)', value: 'JP', flag: '🇯🇵' },
{ label: '美国 (USA)', value: 'US', flag: '🇺🇸' },
]
const pendingAsins = ref<string[]>([])
const selectedFileName = ref('')
// 通用消息提示Element Plus
function showMessage(message: string, type: 'success' | 'warning' | 'error' | 'info' = 'info') {
ElMessage({ message, type })
}
// Excel文件上传处理 - 主要业务逻辑入口
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[]) {
// 刷新VIP状态
if (refreshVipStatus) await refreshVipStatus()
// VIP检查
if (!props.isVip) {
if (vipStatus) trialExpiredType.value = vipStatus.value.expiredType
showTrialExpiredDialog.value = true
return
}
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, region.value, abortController?.signal)
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: any) {
if (error.name === 'AbortError') break
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 = '处理完成'
selectedFileName.value = ''
} catch (error: any) {
if (error.name !== 'AbortError') {
showMessage(error.message || '批量获取产品信息失败', 'error')
currentAsin.value = '处理失败'
selectedFileName.value = ''
}
} finally {
tableLoading.value = false
}
}
// 点击开始采集
async function startQueuedFetch() {
if (!pendingAsins.value.length) {
showMessage('请先导入ASIN列表', 'warning')
return
}
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
}
}
// 导出Excel数据
const exportLoading = ref(false)
async function exportToExcel() {
if (!localProductData.value.length) {
showMessage('没有数据可供导出', 'warning')
return
}
exportLoading.value = true
// 生成Excel HTML格式
let html = `<table>
<tr><th>ASIN</th><th>卖家/配送方</th><th>当前售价</th></tr>`
localProductData.value.forEach(product => {
html += `<tr>
<td>${product.asin || ''}</td>
<td>${getSellerShipperText(product)}</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
}
// 判定无货(用于标红 ASIN
function isOutOfStock(product: any) {
const sellerEmpty = !product?.seller || product.seller === '无货'
const priceEmpty = !product?.price || product.price === '无货'
return sellerEmpty || priceEmpty
}
// 停止获取操作
function stopFetch() {
abortController?.abort()
abortController = null
loading.value = false
currentAsin.value = '已停止'
selectedFileName.value = ''
showMessage('已停止获取产品数据', 'info')
}
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 {}
}
// 分页处理
function handleSizeChange(size: number) {
pageSize.value = size
currentPage.value = 1
}
function handleCurrentChange(page: number) {
currentPage.value = page
}
// 使用 Element Plus 的 jumper不再需要手动跳转函数
// 示例弹窗
const amazonExampleVisible = ref(false)
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()
localProductData.value = resp.data?.products || []
} catch {}
await loadGenmaiAccounts()
})
</script>
<template>
<div class="amazon-root">
<div class="main-container">
<div class="body-layout">
<!-- 左侧步骤栏 -->
<aside class="steps-sidebar">
<!-- 顶部标签栏 -->
<div class="top-tabs">
<div :class="['tab-item', { active: currentTab === 'asin' }]" @click="currentTab = 'asin'">
<span class="tab-icon">📦</span>
<span class="tab-text">ASIN查询</span>
</div>
<div :class="['tab-item', { active: currentTab === 'genmai' }]" @click="currentTab = 'genmai'">
<span class="tab-icon">🔍</span>
<span class="tab-text">跟卖精灵</span>
</div>
</div>
<!-- 跟卖精灵流程区 -->
<div v-if="currentTab === 'genmai'" class="steps-title">操作流程</div>
<div v-if="currentTab === 'genmai'" 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/img_v3_02qd_052605f0-4be3-44db-9691-35ee5ff6201g.jpg" 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>
</el-button>
</div>
</div>
</div>
</div>
<div v-if="currentTab === 'asin'" class="steps-title">操作流程</div>
<div v-if="currentTab === 'asin'" 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>
</aside>
<!-- 右侧主区域 -->
<section class="content-panel">
<!-- 数据显示区域 -->
<div class="table-container">
<div class="table-section">
<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" />
<!-- 账号管理弹框 -->
<AccountManager
ref="accountManagerRef"
v-model="showAccountManager"
platform="genmai"
@refresh="loadGenmaiAccounts"
/>
<!-- 表格上方进度条 -->
<div v-if="progressVisible" 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">
<table 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>
</div>
<div v-if="paginatedData.length === 0" class="empty-abs">
<div v-if="tableLoading || loading" class="empty-container">
<div class="spinner"></div>
<div>加载中...</div>
</div>
<div v-else class="empty-container">
<div class="empty-icon">📄</div>
<div class="empty-text">暂无数据请导入ASIN列表</div>
</div>
</div>
</div>
<div 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="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; }
.main-container { height: 100%; display: flex; flex-direction: column; padding: 12px; box-sizing: border-box; }
/* 顶部标签栏 */
.top-tabs { display: flex; margin-bottom: 8px; }
.tab-item { flex: 1; display: flex; align-items: center; justify-content: center; gap: 3px; padding: 4px 6px; cursor: pointer; transition: all 0.2s ease; background: #f5f7fa; color: #606266; font-size: 11px; font-weight: 500; border: 1px solid #ebeef5; }
.tab-item:first-child { border-radius: 3px 0 0 3px; }
.tab-item:last-child { border-radius: 0 3px 3px 0; border-left: none; }
.tab-item:hover { background: #e8f4ff; color: #409EFF; }
.tab-item.active { background: #1677FF; color: #fff; border-color: #1677FF; cursor: default; }
.tab-icon { font-size: 12px; }
.tab-text { line-height: 1; }
/* 账号列表样式(和斑马一致) */
.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; }
.steps-sidebar { width: 220px; background: #fff; border: 1px solid #ebeef5; border-radius: 6px; padding: 10px; height: 100%; flex-shrink: 0; }
.steps-title { font-size: 14px; font-weight: 600; color: #303133; text-align: left; }
.steps-flow { position: relative; }
.steps-flow:before { content: ''; position: absolute; left: 11px; top: 20px; bottom: 0; width: 1px; background: rgba(229, 231, 235, 0.6); }
.flow-item { position: relative; display: grid; grid-template-columns: 22px 1fr; gap: 10px; padding: 8px 0; }
.flow-item .step-index { position: static; width: 22px; height: 22px; line-height: 22px; text-align: center; border-radius: 50%; background: #1677FF; color: #fff; font-size: 12px; 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: 6px; }
.title { font-size: 13px; font-weight: 600; color: #303133; text-align: left; }
.desc { font-size: 12px; color: #909399; margin-bottom: 8px; text-align: left; }
.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; }
.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: 400px; overflow: hidden; }
.table-section { flex: 1; overflow: hidden; position: relative; background: #fff; border: 1px solid #ebeef5; border-radius: 4px; display: flex; flex-direction: column; }
.table-wrapper { flex: 1; overflow: auto; }
.table-wrapper { 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; }
@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-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; }
</style>
<script lang="ts">
export default {
name: 'AmazonDashboard',
}
</script>