1
This commit is contained in:
@@ -269,7 +269,7 @@ onMounted(async () => {
|
||||
<div class="body-layout">
|
||||
<!-- 左侧步骤栏 -->
|
||||
<aside class="steps-sidebar">
|
||||
<div class="steps-title">查询步骤:</div>
|
||||
<div class="steps-title">操作流程:</div>
|
||||
<div class="steps-flow">
|
||||
<!-- 1 -->
|
||||
<div class="flow-item">
|
||||
@@ -408,7 +408,7 @@ onMounted(async () => {
|
||||
.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; }
|
||||
.body-layout { display: flex; gap: 12px; height: 100%; }
|
||||
.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; margin-bottom: 8px; }
|
||||
.steps-title { font-size: 14px; font-weight: 600; color: #303133; margin-bottom: 8px; text-align: left; }
|
||||
.steps-flow { position: relative; }
|
||||
.steps-flow:before { content: ''; position: absolute; left: 12px; top: 0; bottom: 0; width: 2px; background: #e5e7eb; }
|
||||
.flow-item { position: relative; display: grid; grid-template-columns: 24px 1fr; gap: 10px; padding: 8px 0; }
|
||||
|
||||
@@ -30,19 +30,6 @@ const progressStarted = ref(false)
|
||||
const progressPercentage = ref(0)
|
||||
const totalProducts = ref(0)
|
||||
const processedProducts = ref(0)
|
||||
// 进度头部文案(展示在进度条上方)
|
||||
const successCount = computed(() =>
|
||||
allProducts.value.filter(
|
||||
(p: any) => p && p.mapRecognitionLink && String(p.mapRecognitionLink).trim() !== ''
|
||||
).length
|
||||
)
|
||||
const progressHeader = computed(() => {
|
||||
if (!progressStarted.value) return ''
|
||||
if (progressPercentage.value >= 100) {
|
||||
return `数据获取完成(成功获取 ${successCount.value} 个) 左侧操作栏点击“导出数据”按钮,可导出为Excel文件`
|
||||
}
|
||||
return '数据获取中'
|
||||
})
|
||||
|
||||
// 左侧步骤栏进度
|
||||
const activeStep = computed(() => {
|
||||
@@ -79,20 +66,33 @@ function openRakutenUpload() {
|
||||
uploadInputRef.value?.click()
|
||||
}
|
||||
|
||||
function parseSkuPrices(product: any) {
|
||||
if (!product.skuPrice) return []
|
||||
function parseSkuPrices(input: any) {
|
||||
try {
|
||||
let skuStr = product.skuPrice
|
||||
if (typeof skuStr === 'string') {
|
||||
skuStr = skuStr.replace(/(\d+(?:\.\d+)?):"/g, '"$1":"')
|
||||
skuStr = JSON.parse(skuStr)
|
||||
let skuSource: any = input
|
||||
if (input && typeof input === 'object') {
|
||||
skuSource = input.skuPriceJson ?? input.skuPrice
|
||||
}
|
||||
return Object.keys(skuStr).map(p => parseFloat(p)).filter(n => !isNaN(n)).sort((a, b) => a - b)
|
||||
if (!skuSource) return []
|
||||
if (typeof skuSource === 'string') {
|
||||
skuSource = skuSource.replace(/(\d+(?:\.\d+)?):"/g, '"$1":"')
|
||||
skuSource = JSON.parse(skuSource)
|
||||
}
|
||||
if (!skuSource || typeof skuSource !== 'object') return []
|
||||
return Object.keys(skuSource)
|
||||
.map(p => parseFloat(p))
|
||||
.filter(n => !isNaN(n))
|
||||
.sort((a, b) => a - b)
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
function needsSearch(product: any) {
|
||||
const hasParsedPrices = Array.isArray(product?.skuPrices) && product.skuPrices.length > 0
|
||||
const hasRawPrices = !!(product?.skuPriceJson || product?.skuPrice)
|
||||
return !(hasParsedPrices || hasRawPrices)
|
||||
}
|
||||
|
||||
async function loadLatest() {
|
||||
const resp = await rakutenApi.getLatestProducts()
|
||||
allProducts.value = (resp.products || []).map(p => ({...p, skuPrices: parseSkuPrices(p)}))
|
||||
@@ -101,16 +101,18 @@ async function loadLatest() {
|
||||
|
||||
async function searchProductInternal(product: any) {
|
||||
if (!product || !product.imgUrl) return
|
||||
if (product.mapRecognitionLink && String(product.mapRecognitionLink).trim() !== '') return
|
||||
if (!needsSearch(product)) return
|
||||
const res = await rakutenApi.search1688(product.imgUrl, currentBatchId.value)
|
||||
const data = res
|
||||
const skuJson = (data as any)?.skuPriceJson ?? (data as any)?.skuPrice
|
||||
Object.assign(product, {
|
||||
mapRecognitionLink: data.mapRecognitionLink,
|
||||
freight: data.freight,
|
||||
median: data.median,
|
||||
weight: data.weight,
|
||||
skuPrice: data.skuPrice,
|
||||
skuPrices: parseSkuPrices(data),
|
||||
skuPriceJson: skuJson,
|
||||
skuPrice: skuJson,
|
||||
skuPrices: parseSkuPrices({ skuPriceJson: skuJson }),
|
||||
image1688Url: data.mapRecognitionLink,
|
||||
detailUrl1688: data.mapRecognitionLink,
|
||||
})
|
||||
@@ -163,36 +165,9 @@ async function onDrop(e: DragEvent) {
|
||||
await processFile(file)
|
||||
}
|
||||
|
||||
async function searchSingleShop() {
|
||||
const shop = singleShopName.value.trim()
|
||||
if (!shop) return
|
||||
|
||||
// 重置进度与状态
|
||||
progressStarted.value = true
|
||||
progressPercentage.value = 0
|
||||
totalProducts.value = 0
|
||||
processedProducts.value = 0
|
||||
loading.value = true
|
||||
tableLoading.value = true
|
||||
currentBatchId.value = `RAKUTEN_${Date.now()}`
|
||||
try {
|
||||
const resp = await rakutenApi.getProducts({shopName: shop, batchId: currentBatchId.value})
|
||||
allProducts.value = (resp.products || []).filter((p: any) => p.originalShopName === shop).map(p => ({ ...p, skuPrices: parseSkuPrices(p) }))
|
||||
statusType.value = 'info'
|
||||
statusMessage.value = `店铺 ${shop} 共 ${allProducts.value.length} 条,点击“获取数据”开始识图`
|
||||
singleShopName.value = ''
|
||||
} catch (e: any) {
|
||||
statusMessage.value = e?.message || '查询失败'
|
||||
statusType.value = 'error'
|
||||
} finally {
|
||||
loading.value = false
|
||||
tableLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 点击“获取数据”触发识图
|
||||
// 点击“获取数据
|
||||
async function handleStartSearch() {
|
||||
// 如果存在待解析文件,先请求后端解析再进入识图
|
||||
if (pendingFile.value) {
|
||||
try {
|
||||
loading.value = true
|
||||
@@ -216,10 +191,19 @@ async function handleStartSearch() {
|
||||
tableLoading.value = false
|
||||
}
|
||||
}
|
||||
const items = allProducts.value.filter(p => p && p.imgUrl && !p.mapRecognitionLink)
|
||||
const items = allProducts.value.filter(p => p && p.imgUrl && needsSearch(p))
|
||||
if (allProducts.value.length > 0 && items.length === 0) {
|
||||
progressStarted.value = true
|
||||
totalProducts.value = allProducts.value.length
|
||||
processedProducts.value = totalProducts.value
|
||||
progressPercentage.value = 100
|
||||
statusType.value = 'success'
|
||||
statusMessage.value = ''
|
||||
return
|
||||
}
|
||||
if (items.length === 0) {
|
||||
statusType.value = 'warning'
|
||||
statusMessage.value = '没有可识图的商品,请先导入或查询店铺'
|
||||
statusMessage.value = '没有可处理的商品,请先导入或查询店铺'
|
||||
return
|
||||
}
|
||||
await startBatch1688Search(items)
|
||||
@@ -235,7 +219,7 @@ function stopTask() {
|
||||
}
|
||||
|
||||
async function startBatch1688Search(products: any[]) {
|
||||
const items = (products || []).filter(p => p && p.imgUrl && !p.mapRecognitionLink)
|
||||
const items = (products || []).filter(p => p && p.imgUrl && needsSearch(p))
|
||||
if (items.length === 0) {
|
||||
progressPercentage.value = 100
|
||||
statusType.value = 'success'
|
||||
@@ -319,7 +303,7 @@ onMounted(loadLatest)
|
||||
<div class="body-layout">
|
||||
<!-- 左侧步骤栏 -->
|
||||
<aside class="steps-sidebar">
|
||||
<div class="steps-title">查询步骤:</div>
|
||||
<div class="steps-title">操作流程:</div>
|
||||
|
||||
<div class="steps-flow">
|
||||
<!-- Step 1 导入乐天店铺 -->
|
||||
@@ -399,12 +383,8 @@ onMounted(loadLatest)
|
||||
<!-- 数据显示区域 -->
|
||||
<div class="table-container">
|
||||
<div class="table-section">
|
||||
<!-- 表格上方进度条(移动到表格容器内部) -->
|
||||
<!-- 表格上方进度条 -->
|
||||
<div v-if="progressStarted" class="progress-head">
|
||||
<div class="progress-title">
|
||||
<span v-if="progressPercentage>=100" class="ok-badge">●</span>
|
||||
<span class="title-text">{{ progressHeader }}</span>
|
||||
</div>
|
||||
<div class="progress-section">
|
||||
<div class="progress-box">
|
||||
<div class="progress-container">
|
||||
@@ -519,7 +499,7 @@ onMounted(loadLatest)
|
||||
|
||||
.body-layout { display: flex; gap: 12px; height: 100%; }
|
||||
.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; margin-bottom: 8px; }
|
||||
.steps-title { font-size: 14px; font-weight: 600; color: #303133; margin-bottom: 8px; text-align: left; }
|
||||
|
||||
/* 卡片式步骤,与示例一致 */
|
||||
.steps-flow { position: relative; }
|
||||
@@ -568,43 +548,12 @@ onMounted(loadLatest)
|
||||
.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;
|
||||
}
|
||||
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.progress-section { margin: 12px 12px 6px 12px; }
|
||||
.progress-section { margin: 0px 12px 0px 12px; }
|
||||
.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; }
|
||||
.progress-head { padding: 8px 12px 0 12px; }
|
||||
.progress-title { display:flex; align-items:center; gap:8px; color:#606266; font-size: 13px; margin-bottom: 6px; }
|
||||
.progress-title .ok-badge { color: #52c41a; font-size: 12px; }
|
||||
.progress-title .title-text { color:#303133; font-weight:600; }
|
||||
|
||||
.current-status {
|
||||
font-size: 12px;
|
||||
|
||||
@@ -7,7 +7,7 @@ type Shop = { id: string; shopName: string }
|
||||
|
||||
const accounts = ref<BanmaAccount[]>([])
|
||||
const accountId = ref<number>()
|
||||
const isCollapsed = ref(false)
|
||||
// 收起功能移除
|
||||
|
||||
const shopList = ref<Shop[]>([])
|
||||
const selectedShops = ref<string[]>([])
|
||||
@@ -220,10 +220,9 @@ async function removeCurrentAccount() {
|
||||
<template>
|
||||
<div class="zebra-root">
|
||||
<div class="layout">
|
||||
<aside :class="['aside', { collapsed: isCollapsed }]">
|
||||
<aside class="aside">
|
||||
<div class="aside-header">
|
||||
<span>操作流程</span>
|
||||
<el-button link @click="isCollapsed = !isCollapsed">{{ isCollapsed ? '展开' : '收起' }}</el-button>
|
||||
</div>
|
||||
<div class="aside-steps">
|
||||
<section class="step step-accounts">
|
||||
@@ -428,7 +427,7 @@ export default {
|
||||
.layout { background: #fff; border-radius: 4px; padding: 12px; box-shadow: 0 2px 4px rgba(0,0,0,0.05); height: 100%; display: grid; grid-template-columns: 220px 1fr; gap: 12px; }
|
||||
.aside { border: 1px solid #ebeef5; border-radius: 4px; padding: 10px; display: flex; flex-direction: column; transition: width 0.2s ease; }
|
||||
.aside.collapsed { width: 56px; overflow: hidden; }
|
||||
.aside-header { display: flex; justify-content: space-between; align-items: center; font-weight: 600; color: #606266; margin-bottom: 8px; }
|
||||
.aside-header { display: flex; justify-content: flex-start; align-items: center; font-weight: 600; color: #606266; margin-bottom: 8px; }
|
||||
.aside-steps { position: relative; }
|
||||
.step { display: grid; grid-template-columns: 24px 1fr; gap: 10px; position: relative; padding: 8px 0; }
|
||||
.step + .step { border-top: 1px dashed #ebeef5; }
|
||||
|
||||
Reference in New Issue
Block a user