- 优化设备配额检查逻辑,增加账号存在性验证- 更新前端设备列表刷新逻辑,使用保存的用户名参数 - 修改账号编辑表单,禁用已存在账号的用户名和账号名编辑 -优化跟卖精灵打开功能的错误提示和异常处理- 添加页面刷新 IPC通信功能 - 限制用户名输入只能包含字母、数字和下划线 - 移除冗余的本地 IP 获取函数- 升级 erp_client_sb 模块版本至 2.4.9
590 lines
24 KiB
Vue
590 lines
24 KiB
Vue
<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 { handlePlatformFileExport } from '../../utils/settings'
|
||
import { useFileDrop } from '../../composables/useFileDrop'
|
||
|
||
const TrialExpiredDialog = defineAsyncComponent(() => import('../common/TrialExpiredDialog.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加载状态
|
||
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 checkExpiredType = inject<() => 'device' | 'account' | 'both' | 'subscribe'>('checkExpiredType')
|
||
|
||
// 计算属性 - 当前页数据
|
||
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[]>([])
|
||
|
||
// 通用消息提示(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
|
||
} 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 (checkExpiredType) trialExpiredType.value = checkExpiredType()
|
||
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 = '处理完成'
|
||
|
||
|
||
|
||
} 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
|
||
}
|
||
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 success = await handlePlatformFileExport('amazon', blob, fileName)
|
||
|
||
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 = '已停止'
|
||
showMessage('已停止获取产品数据', 'info')
|
||
}
|
||
|
||
async function openGenmaiSpirit() {
|
||
try {
|
||
await ElMessageBox.confirm('打开跟卖精灵会关闭所有谷歌浏览器进程,是否继续?', '提示', {
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'warning'
|
||
})
|
||
genmaiLoading.value = true
|
||
try {
|
||
await systemApi.openGenmaiSpirit()
|
||
showMessage('跟卖精灵已打开', 'success')
|
||
} catch (error: any) {
|
||
const errorMsg = error?.msg || error?.message || '打开跟卖精灵失败'
|
||
showMessage(errorMsg, 'error')
|
||
} finally {
|
||
genmaiLoading.value = false
|
||
}
|
||
} 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 {
|
||
// 静默处理初始化失败
|
||
}
|
||
})
|
||
</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">
|
||
<span class="tab-icon">📦</span>
|
||
<span class="tab-text">ASIN查询</span>
|
||
</div>
|
||
<div class="tab-item" @click="openGenmaiSpirit">
|
||
<span class="tab-icon">🔍</span>
|
||
<span class="tab-text">跟卖精灵</span>
|
||
</div>
|
||
</div>
|
||
<div class="steps-title">操作流程:</div>
|
||
<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>
|
||
</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 class="mini-hint" v-if="pendingAsins.length">已导入 {{ pendingAsins.length }} 个 ASIN</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" />
|
||
|
||
<!-- 表格上方进度条 -->
|
||
<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
|
||
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>
|
||
</section>
|
||
</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; }
|
||
|
||
/* 顶部标签栏 */
|
||
.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; }
|
||
|
||
.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 + .flow-item { border-top: 1px dashed #ebeef5; }
|
||
.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; }
|
||
.single-input.left { display: flex; gap: 8px; }
|
||
.action-buttons.column { display: flex; flex-direction: column; gap: 8px; }
|
||
.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; 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; }
|
||
.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> |