@@ -21,11 +21,16 @@ | |||
] | |||
}, | |||
"dependencies": { | |||
"@babel/parser": "^7.7.4", | |||
"@babel/runtime": "^7.27.6", | |||
"@riophae/vue-treeselect": "0.4.0", | |||
"axios": "0.24.0", | |||
"beautifier": "^0.1.7", | |||
"bpmn-js": "^7.2.1", | |||
"china-area-data": "^5.0.1", | |||
"clipboard": "2.0.8", | |||
"core-js": "3.25.3", | |||
"core-js": "^3.44.0", | |||
"css-loader": "^3.5.3", | |||
"echarts": "5.4.0", | |||
"element-ui": "2.15.14", | |||
"file-saver": "2.0.5", | |||
@@ -34,27 +39,24 @@ | |||
"js-beautify": "1.13.0", | |||
"js-cookie": "3.0.1", | |||
"jsencrypt": "3.0.0-rc.1", | |||
"moment": "^2.30.1", | |||
"npm": "^6.13.7", | |||
"nprogress": "0.2.0", | |||
"quill": "1.3.7", | |||
"screenfull": "5.0.2", | |||
"sortablejs": "1.10.2", | |||
"voca": "^1.4.0", | |||
"vue": "2.6.12", | |||
"vue-barcode": "^1.3.0", | |||
"vue-codemirror": "^4.0.6", | |||
"vue-count-to": "1.0.13", | |||
"vue-cropper": "0.5.5", | |||
"vue-meta": "2.4.0", | |||
"vue-quill-editor": "^3.0.6", | |||
"vue-router": "3.4.9", | |||
"vuedraggable": "2.24.3", | |||
"vuex": "3.6.0", | |||
"workflow-bpmn-modeler": "^0.2.8", | |||
"@babel/parser": "^7.7.4", | |||
"beautifier": "^0.1.7", | |||
"china-area-data": "^5.0.1", | |||
"css-loader": "^3.5.3", | |||
"npm": "^6.13.7", | |||
"voca": "^1.4.0", | |||
"vue-barcode": "^1.3.0", | |||
"vue-codemirror": "^4.0.6", | |||
"vue-quill-editor": "^3.0.6" | |||
"workflow-bpmn-modeler": "^0.2.8" | |||
}, | |||
"devDependencies": { | |||
"@vue/cli-plugin-babel": "4.4.6", |
@@ -71,7 +71,7 @@ export default { | |||
this.isLoading = true | |||
const response = await getProductCodeSuggestions({ keyword: queryString.trim() }) | |||
if (response.success) { | |||
if (response.code === 200) { | |||
callback(response.data || []) | |||
} else { | |||
callback([]) |
@@ -93,78 +93,6 @@ export const constantRoutes = [ | |||
component: () => import("@/views/m/checkin"), | |||
meta: { title: "考勤打卡", icon: "date" }, | |||
}, | |||
{ | |||
path: "/sales-analysis", | |||
component: Layout, | |||
meta: { title: "销售分析", icon: "dashboard", noCache: true }, | |||
children: [ | |||
// 基准数据管理 | |||
{ | |||
path: "/sales-analysis/categories", | |||
name: "Categories", | |||
component: () => | |||
import("@/views/sales-analysis/categories/Categories.vue"), | |||
meta: { title: "分类管理", noCache: true }, | |||
}, | |||
{ | |||
path: "/sales-analysis/base-data", | |||
name: "BaseData", | |||
component: () => | |||
import("@/views/sales-analysis/base-data/BaseData.vue"), | |||
meta: { title: "基准数据", noCache: true }, | |||
}, | |||
{ | |||
path: "/sales-analysis/shop-customer", | |||
name: "ShopCustomer", | |||
component: () => | |||
import("@/views/sales-analysis/shop-customer/ShopCustomer.vue"), | |||
meta: { title: "店铺客户关联", noCache: true }, | |||
}, | |||
// 分析数据管理 | |||
{ | |||
path: "/sales-analysis/analysis-data/import", | |||
name: "ImportData", | |||
component: () => | |||
import("@/views/sales-analysis/analysis-data/ImportData.vue"), | |||
meta: { title: "导入数据", noCache: true }, | |||
}, | |||
{ | |||
path: "/sales-analysis/analysis-data/list", | |||
name: "DataList", | |||
component: () => | |||
import("@/views/sales-analysis/analysis-data/DataList.vue"), | |||
meta: { title: "数据展示", noCache: true }, | |||
}, | |||
{ | |||
path: "/sales-analysis/reports/overall", | |||
name: "OverallAnalysis", | |||
component: () => | |||
import("@/views/sales-analysis/reports/OverallAnalysis.vue"), | |||
meta: { title: "整体销售分析", noCache: true }, | |||
}, | |||
{ | |||
path: "/sales-analysis/reports/shop", | |||
name: "ShopReports", | |||
component: () => | |||
import("@/views/sales-analysis/reports/ShopAnalysis.vue"), | |||
meta: { title: "店铺销售分析", noCache: true }, | |||
}, | |||
{ | |||
path: "/sales-analysis/reports/category", | |||
name: "CategoryReports", | |||
component: () => | |||
import("@/views/sales-analysis/reports/CategoryAnalysis.vue"), | |||
meta: { title: "品类销售分析", noCache: true }, | |||
}, | |||
{ | |||
path: "/sales-analysis/reports/product", | |||
name: "ProductReports", | |||
component: () => | |||
import("@/views/sales-analysis/reports/ProductAnalysis.vue"), | |||
meta: { title: "单品销售分析", noCache: true }, | |||
}, | |||
], | |||
}, | |||
]; | |||
// 动态路由,基于用户权限动态去加载 |
@@ -189,6 +189,7 @@ | |||
:total="pagination.total" | |||
:page.sync="pagination.page" | |||
:limit.sync="pagination.pageSize" | |||
:page-sizes="[50, 100, 300, 500, 1000]" | |||
@pagination="fetchData" | |||
/> | |||
@@ -154,8 +154,8 @@ | |||
show-icon | |||
:closable="false" | |||
> | |||
<p>请确保Excel文件包含以下列:</p> | |||
<p> | |||
<p style="color: #303030;">当前选择分类是:<u>{{ selectedCategory.name }}</u>,请确保Excel文件包含以下列:</p> | |||
<p style="color: #303030;"> | |||
<strong | |||
>出库日期、目的地、店铺名称、出库类型、发送方式、发送番号、注文番号、商品编号、商品名称、数量、单价、送料、代引、客户名称、备注</strong | |||
> | |||
@@ -170,8 +170,8 @@ | |||
show-icon | |||
:closable="false" | |||
> | |||
<p>请确保Excel文件包含以下列:</p> | |||
<p> | |||
<p style="color: #303030;">当前选择的店铺是:<u>{{ selectedShopName }}</u>,请确保Excel文件包含以下列:</p> | |||
<p style="color: #303030;"> | |||
<strong | |||
>出荷日、出品者SKU、FNSKU、ASIN、FC、数量、Amazon注文番号、通貨、商品金額(商品1点ごと)、配送料、ギフト包装手数料、配送先(市区町村)、都道府県名、配送先(郵便番号)、付与されたAmazon | |||
ポイント</strong | |||
@@ -197,7 +197,7 @@ | |||
<i class="el-icon-upload"></i> | |||
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div> | |||
<div class="el-upload__tip" slot="tip"> | |||
支持多选Excel文件(.xlsx/.xls),单文件不超过10MB | |||
支持多选Excel文件(.xlsx/.xls),单文件不超过10MB,文件数量不超过40个 | |||
<el-button @click="clearAllFiles" type="text" | |||
>清空文件列表</el-button | |||
> | |||
@@ -261,8 +261,8 @@ | |||
:data="processResult.data.slice(0, 50)" | |||
stripe | |||
border | |||
size="small" | |||
height="calc(100vh - 470px)" | |||
size="mini" | |||
height="calc(100vh - 490px)" | |||
> | |||
<el-table-column | |||
prop="rowNumber" |
@@ -1,5 +1,5 @@ | |||
<template> | |||
<div class="overall-analysis-page"> | |||
<div class="app-container"> | |||
<!-- 筛选 --> | |||
<div class="filter-card"> | |||
<div class="filter-section"> | |||
@@ -56,35 +56,13 @@ | |||
:key="dimension" | |||
class="dimension-section" | |||
> | |||
<div class="dimension-header"> | |||
<div class="dimension-title"> | |||
<i class="el-icon-data-analysis"></i> | |||
<span>{{ dimension }}维度分析</span> | |||
</div> | |||
<div class="dimension-stats"> | |||
<span class="stat-item category-stat"> | |||
<i class="el-icon-folder-opened"></i> | |||
归属分类: <strong>{{ currentCategory }}</strong> | |||
</span> | |||
<span class="stat-item"> | |||
<i class="el-icon-money"></i> | |||
金额项: {{ chartData.amountData.length }} | |||
</span> | |||
<span class="stat-item"> | |||
<i class="el-icon-s-goods"></i> | |||
数量项: {{ chartData.quantityData.length }} | |||
</span> | |||
</div> | |||
</div> | |||
<el-row :gutter="24"> | |||
<el-row :gutter="10"> | |||
<el-col :span="12"> | |||
<!-- 销售金额占比 --> | |||
<div class="chart-card"> | |||
<div class="card-header"> | |||
<div class="header-left"> | |||
<i class="el-icon-money header-icon-amount"></i> | |||
<span>销售金额占比</span> | |||
<span>{{ currentCategory }} {{ dimension }} 销售金额占比</span> | |||
</div> | |||
<div class="header-right"> | |||
<span class="total-amount" | |||
@@ -108,8 +86,7 @@ | |||
<div class="chart-card"> | |||
<div class="card-header"> | |||
<div class="header-left"> | |||
<i class="el-icon-s-goods header-icon-quantity"></i> | |||
<span>销售数量占比</span> | |||
<span>{{ currentCategory }} {{ dimension }} 销售数量占比</span> | |||
</div> | |||
<div class="header-right"> | |||
<span class="total-quantity" | |||
@@ -163,14 +140,35 @@ export default { | |||
dimensionCharts: {}, | |||
chartInstances: new Map(), // 存储图表实例 | |||
currentCategory: "全部分类", | |||
resizeTimer: null, // 防抖定时器 | |||
resizeObserver: null, // ResizeObserver 实例 | |||
}; | |||
}, | |||
mounted() { | |||
this.initDateRange(); | |||
this.fetchFilterOptions(); | |||
// this.fetchData(); | |||
// 添加窗口大小变化监听 | |||
window.addEventListener('resize', this.handleResize); | |||
// 使用 ResizeObserver 监听容器尺寸变化 | |||
this.initResizeObserver(); | |||
}, | |||
beforeDestroy() { | |||
// 移除窗口大小变化监听 | |||
window.removeEventListener('resize', this.handleResize); | |||
// 清除防抖定时器 | |||
if (this.resizeTimer) { | |||
clearTimeout(this.resizeTimer); | |||
} | |||
// 断开 ResizeObserver | |||
if (this.resizeObserver) { | |||
this.resizeObserver.disconnect(); | |||
} | |||
// 销毁所有图表实例 | |||
this.chartInstances.forEach((chart) => { | |||
if (chart) { | |||
@@ -249,6 +247,11 @@ export default { | |||
this.$nextTick(() => { | |||
this.renderAllCharts(); | |||
// 数据更新后,确保图表能够响应 resize | |||
setTimeout(() => { | |||
this.handleResize(); | |||
}, 200); | |||
}); | |||
} else { | |||
this.$message.error(response.message || "获取数据失败"); | |||
@@ -296,6 +299,10 @@ export default { | |||
if (!chartDom || !data || data.length === 0) return; | |||
const chart = echarts.init(chartDom); | |||
// 为图表添加 resize 监听 | |||
chart.resize = chart.resize.bind(chart); | |||
this.chartInstances.set(chartId, chart); | |||
const totalAmount = this.getTotalAmount(data); | |||
const processedData = this.processChartData(data); | |||
@@ -437,6 +444,10 @@ export default { | |||
if (!chartDom || !data || data.length === 0) return; | |||
const chart = echarts.init(chartDom); | |||
// 为图表添加 resize 监听 | |||
chart.resize = chart.resize.bind(chart); | |||
this.chartInstances.set(chartId, chart); | |||
const totalQuantity = this.getTotalQuantity(data); | |||
const processedData = this.processChartData(data, false); | |||
@@ -657,19 +668,62 @@ export default { | |||
getTotalQuantity(data) { | |||
return data.reduce((sum, item) => sum + (item.value || 0), 0); | |||
}, | |||
// 处理窗口大小变化 | |||
handleResize() { | |||
console.log('窗口大小变化,触发 resize 处理'); | |||
// 使用防抖处理,避免频繁调用 | |||
if (this.resizeTimer) { | |||
clearTimeout(this.resizeTimer); | |||
} | |||
this.resizeTimer = setTimeout(() => { | |||
console.log('执行图表 resize'); | |||
// 强制触发 DOM 重新计算 | |||
this.$forceUpdate(); | |||
// 遍历所有图表实例并执行 resize | |||
this.chartInstances.forEach((chart, chartId) => { | |||
if (chart && chart.getDom()) { | |||
console.log(`resize 图表: ${chartId}`); | |||
chart.resize(); | |||
} | |||
}); | |||
}, 100); | |||
}, | |||
// 初始化 ResizeObserver | |||
initResizeObserver() { | |||
if (window.ResizeObserver) { | |||
this.resizeObserver = new ResizeObserver((entries) => { | |||
console.log('容器尺寸变化检测到'); | |||
this.handleResize(); | |||
}); | |||
// 监听图表容器 | |||
this.$nextTick(() => { | |||
const chartContainers = document.querySelectorAll('.chart-container'); | |||
chartContainers.forEach(container => { | |||
this.resizeObserver.observe(container); | |||
}); | |||
}); | |||
} | |||
}, | |||
}, | |||
}; | |||
</script> | |||
<style scoped> | |||
.overall-analysis-page { | |||
height: 100vh; | |||
overflow-y: auto; | |||
margin: -20px; | |||
padding: 10px; | |||
.app-container { | |||
display: flex; | |||
flex-direction: column; | |||
gap: 15px; | |||
gap: 10px; | |||
padding: 10px; | |||
width: 100%; | |||
min-height: 100vh; | |||
box-sizing: border-box; | |||
} | |||
.filter-card { | |||
@@ -714,7 +768,7 @@ export default { | |||
.stats-cards { | |||
display: grid; | |||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); | |||
gap: 15px; | |||
gap: 10px; | |||
} | |||
.stat-card { | |||
@@ -723,7 +777,7 @@ export default { | |||
border-radius: 8px; | |||
display: flex; | |||
align-items: center; | |||
gap: 15px; | |||
gap: 10px; | |||
border: 1px solid #e8e8e8; | |||
transition: background-color 0.2s ease; | |||
} | |||
@@ -793,15 +847,17 @@ export default { | |||
} | |||
.charts-section { | |||
width: 100%; | |||
min-width: 0; | |||
display: flex; | |||
flex-direction: column; | |||
gap: 30px; | |||
gap: 10px; | |||
} | |||
.dimension-section { | |||
display: flex; | |||
flex-direction: column; | |||
gap: 20px; | |||
gap: 10px; | |||
overflow: hidden; | |||
} | |||
@@ -809,10 +865,7 @@ export default { | |||
display: flex; | |||
justify-content: space-between; | |||
align-items: center; | |||
padding: 15px 20px; | |||
background: #f8f9fa; | |||
border-radius: 8px; | |||
border-bottom: 1px solid #e8e8e8; | |||
padding: 15px 0; | |||
} | |||
.dimension-title { | |||
@@ -820,7 +873,6 @@ export default { | |||
align-items: center; | |||
gap: 10px; | |||
font-size: 18px; | |||
font-weight: 600; | |||
color: #333; | |||
} | |||
@@ -831,7 +883,7 @@ export default { | |||
.dimension-stats { | |||
display: flex; | |||
gap: 20px; | |||
gap: 10px; | |||
} | |||
.stat-item { | |||
@@ -862,6 +914,9 @@ export default { | |||
.chart-container { | |||
padding: 15px 0; | |||
width: 100%; | |||
height: auto; | |||
min-height: 0; | |||
overflow: hidden; | |||
} | |||
.card-header { | |||
@@ -877,7 +932,6 @@ export default { | |||
display: flex; | |||
align-items: center; | |||
gap: 8px; | |||
font-weight: 600; | |||
font-size: 16px; | |||
color: #333; | |||
} |
@@ -1,5 +1,5 @@ | |||
<template> | |||
<div class="overall-analysis-page"> | |||
<div class="app-container"> | |||
<!-- 筛选 --> | |||
<div class="filter-card"> | |||
<div class="filter-section"> | |||
@@ -267,6 +267,8 @@ export default { | |||
trendChart: null, | |||
topProductsChart: null, | |||
brandChart: null, | |||
resizeTimer: null, // 防抖定时器 | |||
resizeObserver: null, // ResizeObserver 实例 | |||
}; | |||
}, | |||
filters: { | |||
@@ -292,8 +294,26 @@ export default { | |||
this.initDateRange(); | |||
this.fetchFilterOptions(); | |||
this.fetchData(); | |||
// 添加窗口大小变化监听 | |||
window.addEventListener('resize', this.handleResize); | |||
// 使用 ResizeObserver 监听容器尺寸变化 | |||
this.initResizeObserver(); | |||
}, | |||
beforeDestroy() { | |||
// 移除窗口大小变化监听 | |||
window.removeEventListener('resize', this.handleResize); | |||
// 清除防抖定时器 | |||
if (this.resizeTimer) { | |||
clearTimeout(this.resizeTimer); | |||
} | |||
// 断开 ResizeObserver | |||
if (this.resizeObserver) { | |||
this.resizeObserver.disconnect(); | |||
} | |||
if (this.trendChart) { | |||
this.trendChart.dispose(); | |||
} | |||
@@ -401,6 +421,11 @@ export default { | |||
this.renderTrendChart(); | |||
this.renderTopProductsChart(); | |||
this.renderBrandChart(); | |||
// 数据更新后,确保图表能够响应 resize | |||
setTimeout(() => { | |||
this.handleResize(); | |||
}, 200); | |||
}); | |||
} else { | |||
this.$message.error(response.message || "获取数据失败"); | |||
@@ -431,6 +456,9 @@ export default { | |||
} | |||
this.trendChart = echarts.init(chartDom); | |||
// 为图表添加 resize 监听 | |||
this.trendChart.resize = this.trendChart.resize.bind(this.trendChart); | |||
const dates = this.statsData.trendData.map((item) => item.date); | |||
const amounts = this.statsData.trendData.map((item) => item.amount); | |||
@@ -451,7 +479,7 @@ export default { | |||
tooltipText += `${param.marker} ${param.seriesName}: `; | |||
const value = param.value || 0; | |||
if (param.seriesName === "销售额") { | |||
tooltipText += `¥${this.formatNumber(value.toFixed(0))}`; | |||
tooltipText += `¥${this.formatNumber(value)}`; | |||
} else { | |||
tooltipText += `${this.formatNumber(value)}`; | |||
} | |||
@@ -605,6 +633,10 @@ export default { | |||
} | |||
this.topProductsChart = echarts.init(chartDom); | |||
// 为图表添加 resize 监听 | |||
this.topProductsChart.resize = this.topProductsChart.resize.bind(this.topProductsChart); | |||
const data = [...this.statsData.topProducts] | |||
.sort((a, b) => a.amount - b.amount) | |||
.map((item) => ({ | |||
@@ -710,6 +742,9 @@ export default { | |||
} | |||
this.brandChart = echarts.init(chartDom); | |||
// 为图表添加 resize 监听 | |||
this.brandChart.resize = this.brandChart.resize.bind(this.brandChart); | |||
const brands = this.statsData.brandData.map( | |||
(item) => item.brand | |||
@@ -885,19 +920,68 @@ export default { | |||
if (rate < 0) return "growth-negative"; | |||
return "growth-neutral"; | |||
}, | |||
// 处理窗口大小变化 | |||
handleResize() { | |||
console.log('窗口大小变化,触发 resize 处理'); | |||
// 使用防抖处理,避免频繁调用 | |||
if (this.resizeTimer) { | |||
clearTimeout(this.resizeTimer); | |||
} | |||
this.resizeTimer = setTimeout(() => { | |||
console.log('执行图表 resize'); | |||
// 强制触发 DOM 重新计算 | |||
this.$forceUpdate(); | |||
// 检查图表是否存在且已初始化 | |||
if (this.trendChart && this.trendChart.getDom()) { | |||
console.log('resize 趋势图'); | |||
this.trendChart.resize(); | |||
} | |||
if (this.topProductsChart && this.topProductsChart.getDom()) { | |||
console.log('resize TOP20商品图'); | |||
this.topProductsChart.resize(); | |||
} | |||
if (this.brandChart && this.brandChart.getDom()) { | |||
console.log('resize 品牌图'); | |||
this.brandChart.resize(); | |||
} | |||
}, 100); | |||
}, | |||
// 初始化 ResizeObserver | |||
initResizeObserver() { | |||
if (window.ResizeObserver) { | |||
this.resizeObserver = new ResizeObserver((entries) => { | |||
console.log('容器尺寸变化检测到'); | |||
this.handleResize(); | |||
}); | |||
// 监听图表容器 | |||
this.$nextTick(() => { | |||
const chartContainers = document.querySelectorAll('.chart-container'); | |||
chartContainers.forEach(container => { | |||
this.resizeObserver.observe(container); | |||
}); | |||
}); | |||
} | |||
}, | |||
}, | |||
}; | |||
</script> | |||
<style scoped> | |||
.overall-analysis-page { | |||
height: 100vh; | |||
overflow-y: auto; | |||
margin: -20px; | |||
padding: 10px; | |||
.app-container { | |||
display: flex; | |||
flex-direction: column; | |||
gap: 10px; | |||
padding: 10px; | |||
width: 100%; | |||
min-height: 100vh; | |||
box-sizing: border-box; | |||
} | |||
.filter-card { | |||
background-color: #fff; | |||
@@ -1016,9 +1100,11 @@ export default { | |||
} | |||
.charts-section { | |||
display: grid; | |||
grid-template-columns: 1fr; | |||
gap: 20px; | |||
width: 100%; | |||
min-width: 0; | |||
display: flex; | |||
flex-direction: column; | |||
gap: 10px; | |||
} | |||
.chart-card { | |||
@@ -1027,6 +1113,9 @@ export default { | |||
.chart-container { | |||
width: 100%; | |||
height: auto; | |||
min-height: 0; | |||
overflow: hidden; | |||
} | |||
.card-header { |
@@ -1,5 +1,5 @@ | |||
<template> | |||
<div class="product-analysis-page"> | |||
<div class="app-container"> | |||
<!-- 筛选 --> | |||
<div class="filter-card"> | |||
<div class="filter-section"> | |||
@@ -99,6 +99,8 @@ export default { | |||
}, | |||
trendChart: null, | |||
rankingChart: null, | |||
resizeTimer: null, // 防抖定时器 | |||
resizeObserver: null, // ResizeObserver 实例 | |||
}; | |||
}, | |||
computed: { | |||
@@ -114,8 +116,32 @@ export default { | |||
this.initDateRange(); | |||
this.fetchFilterOptions(); | |||
// this.fetchData(); | |||
// 添加窗口大小变化监听 | |||
window.addEventListener('resize', this.handleResize); | |||
// 使用 ResizeObserver 监听容器尺寸变化 | |||
this.initResizeObserver(); | |||
// 初始化图表实例,即使没有数据也要创建 | |||
this.$nextTick(() => { | |||
this.initCharts(); | |||
}); | |||
}, | |||
beforeDestroy() { | |||
// 移除窗口大小变化监听 | |||
window.removeEventListener('resize', this.handleResize); | |||
// 清除防抖定时器 | |||
if (this.resizeTimer) { | |||
clearTimeout(this.resizeTimer); | |||
} | |||
// 断开 ResizeObserver | |||
if (this.resizeObserver) { | |||
this.resizeObserver.disconnect(); | |||
} | |||
if (this.trendChart) { | |||
this.trendChart.dispose(); | |||
} | |||
@@ -184,6 +210,11 @@ export default { | |||
this.$nextTick(() => { | |||
this.renderTrendChart(); | |||
this.renderRankingChart(); | |||
// 数据更新后,确保图表能够响应 resize | |||
setTimeout(() => { | |||
this.handleResize(); | |||
}, 200); | |||
}); | |||
} else { | |||
this.$message.error(response.message || "获取数据失败"); | |||
@@ -211,13 +242,15 @@ export default { | |||
const chartDom = document.getElementById("trendChart"); | |||
if (!chartDom) return; | |||
if (this.trendChart) { | |||
this.trendChart.dispose(); | |||
// 如果图表实例不存在,先初始化 | |||
if (!this.trendChart) { | |||
this.trendChart = echarts.init(chartDom); | |||
this.trendChart.resize = this.trendChart.resize.bind(this.trendChart); | |||
} | |||
this.trendChart = echarts.init(chartDom); | |||
if (!this.statsData.trendData || this.statsData.trendData.length === 0) { | |||
this.trendChart.clear(); | |||
// 即使没有数据也要保持图表实例,以便 resize 功能正常工作 | |||
return; | |||
} | |||
@@ -296,16 +329,18 @@ export default { | |||
const chartDom = document.getElementById("rankingChart"); | |||
if (!chartDom) return; | |||
if (this.rankingChart) { | |||
this.rankingChart.dispose(); | |||
// 如果图表实例不存在,先初始化 | |||
if (!this.rankingChart) { | |||
this.rankingChart = echarts.init(chartDom); | |||
this.rankingChart.resize = this.rankingChart.resize.bind(this.rankingChart); | |||
} | |||
this.rankingChart = echarts.init(chartDom); | |||
if ( | |||
!this.statsData.rankingData || | |||
this.statsData.rankingData.length === 0 | |||
) { | |||
this.rankingChart.clear(); | |||
// 即使没有数据也要保持图表实例,以便 resize 功能正常工作 | |||
return; | |||
} | |||
@@ -413,19 +448,124 @@ export default { | |||
if (num === null || num === undefined) return "0"; | |||
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); | |||
}, | |||
// 处理窗口大小变化 | |||
handleResize() { | |||
console.log('窗口大小变化,触发 resize 处理'); | |||
// 使用防抖处理,避免频繁调用 | |||
if (this.resizeTimer) { | |||
clearTimeout(this.resizeTimer); | |||
} | |||
this.resizeTimer = setTimeout(() => { | |||
console.log('执行图表 resize'); | |||
// 强制触发 DOM 重新计算 | |||
this.$forceUpdate(); | |||
// 延迟执行 resize,确保 DOM 更新完成 | |||
this.$nextTick(() => { | |||
// 检查图表是否存在且已初始化 | |||
if (this.trendChart && this.trendChart.getDom()) { | |||
console.log('resize 趋势图'); | |||
this.trendChart.resize(); | |||
} else { | |||
console.log('趋势图不存在或未初始化'); | |||
} | |||
if (this.rankingChart && this.rankingChart.getDom()) { | |||
console.log('resize 排行图'); | |||
this.rankingChart.resize(); | |||
} else { | |||
console.log('排行图不存在或未初始化'); | |||
} | |||
// 额外延迟执行强制 resize,确保大变小的情况也能处理 | |||
setTimeout(() => { | |||
this.forceChartResize(); | |||
}, 200); | |||
}); | |||
}, 100); | |||
}, | |||
// 初始化 ResizeObserver | |||
initResizeObserver() { | |||
if (window.ResizeObserver) { | |||
this.resizeObserver = new ResizeObserver((entries) => { | |||
console.log('容器尺寸变化检测到'); | |||
this.handleResize(); | |||
}); | |||
// 监听图表容器 | |||
this.$nextTick(() => { | |||
const chartContainers = document.querySelectorAll('.chart-container'); | |||
chartContainers.forEach(container => { | |||
this.resizeObserver.observe(container); | |||
}); | |||
}); | |||
} | |||
}, | |||
// 强制图表 resize | |||
forceChartResize() { | |||
console.log('强制图表 resize'); | |||
// 强制所有图表重新计算尺寸 | |||
if (this.trendChart && this.trendChart.getDom()) { | |||
const container = this.trendChart.getDom(); | |||
// 临时改变容器尺寸,然后恢复,强制触发 resize | |||
const originalWidth = container.style.width; | |||
container.style.width = '99%'; | |||
setTimeout(() => { | |||
container.style.width = originalWidth; | |||
this.trendChart.resize(); | |||
}, 10); | |||
} | |||
if (this.rankingChart && this.rankingChart.getDom()) { | |||
const container = this.rankingChart.getDom(); | |||
const originalWidth = container.style.width; | |||
container.style.width = '99%'; | |||
setTimeout(() => { | |||
container.style.width = originalWidth; | |||
this.rankingChart.resize(); | |||
}, 10); | |||
} | |||
}, | |||
// 初始化图表实例 | |||
initCharts() { | |||
console.log('初始化图表实例'); | |||
// 初始化趋势图 | |||
const trendChartDom = document.getElementById("trendChart"); | |||
if (trendChartDom && !this.trendChart) { | |||
this.trendChart = echarts.init(trendChartDom); | |||
this.trendChart.resize = this.trendChart.resize.bind(this.trendChart); | |||
console.log('趋势图实例已创建'); | |||
} | |||
// 初始化排行图 | |||
const rankingChartDom = document.getElementById("rankingChart"); | |||
if (rankingChartDom && !this.rankingChart) { | |||
this.rankingChart = echarts.init(rankingChartDom); | |||
this.rankingChart.resize = this.rankingChart.resize.bind(this.rankingChart); | |||
console.log('排行图实例已创建'); | |||
} | |||
}, | |||
}, | |||
}; | |||
</script> | |||
<style scoped> | |||
.product-analysis-page { | |||
height: 100vh; | |||
overflow-y: auto; | |||
margin: -20px; | |||
padding: 10px; | |||
.app-container { | |||
display: flex; | |||
flex-direction: column; | |||
gap: 15px; | |||
gap: 10px; | |||
padding: 10px; | |||
width: 100%; | |||
min-height: 100vh; | |||
box-sizing: border-box; | |||
} | |||
.filter-card { | |||
background-color: #fff; | |||
@@ -456,9 +596,11 @@ export default { | |||
} | |||
.charts-section { | |||
width: 100%; | |||
min-width: 0; | |||
display: flex; | |||
flex-direction: column; | |||
gap: 20px; | |||
gap: 10px; | |||
} | |||
.chart-card { | |||
@@ -470,13 +612,16 @@ export default { | |||
.card-header { | |||
display: flex; | |||
align-items: center; | |||
font-weight: 600; | |||
justify-content: center; | |||
font-size: 18px; | |||
padding: 15px 20px; | |||
border-bottom: 1px solid #e8e8e8; | |||
} | |||
.chart-container { | |||
padding: 20px; | |||
width: 100%; | |||
height: auto; | |||
min-height: 0; | |||
overflow: hidden; | |||
} | |||
.no-data-section { | |||
display: flex; |
@@ -1,5 +1,5 @@ | |||
<template> | |||
<div class="overall-analysis-page"> | |||
<div class="app-container"> | |||
<!-- 筛选 --> | |||
<div class="filter-card"> | |||
<div class="filter-section"> | |||
@@ -24,8 +24,6 @@ | |||
</el-select> | |||
</div> | |||
<div class="filter-item"> | |||
<label>日期范围:</label> | |||
<el-date-picker | |||
@@ -43,7 +41,12 @@ | |||
</div> | |||
<div class="filter-item"> | |||
<el-button type="primary" @click="fetchData" :loading="loading" size="small"> | |||
<el-button | |||
type="primary" | |||
@click="fetchData" | |||
:loading="loading" | |||
size="small" | |||
> | |||
查询分析 | |||
</el-button> | |||
<el-button @click="resetFilter" size="small">重置筛选</el-button> | |||
@@ -52,10 +55,9 @@ | |||
</div> | |||
</div> | |||
<!-- 图表区域 --> | |||
<div class="charts-section"> | |||
<el-row :gutter="20"> | |||
<el-row :gutter="10"> | |||
<el-col :span="12"> | |||
<!-- 各店铺销售金额占比 --> | |||
<div class="chart-card"> | |||
@@ -86,25 +88,25 @@ | |||
</el-col> | |||
</el-row> | |||
<!-- 各品类销售趋势 --> | |||
<div class="chart-card"> | |||
<div class="card-header"> | |||
<span>各品类销售数量和销售额趋势</span> | |||
</div> | |||
<div class="chart-container"> | |||
<div id="categoryTrendChart" style="width: 100%; height: 500px"></div> | |||
</div> | |||
<!-- 各品类销售趋势 --> | |||
<div class="chart-card"> | |||
<div class="card-header"> | |||
<span>各品类销售数量和销售额趋势</span> | |||
</div> | |||
<div class="chart-container"> | |||
<div id="categoryTrendChart" style="width: 100%; height: 500px"></div> | |||
</div> | |||
</div> | |||
</div> | |||
</div> | |||
</template> | |||
<script> | |||
import * as echarts from "echarts"; | |||
import { getReportFilterOptions, getShopAnalysisReport } from '@/api/sales-analysis' | |||
import { | |||
getReportFilterOptions, | |||
getShopAnalysisReport, | |||
} from "@/api/sales-analysis"; | |||
export default { | |||
name: "ShopAnalysis", | |||
@@ -128,6 +130,8 @@ export default { | |||
shopAmountChart: null, | |||
shopQuantityChart: null, | |||
categoryTrendChart: null, | |||
resizeTimer: null, // 防抖定时器 | |||
resizeObserver: null, // ResizeObserver 实例 | |||
}; | |||
}, | |||
filters: { | |||
@@ -153,8 +157,26 @@ export default { | |||
this.initDateRange(); | |||
this.fetchFilterOptions(); | |||
this.fetchData(); | |||
// 添加窗口大小变化监听 | |||
window.addEventListener('resize', this.handleResize); | |||
// 使用 ResizeObserver 监听容器尺寸变化 | |||
this.initResizeObserver(); | |||
}, | |||
beforeDestroy() { | |||
// 移除窗口大小变化监听 | |||
window.removeEventListener('resize', this.handleResize); | |||
// 清除防抖定时器 | |||
if (this.resizeTimer) { | |||
clearTimeout(this.resizeTimer); | |||
} | |||
// 断开 ResizeObserver | |||
if (this.resizeObserver) { | |||
this.resizeObserver.disconnect(); | |||
} | |||
if (this.shopAmountChart) { | |||
this.shopAmountChart.dispose(); | |||
} | |||
@@ -164,7 +186,6 @@ export default { | |||
if (this.categoryTrendChart) { | |||
this.categoryTrendChart.dispose(); | |||
} | |||
}, | |||
methods: { | |||
// 初始化日期范围(默认最近30天) | |||
@@ -193,8 +214,6 @@ export default { | |||
this.fetchData(); | |||
}, | |||
// 获取筛选选项 | |||
async fetchFilterOptions() { | |||
try { | |||
@@ -233,19 +252,20 @@ export default { | |||
params.shop = this.filters.shop; | |||
} | |||
const response = await getShopAnalysisReport(params); | |||
if (response.code === 200) { | |||
this.statsData = response.data; | |||
this.$nextTick(() => { | |||
this.renderShopAmountChart(); | |||
this.renderShopQuantityChart(); | |||
this.renderCategoryTrendChart(); | |||
// 数据更新后,确保图表能够响应 resize | |||
setTimeout(() => { | |||
this.handleResize(); | |||
}, 200); | |||
}); | |||
} else { | |||
this.$message.error(response.message || "获取数据失败"); | |||
@@ -279,6 +299,9 @@ export default { | |||
} | |||
this.shopAmountChart = echarts.init(chartDom); | |||
// 为图表添加 resize 监听 | |||
this.shopAmountChart.resize = this.shopAmountChart.resize.bind(this.shopAmountChart); | |||
const data = this.statsData.shopAmountData.map((item) => ({ | |||
name: item.shopName, | |||
@@ -382,6 +405,9 @@ export default { | |||
} | |||
this.shopQuantityChart = echarts.init(chartDom); | |||
// 为图表添加 resize 监听 | |||
this.shopQuantityChart.resize = this.shopQuantityChart.resize.bind(this.shopQuantityChart); | |||
const data = this.statsData.shopQuantityData.map((item) => ({ | |||
name: item.shopName, | |||
@@ -485,6 +511,9 @@ export default { | |||
} | |||
this.categoryTrendChart = echarts.init(chartDom); | |||
// 为图表添加 resize 监听 | |||
this.categoryTrendChart.resize = this.categoryTrendChart.resize.bind(this.categoryTrendChart); | |||
const categories = this.statsData.categoryTrendData.map( | |||
(item) => item.category | |||
@@ -648,28 +677,73 @@ export default { | |||
this.categoryTrendChart.setOption(option); | |||
}, | |||
// 格式化数字 | |||
formatNumber(num) { | |||
if (!num) return "0"; | |||
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); | |||
}, | |||
// 处理窗口大小变化 | |||
handleResize() { | |||
console.log('窗口大小变化,触发 resize 处理'); | |||
// 使用防抖处理,避免频繁调用 | |||
if (this.resizeTimer) { | |||
clearTimeout(this.resizeTimer); | |||
} | |||
this.resizeTimer = setTimeout(() => { | |||
console.log('执行图表 resize'); | |||
// 强制触发 DOM 重新计算 | |||
this.$forceUpdate(); | |||
// 检查图表是否存在且已初始化 | |||
if (this.shopAmountChart && this.shopAmountChart.getDom()) { | |||
console.log('resize 店铺金额图'); | |||
this.shopAmountChart.resize(); | |||
} | |||
if (this.shopQuantityChart && this.shopQuantityChart.getDom()) { | |||
console.log('resize 店铺数量图'); | |||
this.shopQuantityChart.resize(); | |||
} | |||
if (this.categoryTrendChart && this.categoryTrendChart.getDom()) { | |||
console.log('resize 品类趋势图'); | |||
this.categoryTrendChart.resize(); | |||
} | |||
}, 100); | |||
}, | |||
// 初始化 ResizeObserver | |||
initResizeObserver() { | |||
if (window.ResizeObserver) { | |||
this.resizeObserver = new ResizeObserver((entries) => { | |||
console.log('容器尺寸变化检测到'); | |||
this.handleResize(); | |||
}); | |||
// 监听图表容器 | |||
this.$nextTick(() => { | |||
const chartContainers = document.querySelectorAll('.chart-container'); | |||
chartContainers.forEach(container => { | |||
this.resizeObserver.observe(container); | |||
}); | |||
}); | |||
} | |||
}, | |||
}, | |||
}; | |||
</script> | |||
<style scoped> | |||
.overall-analysis-page { | |||
height: 100vh; | |||
overflow-y: auto; | |||
margin: -20px; | |||
padding: 10px; | |||
.app-container { | |||
display: flex; | |||
flex-direction: column; | |||
gap: 10px; | |||
padding: 10px; | |||
width: 100%; | |||
min-height: 100vh; | |||
box-sizing: border-box; | |||
} | |||
.filter-card { | |||
background-color: #fff; | |||
@@ -772,9 +846,11 @@ export default { | |||
} | |||
.charts-section { | |||
display: grid; | |||
grid-template-columns: 1fr; | |||
gap: 20px; | |||
width: 100%; | |||
min-width: 0; | |||
display: flex; | |||
flex-direction: column; | |||
gap: 10px; | |||
} | |||
.chart-card { | |||
@@ -783,6 +859,9 @@ export default { | |||
.chart-container { | |||
width: 100%; | |||
height: auto; | |||
min-height: 0; | |||
overflow: hidden; | |||
} | |||
.card-header { | |||
@@ -794,8 +873,6 @@ export default { | |||
justify-content: center; | |||
} | |||
.chart-card { | |||
background-color: #fff; | |||
border-radius: 8px; |