|
|
@@ -70,14 +70,18 @@
|
|
|
<el-button type="primary" size="small" @click="openUploadDialog">
|
|
|
<i class="el-icon-upload2"></i> 上传数据
|
|
|
</el-button>
|
|
|
- <el-button size="small" @click="refreshData">
|
|
|
+ <el-button size="small" @click="refreshData" :disabled="uploadTaskPolling">
|
|
|
<i class="el-icon-refresh"></i> 刷新
|
|
|
</el-button>
|
|
|
</div>
|
|
|
</div>
|
|
|
</template>
|
|
|
<div class="upload-hint">
|
|
|
- 支持上传入库、销售、组装、产品资料、半成品映射文件(xlsx/xls)。上传后将自动刷新本页分析。
|
|
|
+ 支持上传入库、销售、组装、产品资料、半成品映射文件(xlsx/xls)。上传后会先创建后台任务,处理完成后自动刷新本页分析。
|
|
|
+ </div>
|
|
|
+ <div v-if="uploadTaskStatusText" class="upload-task-status">
|
|
|
+ <el-tag :type="uploadTaskStatusType" size="mini">{{ uploadTaskStatusLabel }}</el-tag>
|
|
|
+ <span>{{ uploadTaskStatusText }}</span>
|
|
|
</div>
|
|
|
<div v-if="lastUploadSummary" class="upload-summary">
|
|
|
<el-tag type="success" size="mini">最近上传</el-tag>
|
|
|
@@ -105,7 +109,7 @@
|
|
|
<template slot="header">
|
|
|
<div class="card-header">
|
|
|
<span>SKU指标汇总</span>
|
|
|
- <el-button type="primary" size="small" @click="refreshData">
|
|
|
+ <el-button type="primary" size="small" @click="refreshData" :disabled="uploadTaskPolling">
|
|
|
<i class="el-icon-refresh"></i> 刷新
|
|
|
</el-button>
|
|
|
</div>
|
|
|
@@ -165,7 +169,7 @@
|
|
|
<template slot="header">
|
|
|
<div class="card-header">
|
|
|
<span>SPU指标汇总(成品)</span>
|
|
|
- <el-button type="primary" size="small" @click="refreshData">
|
|
|
+ <el-button type="primary" size="small" @click="refreshData" :disabled="uploadTaskPolling">
|
|
|
<i class="el-icon-refresh"></i> 刷新
|
|
|
</el-button>
|
|
|
</div>
|
|
|
@@ -271,8 +275,8 @@
|
|
|
<input ref="productInput" class="hidden-file-input" type="file" accept=".xlsx,.xls" @change="handleFileChange('product', $event)">
|
|
|
<input ref="mappingInput" class="hidden-file-input" type="file" accept=".xlsx,.xls" @change="handleFileChange('mapping', $event)">
|
|
|
<span slot="footer" class="dialog-footer">
|
|
|
- <el-button @click="uploadDialogVisible = false">取消</el-button>
|
|
|
- <el-button type="primary" :loading="uploadLoading" @click="submitUpload">上传并刷新</el-button>
|
|
|
+ <el-button @click="uploadDialogVisible = false" :disabled="uploadLoading">取消</el-button>
|
|
|
+ <el-button type="primary" :loading="uploadLoading" @click="submitUpload">上传并处理</el-button>
|
|
|
</span>
|
|
|
</el-dialog>
|
|
|
</div>
|
|
|
@@ -283,6 +287,9 @@ import request from '@/utils/request'
|
|
|
import * as echarts from 'echarts'
|
|
|
require('echarts/theme/macarons')
|
|
|
|
|
|
+const TASK_POLL_INTERVAL = 2000
|
|
|
+const TASK_POLL_LIMIT = 120
|
|
|
+
|
|
|
export default {
|
|
|
name: 'StorageOverview',
|
|
|
data() {
|
|
|
@@ -317,7 +324,26 @@ export default {
|
|
|
product: '',
|
|
|
mapping: ''
|
|
|
},
|
|
|
- lastUploadSummary: ''
|
|
|
+ lastUploadSummary: '',
|
|
|
+ uploadTaskId: '',
|
|
|
+ uploadTaskStatus: '',
|
|
|
+ uploadTaskStatusText: '',
|
|
|
+ uploadTaskPolling: false
|
|
|
+ }
|
|
|
+ },
|
|
|
+ computed: {
|
|
|
+ uploadTaskStatusType() {
|
|
|
+ if (this.uploadTaskStatus === 'success') return 'success'
|
|
|
+ if (this.uploadTaskStatus === 'failed') return 'danger'
|
|
|
+ if (this.uploadTaskStatus === 'running') return 'warning'
|
|
|
+ return 'info'
|
|
|
+ },
|
|
|
+ uploadTaskStatusLabel() {
|
|
|
+ if (this.uploadTaskStatus === 'success') return '处理完成'
|
|
|
+ if (this.uploadTaskStatus === 'failed') return '处理失败'
|
|
|
+ if (this.uploadTaskStatus === 'running') return '处理中'
|
|
|
+ if (this.uploadTaskStatus === 'pending') return '排队中'
|
|
|
+ return '任务状态'
|
|
|
}
|
|
|
},
|
|
|
mounted() {
|
|
|
@@ -336,7 +362,7 @@ export default {
|
|
|
},
|
|
|
methods: {
|
|
|
inventoryRequest(config) {
|
|
|
- return request({ timeout: 30000, ...config })
|
|
|
+ return request({ timeout: 120000, ...config })
|
|
|
},
|
|
|
normalizeResponse(res) {
|
|
|
if (!res) return null
|
|
|
@@ -444,9 +470,9 @@ export default {
|
|
|
},
|
|
|
async refreshData() {
|
|
|
await this.fetchOverviewData()
|
|
|
- this.fetchMonthlyComparison()
|
|
|
- this.fetchSkuSummary()
|
|
|
- this.fetchSpuSummary()
|
|
|
+ await this.fetchMonthlyComparison()
|
|
|
+ await this.fetchSkuSummary()
|
|
|
+ await this.fetchSpuSummary()
|
|
|
},
|
|
|
openUploadDialog() {
|
|
|
this.uploadDialogVisible = true
|
|
|
@@ -510,30 +536,77 @@ export default {
|
|
|
if (this.uploadFiles.mapping) formData.append('semiMappingFile', this.uploadFiles.mapping)
|
|
|
|
|
|
this.uploadLoading = true
|
|
|
+ this.uploadTaskStatus = 'pending'
|
|
|
+ this.uploadTaskStatusText = '文件上传中,请稍候'
|
|
|
try {
|
|
|
const res = await this.inventoryRequest({
|
|
|
url: '/api/inventory/upload',
|
|
|
method: 'post',
|
|
|
data: formData,
|
|
|
- headers: { 'Content-Type': 'multipart/form-data', repeatSubmit: false }
|
|
|
+ headers: { 'Content-Type': 'multipart/form-data', repeatSubmit: false },
|
|
|
+ timeout: 300000
|
|
|
})
|
|
|
- if (res && res.code === 200) {
|
|
|
- const count = res.data && res.data.count ? res.data.count : 0
|
|
|
- this.lastUploadSummary = `已保存 ${count} 个文件`
|
|
|
- this.$message.success('上传成功,已刷新分析数据')
|
|
|
- this.uploadDialogVisible = false
|
|
|
- this.resetUploadForm()
|
|
|
- await this.refreshData()
|
|
|
- } else {
|
|
|
+ if (!(res && res.code === 200 && res.data && res.data.taskId)) {
|
|
|
this.$message.error((res && res.msg) || '上传失败')
|
|
|
+ this.uploadTaskStatus = 'failed'
|
|
|
+ this.uploadTaskStatusText = (res && res.msg) || '上传失败'
|
|
|
+ return
|
|
|
}
|
|
|
+ this.uploadTaskId = res.data.taskId
|
|
|
+ this.uploadTaskStatus = res.data.status || 'pending'
|
|
|
+ this.uploadTaskStatusText = res.data.message || '任务已创建,等待后台处理'
|
|
|
+ this.lastUploadSummary = `已接收 ${res.data.count || 0} 个文件,任务ID:${this.uploadTaskId}`
|
|
|
+ this.uploadDialogVisible = false
|
|
|
+ this.resetUploadForm()
|
|
|
+ this.$message.success('上传成功,后台正在处理库存数据')
|
|
|
+ await this.pollUploadTask(this.uploadTaskId)
|
|
|
} catch (error) {
|
|
|
console.error('上传库存数据失败:', error)
|
|
|
+ this.uploadTaskStatus = 'failed'
|
|
|
+ this.uploadTaskStatusText = '上传失败,请检查文件格式或后端日志'
|
|
|
this.$message.error('上传失败,请检查文件格式或后端日志')
|
|
|
} finally {
|
|
|
this.uploadLoading = false
|
|
|
}
|
|
|
},
|
|
|
+ async pollUploadTask(taskId) {
|
|
|
+ this.uploadTaskPolling = true
|
|
|
+ try {
|
|
|
+ for (let attempt = 0; attempt < TASK_POLL_LIMIT; attempt++) {
|
|
|
+ const res = await this.inventoryRequest({
|
|
|
+ url: `/api/inventory/upload-task/${taskId}`,
|
|
|
+ method: 'get',
|
|
|
+ timeout: 30000
|
|
|
+ })
|
|
|
+ const data = this.normalizeResponse(res)
|
|
|
+ if (!data) {
|
|
|
+ throw new Error((res && res.msg) || '任务状态获取失败')
|
|
|
+ }
|
|
|
+ this.uploadTaskStatus = data.status || 'pending'
|
|
|
+ this.uploadTaskStatusText = data.message || '后台处理中'
|
|
|
+ if (data.status === 'success') {
|
|
|
+ this.lastUploadSummary = `已处理 ${data.count || 0} 个文件`
|
|
|
+ this.$message.success('库存数据处理完成,已刷新分析结果')
|
|
|
+ await this.refreshData()
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if (data.status === 'failed') {
|
|
|
+ throw new Error(data.message || '库存数据处理失败')
|
|
|
+ }
|
|
|
+ await this.sleep(TASK_POLL_INTERVAL)
|
|
|
+ }
|
|
|
+ throw new Error('库存数据处理超时,请稍后手动刷新')
|
|
|
+ } catch (error) {
|
|
|
+ this.uploadTaskStatus = 'failed'
|
|
|
+ this.uploadTaskStatusText = error.message || '库存数据处理失败'
|
|
|
+ this.$message.error(this.uploadTaskStatusText)
|
|
|
+ } finally {
|
|
|
+ this.uploadTaskPolling = false
|
|
|
+ }
|
|
|
+ },
|
|
|
+ sleep(ms) {
|
|
|
+ return new Promise(resolve => setTimeout(resolve, ms))
|
|
|
+ },
|
|
|
getTurnoverRateType(rate) {
|
|
|
if (rate === 0) return 'info'
|
|
|
if (rate >= 1) return 'success'
|
|
|
@@ -627,6 +700,7 @@ export default {
|
|
|
line-height: 1.6;
|
|
|
}
|
|
|
|
|
|
+.upload-task-status,
|
|
|
.upload-summary {
|
|
|
margin-top: 10px;
|
|
|
display: flex;
|