|
|
@@ -28,21 +28,12 @@
|
|
|
</div>
|
|
|
</header>
|
|
|
|
|
|
- <div class="upload-row">
|
|
|
- <button class="btn-upload" :disabled="uploadingShop" @click="triggerShopUpload">
|
|
|
- {{ uploadingShop ? '上传中...' : '上传CSV导入' }}
|
|
|
- </button>
|
|
|
- <input
|
|
|
- ref="shopUploadInput"
|
|
|
- type="file"
|
|
|
- accept=".csv"
|
|
|
- multiple
|
|
|
- style="display: none;"
|
|
|
- @change="handleShopUploadChange"
|
|
|
- >
|
|
|
- </div>
|
|
|
-
|
|
|
<div class="chart-card">
|
|
|
+ <OrderLoadingPanel
|
|
|
+ v-if="topProductLoading"
|
|
|
+ title="正在加载店铺价值指标"
|
|
|
+ detail="正在读取 Top 5 商品销售额与贡献占比"
|
|
|
+ />
|
|
|
<div class="top-product-contribution-container">
|
|
|
<div class="kpi-card-grid">
|
|
|
<div class="kpi-card">
|
|
|
@@ -68,6 +59,11 @@
|
|
|
|
|
|
<div class="charts-container">
|
|
|
<div class="chart-card">
|
|
|
+ <OrderLoadingPanel
|
|
|
+ v-if="chartsLoading"
|
|
|
+ title="正在加载部门业绩数据"
|
|
|
+ detail="正在汇总各部门销量与销售额"
|
|
|
+ />
|
|
|
<div class="card-header">
|
|
|
<h3 class="chart-title">各部门业绩分析(销量 / 销售额)</h3>
|
|
|
</div>
|
|
|
@@ -77,6 +73,11 @@
|
|
|
</div>
|
|
|
|
|
|
<div class="chart-card">
|
|
|
+ <OrderLoadingPanel
|
|
|
+ v-if="chartsLoading"
|
|
|
+ title="正在加载渠道业绩数据"
|
|
|
+ detail="正在汇总各渠道销量与销售额"
|
|
|
+ />
|
|
|
<div class="card-header">
|
|
|
<h3 class="chart-title">各渠道业绩分析(销量 / 销售额)</h3>
|
|
|
</div>
|
|
|
@@ -86,6 +87,11 @@
|
|
|
</div>
|
|
|
|
|
|
<div class="chart-card">
|
|
|
+ <OrderLoadingPanel
|
|
|
+ v-if="chartsLoading"
|
|
|
+ title="正在加载平台价值数据"
|
|
|
+ detail="正在分析各平台销量与平均订单价值"
|
|
|
+ />
|
|
|
<div class="card-header">
|
|
|
<h3 class="chart-title">各平台价值分析(销量 / 销售额)</h3>
|
|
|
</div>
|
|
|
@@ -99,18 +105,21 @@
|
|
|
|
|
|
<script>
|
|
|
import * as echarts from 'echarts'
|
|
|
+import OrderLoadingPanel from '../components/OrderLoadingPanel.vue'
|
|
|
import {
|
|
|
getShopChannelContribution,
|
|
|
getShopChannelRoiValue,
|
|
|
getShopChannelTotalContribution,
|
|
|
getShopMaxDate,
|
|
|
getShopTopProductContribution,
|
|
|
- getShopUnitContribution,
|
|
|
- uploadShopValueFiles
|
|
|
+ getShopUnitContribution
|
|
|
} from '@/api/order'
|
|
|
|
|
|
export default {
|
|
|
name: 'ShopValue',
|
|
|
+ components: {
|
|
|
+ OrderLoadingPanel
|
|
|
+ },
|
|
|
data() {
|
|
|
return {
|
|
|
topProductData: {
|
|
|
@@ -119,7 +128,8 @@ export default {
|
|
|
contributionRatio: 0,
|
|
|
top5Products: []
|
|
|
},
|
|
|
- uploadingShop: false,
|
|
|
+ topProductLoading: true,
|
|
|
+ chartsLoading: true,
|
|
|
selectedDate: '',
|
|
|
maxDate: '',
|
|
|
activeTab: '7d',
|
|
|
@@ -146,48 +156,6 @@ export default {
|
|
|
endDate: this.currentDateRange.end
|
|
|
}
|
|
|
},
|
|
|
- triggerShopUpload() {
|
|
|
- if (!this.uploadingShop && this.$refs.shopUploadInput) {
|
|
|
- this.$refs.shopUploadInput.click()
|
|
|
- }
|
|
|
- },
|
|
|
- async handleShopUploadChange(event) {
|
|
|
- const files = Array.from(event?.target?.files || [])
|
|
|
- if (!files.length) return
|
|
|
- const invalid = files.find(file => !String(file.name || '').toLowerCase().endsWith('.csv'))
|
|
|
- if (invalid) {
|
|
|
- this.$modal.msgError('仅支持上传 CSV 文件')
|
|
|
- event.target.value = ''
|
|
|
- return
|
|
|
- }
|
|
|
- await this.uploadShopFiles(files)
|
|
|
- event.target.value = ''
|
|
|
- },
|
|
|
- async uploadShopFiles(files) {
|
|
|
- this.uploadingShop = true
|
|
|
- try {
|
|
|
- const formData = new FormData()
|
|
|
- files.forEach(file => formData.append('files', file))
|
|
|
- const data = await uploadShopValueFiles(formData)
|
|
|
- if (data?.success) {
|
|
|
- this.$modal.msgSuccess(data.message || '上传并导入成功')
|
|
|
- await this.initDashboard()
|
|
|
- return
|
|
|
- }
|
|
|
- if (data?.debug) {
|
|
|
- console.error('[shop-upload-debug]', data.debug)
|
|
|
- }
|
|
|
- this.$modal.msgError(data?.message || '上传导入失败')
|
|
|
- } catch (error) {
|
|
|
- if (error?.response?.data?.debug) {
|
|
|
- console.error('[shop-upload-debug]', error.response.data.debug)
|
|
|
- }
|
|
|
- const msg = error?.response?.data?.message || error?.message || '上传导入失败'
|
|
|
- this.$modal.msgError(msg)
|
|
|
- } finally {
|
|
|
- this.uploadingShop = false
|
|
|
- }
|
|
|
- },
|
|
|
handleDateChange() {
|
|
|
if (!this.selectedDate) {
|
|
|
this.selectedDate = this.maxDate
|
|
|
@@ -324,6 +292,7 @@ export default {
|
|
|
}, true)
|
|
|
},
|
|
|
async fetchTopProductData() {
|
|
|
+ this.topProductLoading = true
|
|
|
try {
|
|
|
const response = await getShopTopProductContribution(this.getQueryParams())
|
|
|
if (!response?.success) return
|
|
|
@@ -342,6 +311,9 @@ export default {
|
|
|
console.error('获取 Top 5 商品贡献失败:', error)
|
|
|
}
|
|
|
},
|
|
|
+ finishTopProductLoading() {
|
|
|
+ this.topProductLoading = false
|
|
|
+ },
|
|
|
initDualIndicatorBarChart(chartEl, data, categoryKey, seriesConfig) {
|
|
|
if (!chartEl) return
|
|
|
const spacePerBar = 120
|
|
|
@@ -451,6 +423,7 @@ export default {
|
|
|
}, true)
|
|
|
},
|
|
|
async fetchData() {
|
|
|
+ this.chartsLoading = true
|
|
|
try {
|
|
|
const params = this.getQueryParams()
|
|
|
const [unitRes, channelTotalRes, channelContributionRes, channelRoiValueRes] = await Promise.all([
|
|
|
@@ -504,9 +477,13 @@ export default {
|
|
|
}
|
|
|
|
|
|
await this.fetchTopProductData()
|
|
|
+ this.finishTopProductLoading()
|
|
|
+ this.chartsLoading = false
|
|
|
} catch (error) {
|
|
|
console.error('获取店铺价值数据失败:', error)
|
|
|
}
|
|
|
+ this.chartsLoading = false
|
|
|
+ this.finishTopProductLoading()
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
@@ -575,28 +552,6 @@ export default {
|
|
|
border-color: #188df0;
|
|
|
}
|
|
|
|
|
|
-.upload-row {
|
|
|
- display: flex;
|
|
|
- justify-content: flex-end;
|
|
|
- margin-bottom: 20px;
|
|
|
-}
|
|
|
-
|
|
|
-.btn-upload {
|
|
|
- background-color: #2a7f62;
|
|
|
- color: #fff;
|
|
|
- border: none;
|
|
|
- padding: 12px 28px;
|
|
|
- border-radius: 8px;
|
|
|
- cursor: pointer;
|
|
|
- font-size: 16px;
|
|
|
- font-weight: 600;
|
|
|
-}
|
|
|
-
|
|
|
-.btn-upload:disabled {
|
|
|
- opacity: 0.7;
|
|
|
- cursor: not-allowed;
|
|
|
-}
|
|
|
-
|
|
|
.charts-container {
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
@@ -604,6 +559,7 @@ export default {
|
|
|
}
|
|
|
|
|
|
.chart-card {
|
|
|
+ position: relative;
|
|
|
background: #fff;
|
|
|
padding: 24px;
|
|
|
border-radius: 4px;
|