|
@@ -8,6 +8,12 @@
|
|
|
</div>
|
|
</div>
|
|
|
</header>
|
|
</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>
|
|
|
|
|
+
|
|
|
<!-- 新增:Top 5 商品贡献分析 (移到顶部) -->
|
|
<!-- 新增:Top 5 商品贡献分析 (移到顶部) -->
|
|
|
<div class="chart-card">
|
|
<div class="chart-card">
|
|
|
<div class="top-product-contribution-container">
|
|
<div class="top-product-contribution-container">
|
|
@@ -35,20 +41,6 @@
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
- <!-- 3. 筛选与操作栏 -->
|
|
|
|
|
- <div class="filter-bar">
|
|
|
|
|
- <div class="filters">
|
|
|
|
|
- <span>筛选:</span>
|
|
|
|
|
- <select><option>全部时间</option></select>
|
|
|
|
|
- <select><option>全部渠道</option></select>
|
|
|
|
|
- <select><option>全部区域</option></select>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="actions">
|
|
|
|
|
- <button class="btn-secondary">导出数据</button>
|
|
|
|
|
- <button class="btn-primary" @click="fetchData">刷新</button>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
<!-- 4. 图表卡片区域 -->
|
|
<!-- 4. 图表卡片区域 -->
|
|
|
<div class="charts-container">
|
|
<div class="charts-container">
|
|
|
<!-- 部门图表:销量 + 销售金额 -->
|
|
<!-- 部门图表:销量 + 销售金额 -->
|
|
@@ -97,7 +89,8 @@ export default {
|
|
|
top5TotalSales: 0,
|
|
top5TotalSales: 0,
|
|
|
contributionRatio: 0,
|
|
contributionRatio: 0,
|
|
|
top5Products: []
|
|
top5Products: []
|
|
|
- }
|
|
|
|
|
|
|
+ },
|
|
|
|
|
+ uploadingShop: false
|
|
|
};
|
|
};
|
|
|
},
|
|
},
|
|
|
mounted() {
|
|
mounted() {
|
|
@@ -108,6 +101,55 @@ export default {
|
|
|
window.removeEventListener('resize', this.handleResize);
|
|
window.removeEventListener('resize', this.handleResize);
|
|
|
},
|
|
},
|
|
|
methods: {
|
|
methods: {
|
|
|
|
|
+ triggerShopUpload() {
|
|
|
|
|
+ if (this.uploadingShop) return;
|
|
|
|
|
+ if (this.$refs.shopUploadInput) {
|
|
|
|
|
+ this.$refs.shopUploadInput.click();
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ async handleShopUploadChange(event) {
|
|
|
|
|
+ const files = Array.from((event && event.target && 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 axios.post('/api/shop/import/import-sales-data/upload', formData, {
|
|
|
|
|
+ headers: { 'Content-Type': 'multipart/form-data' }
|
|
|
|
|
+ });
|
|
|
|
|
+ if (data && data.success) {
|
|
|
|
|
+ this.$modal.msgSuccess(data.message || '上传并导入成功');
|
|
|
|
|
+ await this.fetchData();
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (data && data.debug) {
|
|
|
|
|
+ console.error('[shop-upload-debug]', data.debug);
|
|
|
|
|
+ }
|
|
|
|
|
+ this.$modal.msgError((data && data.message) || '上传导入失败');
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ if (error && error.response && error.response.data && error.response.data.debug) {
|
|
|
|
|
+ console.error('[shop-upload-debug]', error.response.data.debug);
|
|
|
|
|
+ }
|
|
|
|
|
+ const msg = error && error.response && error.response.data && error.response.data.message
|
|
|
|
|
+ ? error.response.data.message
|
|
|
|
|
+ : (error && error.message ? error.message : '上传导入失败');
|
|
|
|
|
+ this.$modal.msgError(msg);
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ this.uploadingShop = false;
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
formatNumber(num) {
|
|
formatNumber(num) {
|
|
|
if (!num) return '0';
|
|
if (!num) return '0';
|
|
|
return new Intl.NumberFormat().format(Number(num).toFixed(0));
|
|
return new Intl.NumberFormat().format(Number(num).toFixed(0));
|
|
@@ -446,21 +488,22 @@ export default {
|
|
|
.kpi-comparison.positive { color: #00b42a; }
|
|
.kpi-comparison.positive { color: #00b42a; }
|
|
|
.kpi-comparison.negative { color: #f53f3f; }
|
|
.kpi-comparison.negative { color: #f53f3f; }
|
|
|
|
|
|
|
|
-.filter-bar {
|
|
|
|
|
|
|
+.upload-row {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
- justify-content: space-between;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- background: #fff;
|
|
|
|
|
- padding: 16px 20px;
|
|
|
|
|
- border-radius: 4px;
|
|
|
|
|
- border: 1px solid #e5e6eb;
|
|
|
|
|
|
|
+ justify-content: flex-end;
|
|
|
margin-bottom: 20px;
|
|
margin-bottom: 20px;
|
|
|
}
|
|
}
|
|
|
-.filters { display: flex; align-items: center; gap: 16px; font-size: 14px; color: #4e5969; }
|
|
|
|
|
-.filters select { padding: 4px 8px; border-radius: 4px; border: 1px solid #e5e6eb; }
|
|
|
|
|
-.actions { display: flex; gap: 12px; }
|
|
|
|
|
-.btn-primary { background-color: #1677ff; color: #fff; border: none; padding: 6px 16px; border-radius: 4px; cursor: pointer; }
|
|
|
|
|
-.btn-secondary { background-color: #f2f3f5; color: #4e5969; border: 1px solid #e5e6eb; padding: 6px 16px; border-radius: 4px; cursor: pointer; }
|
|
|
|
|
|
|
+.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 {
|
|
.charts-container {
|
|
|
display: flex;
|
|
display: flex;
|
|
@@ -535,4 +578,4 @@ export default {
|
|
|
color: #333;
|
|
color: #333;
|
|
|
margin-bottom: 20px;
|
|
margin-bottom: 20px;
|
|
|
}
|
|
}
|
|
|
-</style>
|
|
|
|
|
|
|
+</style>
|