This commit is contained in:
2025-09-24 11:10:42 +08:00
parent a72b60be98
commit 05b923b1ac
348 changed files with 611 additions and 8472 deletions

View File

@@ -150,6 +150,12 @@ async function handleLoginSuccess(data: { token: string; permissions?: string })
}
async function logout() {
const token = await authApi.getToken()
if (token) {
await authApi.logout(token)
}
await authApi.deleteTokenCache()
// 清理前端状态
isAuthenticated.value = false
@@ -158,7 +164,8 @@ async function logout() {
showAuthDialog.value = true
showDeviceDialog.value = false
// 关闭SSE连接
// 关闭SSE连接`-+++++++
SSEManager.disconnect()
}

View File

@@ -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; }

View File

@@ -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;

View File

@@ -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; }