] | ] | ||||
}, | }, | ||||
"dependencies": { | "dependencies": { | ||||
"@babel/parser": "^7.7.4", | |||||
"@babel/runtime": "^7.27.6", | |||||
"@riophae/vue-treeselect": "0.4.0", | "@riophae/vue-treeselect": "0.4.0", | ||||
"axios": "0.24.0", | "axios": "0.24.0", | ||||
"beautifier": "^0.1.7", | |||||
"bpmn-js": "^7.2.1", | "bpmn-js": "^7.2.1", | ||||
"china-area-data": "^5.0.1", | |||||
"clipboard": "2.0.8", | "clipboard": "2.0.8", | ||||
"core-js": "3.25.3", | |||||
"core-js": "^3.44.0", | |||||
"css-loader": "^3.5.3", | |||||
"echarts": "5.4.0", | "echarts": "5.4.0", | ||||
"element-ui": "2.15.14", | "element-ui": "2.15.14", | ||||
"file-saver": "2.0.5", | "file-saver": "2.0.5", | ||||
"js-beautify": "1.13.0", | "js-beautify": "1.13.0", | ||||
"js-cookie": "3.0.1", | "js-cookie": "3.0.1", | ||||
"jsencrypt": "3.0.0-rc.1", | "jsencrypt": "3.0.0-rc.1", | ||||
"moment": "^2.30.1", | |||||
"npm": "^6.13.7", | |||||
"nprogress": "0.2.0", | "nprogress": "0.2.0", | ||||
"quill": "1.3.7", | "quill": "1.3.7", | ||||
"screenfull": "5.0.2", | "screenfull": "5.0.2", | ||||
"sortablejs": "1.10.2", | "sortablejs": "1.10.2", | ||||
"voca": "^1.4.0", | |||||
"vue": "2.6.12", | "vue": "2.6.12", | ||||
"vue-barcode": "^1.3.0", | |||||
"vue-codemirror": "^4.0.6", | |||||
"vue-count-to": "1.0.13", | "vue-count-to": "1.0.13", | ||||
"vue-cropper": "0.5.5", | "vue-cropper": "0.5.5", | ||||
"vue-meta": "2.4.0", | "vue-meta": "2.4.0", | ||||
"vue-quill-editor": "^3.0.6", | |||||
"vue-router": "3.4.9", | "vue-router": "3.4.9", | ||||
"vuedraggable": "2.24.3", | "vuedraggable": "2.24.3", | ||||
"vuex": "3.6.0", | "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": { | "devDependencies": { | ||||
"@vue/cli-plugin-babel": "4.4.6", | "@vue/cli-plugin-babel": "4.4.6", |
this.isLoading = true | this.isLoading = true | ||||
const response = await getProductCodeSuggestions({ keyword: queryString.trim() }) | const response = await getProductCodeSuggestions({ keyword: queryString.trim() }) | ||||
if (response.success) { | |||||
if (response.code === 200) { | |||||
callback(response.data || []) | callback(response.data || []) | ||||
} else { | } else { | ||||
callback([]) | callback([]) |
component: () => import("@/views/m/checkin"), | component: () => import("@/views/m/checkin"), | ||||
meta: { title: "考勤打卡", icon: "date" }, | 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 }, | |||||
}, | |||||
], | |||||
}, | |||||
]; | ]; | ||||
// 动态路由,基于用户权限动态去加载 | // 动态路由,基于用户权限动态去加载 |
:total="pagination.total" | :total="pagination.total" | ||||
:page.sync="pagination.page" | :page.sync="pagination.page" | ||||
:limit.sync="pagination.pageSize" | :limit.sync="pagination.pageSize" | ||||
:page-sizes="[50, 100, 300, 500, 1000]" | |||||
@pagination="fetchData" | @pagination="fetchData" | ||||
/> | /> | ||||
show-icon | show-icon | ||||
:closable="false" | :closable="false" | ||||
> | > | ||||
<p>请确保Excel文件包含以下列:</p> | |||||
<p> | |||||
<p style="color: #303030;">当前选择分类是:<u>{{ selectedCategory.name }}</u>,请确保Excel文件包含以下列:</p> | |||||
<p style="color: #303030;"> | |||||
<strong | <strong | ||||
>出库日期、目的地、店铺名称、出库类型、发送方式、发送番号、注文番号、商品编号、商品名称、数量、单价、送料、代引、客户名称、备注</strong | >出库日期、目的地、店铺名称、出库类型、发送方式、发送番号、注文番号、商品编号、商品名称、数量、单价、送料、代引、客户名称、备注</strong | ||||
> | > | ||||
show-icon | show-icon | ||||
:closable="false" | :closable="false" | ||||
> | > | ||||
<p>请确保Excel文件包含以下列:</p> | |||||
<p> | |||||
<p style="color: #303030;">当前选择的店铺是:<u>{{ selectedShopName }}</u>,请确保Excel文件包含以下列:</p> | |||||
<p style="color: #303030;"> | |||||
<strong | <strong | ||||
>出荷日、出品者SKU、FNSKU、ASIN、FC、数量、Amazon注文番号、通貨、商品金額(商品1点ごと)、配送料、ギフト包装手数料、配送先(市区町村)、都道府県名、配送先(郵便番号)、付与されたAmazon | >出荷日、出品者SKU、FNSKU、ASIN、FC、数量、Amazon注文番号、通貨、商品金額(商品1点ごと)、配送料、ギフト包装手数料、配送先(市区町村)、都道府県名、配送先(郵便番号)、付与されたAmazon | ||||
ポイント</strong | ポイント</strong | ||||
<i class="el-icon-upload"></i> | <i class="el-icon-upload"></i> | ||||
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div> | <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div> | ||||
<div class="el-upload__tip" slot="tip"> | <div class="el-upload__tip" slot="tip"> | ||||
支持多选Excel文件(.xlsx/.xls),单文件不超过10MB | |||||
支持多选Excel文件(.xlsx/.xls),单文件不超过10MB,文件数量不超过40个 | |||||
<el-button @click="clearAllFiles" type="text" | <el-button @click="clearAllFiles" type="text" | ||||
>清空文件列表</el-button | >清空文件列表</el-button | ||||
> | > | ||||
:data="processResult.data.slice(0, 50)" | :data="processResult.data.slice(0, 50)" | ||||
stripe | stripe | ||||
border | border | ||||
size="small" | |||||
height="calc(100vh - 470px)" | |||||
size="mini" | |||||
height="calc(100vh - 490px)" | |||||
> | > | ||||
<el-table-column | <el-table-column | ||||
prop="rowNumber" | prop="rowNumber" |
<template> | <template> | ||||
<div class="overall-analysis-page"> | |||||
<div class="app-container"> | |||||
<!-- 筛选 --> | <!-- 筛选 --> | ||||
<div class="filter-card"> | <div class="filter-card"> | ||||
<div class="filter-section"> | <div class="filter-section"> | ||||
:key="dimension" | :key="dimension" | ||||
class="dimension-section" | 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"> | <el-col :span="12"> | ||||
<!-- 销售金额占比 --> | <!-- 销售金额占比 --> | ||||
<div class="chart-card"> | <div class="chart-card"> | ||||
<div class="card-header"> | <div class="card-header"> | ||||
<div class="header-left"> | <div class="header-left"> | ||||
<i class="el-icon-money header-icon-amount"></i> | |||||
<span>销售金额占比</span> | |||||
<span>{{ currentCategory }} {{ dimension }} 销售金额占比</span> | |||||
</div> | </div> | ||||
<div class="header-right"> | <div class="header-right"> | ||||
<span class="total-amount" | <span class="total-amount" | ||||
<div class="chart-card"> | <div class="chart-card"> | ||||
<div class="card-header"> | <div class="card-header"> | ||||
<div class="header-left"> | <div class="header-left"> | ||||
<i class="el-icon-s-goods header-icon-quantity"></i> | |||||
<span>销售数量占比</span> | |||||
<span>{{ currentCategory }} {{ dimension }} 销售数量占比</span> | |||||
</div> | </div> | ||||
<div class="header-right"> | <div class="header-right"> | ||||
<span class="total-quantity" | <span class="total-quantity" | ||||
dimensionCharts: {}, | dimensionCharts: {}, | ||||
chartInstances: new Map(), // 存储图表实例 | chartInstances: new Map(), // 存储图表实例 | ||||
currentCategory: "全部分类", | currentCategory: "全部分类", | ||||
resizeTimer: null, // 防抖定时器 | |||||
resizeObserver: null, // ResizeObserver 实例 | |||||
}; | }; | ||||
}, | }, | ||||
mounted() { | mounted() { | ||||
this.initDateRange(); | this.initDateRange(); | ||||
this.fetchFilterOptions(); | this.fetchFilterOptions(); | ||||
// this.fetchData(); | // this.fetchData(); | ||||
// 添加窗口大小变化监听 | |||||
window.addEventListener('resize', this.handleResize); | |||||
// 使用 ResizeObserver 监听容器尺寸变化 | |||||
this.initResizeObserver(); | |||||
}, | }, | ||||
beforeDestroy() { | beforeDestroy() { | ||||
// 移除窗口大小变化监听 | |||||
window.removeEventListener('resize', this.handleResize); | |||||
// 清除防抖定时器 | |||||
if (this.resizeTimer) { | |||||
clearTimeout(this.resizeTimer); | |||||
} | |||||
// 断开 ResizeObserver | |||||
if (this.resizeObserver) { | |||||
this.resizeObserver.disconnect(); | |||||
} | |||||
// 销毁所有图表实例 | // 销毁所有图表实例 | ||||
this.chartInstances.forEach((chart) => { | this.chartInstances.forEach((chart) => { | ||||
if (chart) { | if (chart) { | ||||
this.$nextTick(() => { | this.$nextTick(() => { | ||||
this.renderAllCharts(); | this.renderAllCharts(); | ||||
// 数据更新后,确保图表能够响应 resize | |||||
setTimeout(() => { | |||||
this.handleResize(); | |||||
}, 200); | |||||
}); | }); | ||||
} else { | } else { | ||||
this.$message.error(response.message || "获取数据失败"); | this.$message.error(response.message || "获取数据失败"); | ||||
if (!chartDom || !data || data.length === 0) return; | if (!chartDom || !data || data.length === 0) return; | ||||
const chart = echarts.init(chartDom); | const chart = echarts.init(chartDom); | ||||
// 为图表添加 resize 监听 | |||||
chart.resize = chart.resize.bind(chart); | |||||
this.chartInstances.set(chartId, chart); | this.chartInstances.set(chartId, chart); | ||||
const totalAmount = this.getTotalAmount(data); | const totalAmount = this.getTotalAmount(data); | ||||
const processedData = this.processChartData(data); | const processedData = this.processChartData(data); | ||||
if (!chartDom || !data || data.length === 0) return; | if (!chartDom || !data || data.length === 0) return; | ||||
const chart = echarts.init(chartDom); | const chart = echarts.init(chartDom); | ||||
// 为图表添加 resize 监听 | |||||
chart.resize = chart.resize.bind(chart); | |||||
this.chartInstances.set(chartId, chart); | this.chartInstances.set(chartId, chart); | ||||
const totalQuantity = this.getTotalQuantity(data); | const totalQuantity = this.getTotalQuantity(data); | ||||
const processedData = this.processChartData(data, false); | const processedData = this.processChartData(data, false); | ||||
getTotalQuantity(data) { | getTotalQuantity(data) { | ||||
return data.reduce((sum, item) => sum + (item.value || 0), 0); | 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> | </script> | ||||
<style scoped> | <style scoped> | ||||
.overall-analysis-page { | |||||
height: 100vh; | |||||
overflow-y: auto; | |||||
margin: -20px; | |||||
padding: 10px; | |||||
.app-container { | |||||
display: flex; | display: flex; | ||||
flex-direction: column; | flex-direction: column; | ||||
gap: 15px; | |||||
gap: 10px; | |||||
padding: 10px; | |||||
width: 100%; | |||||
min-height: 100vh; | |||||
box-sizing: border-box; | |||||
} | } | ||||
.filter-card { | .filter-card { | ||||
.stats-cards { | .stats-cards { | ||||
display: grid; | display: grid; | ||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); | grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); | ||||
gap: 15px; | |||||
gap: 10px; | |||||
} | } | ||||
.stat-card { | .stat-card { | ||||
border-radius: 8px; | border-radius: 8px; | ||||
display: flex; | display: flex; | ||||
align-items: center; | align-items: center; | ||||
gap: 15px; | |||||
gap: 10px; | |||||
border: 1px solid #e8e8e8; | border: 1px solid #e8e8e8; | ||||
transition: background-color 0.2s ease; | transition: background-color 0.2s ease; | ||||
} | } | ||||
} | } | ||||
.charts-section { | .charts-section { | ||||
width: 100%; | |||||
min-width: 0; | |||||
display: flex; | display: flex; | ||||
flex-direction: column; | flex-direction: column; | ||||
gap: 30px; | |||||
gap: 10px; | |||||
} | } | ||||
.dimension-section { | .dimension-section { | ||||
display: flex; | display: flex; | ||||
flex-direction: column; | flex-direction: column; | ||||
gap: 20px; | |||||
gap: 10px; | |||||
overflow: hidden; | overflow: hidden; | ||||
} | } | ||||
display: flex; | display: flex; | ||||
justify-content: space-between; | justify-content: space-between; | ||||
align-items: center; | align-items: center; | ||||
padding: 15px 20px; | |||||
background: #f8f9fa; | |||||
border-radius: 8px; | |||||
border-bottom: 1px solid #e8e8e8; | |||||
padding: 15px 0; | |||||
} | } | ||||
.dimension-title { | .dimension-title { | ||||
align-items: center; | align-items: center; | ||||
gap: 10px; | gap: 10px; | ||||
font-size: 18px; | font-size: 18px; | ||||
font-weight: 600; | |||||
color: #333; | color: #333; | ||||
} | } | ||||
.dimension-stats { | .dimension-stats { | ||||
display: flex; | display: flex; | ||||
gap: 20px; | |||||
gap: 10px; | |||||
} | } | ||||
.stat-item { | .stat-item { | ||||
.chart-container { | .chart-container { | ||||
padding: 15px 0; | padding: 15px 0; | ||||
width: 100%; | width: 100%; | ||||
height: auto; | |||||
min-height: 0; | |||||
overflow: hidden; | |||||
} | } | ||||
.card-header { | .card-header { | ||||
display: flex; | display: flex; | ||||
align-items: center; | align-items: center; | ||||
gap: 8px; | gap: 8px; | ||||
font-weight: 600; | |||||
font-size: 16px; | font-size: 16px; | ||||
color: #333; | color: #333; | ||||
} | } |
<template> | <template> | ||||
<div class="overall-analysis-page"> | |||||
<div class="app-container"> | |||||
<!-- 筛选 --> | <!-- 筛选 --> | ||||
<div class="filter-card"> | <div class="filter-card"> | ||||
<div class="filter-section"> | <div class="filter-section"> | ||||
trendChart: null, | trendChart: null, | ||||
topProductsChart: null, | topProductsChart: null, | ||||
brandChart: null, | brandChart: null, | ||||
resizeTimer: null, // 防抖定时器 | |||||
resizeObserver: null, // ResizeObserver 实例 | |||||
}; | }; | ||||
}, | }, | ||||
filters: { | filters: { | ||||
this.initDateRange(); | this.initDateRange(); | ||||
this.fetchFilterOptions(); | this.fetchFilterOptions(); | ||||
this.fetchData(); | this.fetchData(); | ||||
// 添加窗口大小变化监听 | |||||
window.addEventListener('resize', this.handleResize); | |||||
// 使用 ResizeObserver 监听容器尺寸变化 | |||||
this.initResizeObserver(); | |||||
}, | }, | ||||
beforeDestroy() { | beforeDestroy() { | ||||
// 移除窗口大小变化监听 | |||||
window.removeEventListener('resize', this.handleResize); | |||||
// 清除防抖定时器 | |||||
if (this.resizeTimer) { | |||||
clearTimeout(this.resizeTimer); | |||||
} | |||||
// 断开 ResizeObserver | |||||
if (this.resizeObserver) { | |||||
this.resizeObserver.disconnect(); | |||||
} | |||||
if (this.trendChart) { | if (this.trendChart) { | ||||
this.trendChart.dispose(); | this.trendChart.dispose(); | ||||
} | } | ||||
this.renderTrendChart(); | this.renderTrendChart(); | ||||
this.renderTopProductsChart(); | this.renderTopProductsChart(); | ||||
this.renderBrandChart(); | this.renderBrandChart(); | ||||
// 数据更新后,确保图表能够响应 resize | |||||
setTimeout(() => { | |||||
this.handleResize(); | |||||
}, 200); | |||||
}); | }); | ||||
} else { | } else { | ||||
this.$message.error(response.message || "获取数据失败"); | this.$message.error(response.message || "获取数据失败"); | ||||
} | } | ||||
this.trendChart = echarts.init(chartDom); | 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 dates = this.statsData.trendData.map((item) => item.date); | ||||
const amounts = this.statsData.trendData.map((item) => item.amount); | const amounts = this.statsData.trendData.map((item) => item.amount); | ||||
tooltipText += `${param.marker} ${param.seriesName}: `; | tooltipText += `${param.marker} ${param.seriesName}: `; | ||||
const value = param.value || 0; | const value = param.value || 0; | ||||
if (param.seriesName === "销售额") { | if (param.seriesName === "销售额") { | ||||
tooltipText += `¥${this.formatNumber(value.toFixed(0))}`; | |||||
tooltipText += `¥${this.formatNumber(value)}`; | |||||
} else { | } else { | ||||
tooltipText += `${this.formatNumber(value)}`; | tooltipText += `${this.formatNumber(value)}`; | ||||
} | } | ||||
} | } | ||||
this.topProductsChart = echarts.init(chartDom); | this.topProductsChart = echarts.init(chartDom); | ||||
// 为图表添加 resize 监听 | |||||
this.topProductsChart.resize = this.topProductsChart.resize.bind(this.topProductsChart); | |||||
const data = [...this.statsData.topProducts] | const data = [...this.statsData.topProducts] | ||||
.sort((a, b) => a.amount - b.amount) | .sort((a, b) => a.amount - b.amount) | ||||
.map((item) => ({ | .map((item) => ({ | ||||
} | } | ||||
this.brandChart = echarts.init(chartDom); | this.brandChart = echarts.init(chartDom); | ||||
// 为图表添加 resize 监听 | |||||
this.brandChart.resize = this.brandChart.resize.bind(this.brandChart); | |||||
const brands = this.statsData.brandData.map( | const brands = this.statsData.brandData.map( | ||||
(item) => item.brand | (item) => item.brand | ||||
if (rate < 0) return "growth-negative"; | if (rate < 0) return "growth-negative"; | ||||
return "growth-neutral"; | 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> | </script> | ||||
<style scoped> | <style scoped> | ||||
.overall-analysis-page { | |||||
height: 100vh; | |||||
overflow-y: auto; | |||||
margin: -20px; | |||||
padding: 10px; | |||||
.app-container { | |||||
display: flex; | display: flex; | ||||
flex-direction: column; | flex-direction: column; | ||||
gap: 10px; | gap: 10px; | ||||
padding: 10px; | |||||
width: 100%; | |||||
min-height: 100vh; | |||||
box-sizing: border-box; | |||||
} | } | ||||
.filter-card { | .filter-card { | ||||
background-color: #fff; | background-color: #fff; | ||||
} | } | ||||
.charts-section { | .charts-section { | ||||
display: grid; | |||||
grid-template-columns: 1fr; | |||||
gap: 20px; | |||||
width: 100%; | |||||
min-width: 0; | |||||
display: flex; | |||||
flex-direction: column; | |||||
gap: 10px; | |||||
} | } | ||||
.chart-card { | .chart-card { | ||||
.chart-container { | .chart-container { | ||||
width: 100%; | width: 100%; | ||||
height: auto; | |||||
min-height: 0; | |||||
overflow: hidden; | |||||
} | } | ||||
.card-header { | .card-header { |
<template> | <template> | ||||
<div class="product-analysis-page"> | |||||
<div class="app-container"> | |||||
<!-- 筛选 --> | <!-- 筛选 --> | ||||
<div class="filter-card"> | <div class="filter-card"> | ||||
<div class="filter-section"> | <div class="filter-section"> | ||||
}, | }, | ||||
trendChart: null, | trendChart: null, | ||||
rankingChart: null, | rankingChart: null, | ||||
resizeTimer: null, // 防抖定时器 | |||||
resizeObserver: null, // ResizeObserver 实例 | |||||
}; | }; | ||||
}, | }, | ||||
computed: { | computed: { | ||||
this.initDateRange(); | this.initDateRange(); | ||||
this.fetchFilterOptions(); | this.fetchFilterOptions(); | ||||
// this.fetchData(); | // this.fetchData(); | ||||
// 添加窗口大小变化监听 | |||||
window.addEventListener('resize', this.handleResize); | |||||
// 使用 ResizeObserver 监听容器尺寸变化 | |||||
this.initResizeObserver(); | |||||
// 初始化图表实例,即使没有数据也要创建 | |||||
this.$nextTick(() => { | |||||
this.initCharts(); | |||||
}); | |||||
}, | }, | ||||
beforeDestroy() { | beforeDestroy() { | ||||
// 移除窗口大小变化监听 | |||||
window.removeEventListener('resize', this.handleResize); | |||||
// 清除防抖定时器 | |||||
if (this.resizeTimer) { | |||||
clearTimeout(this.resizeTimer); | |||||
} | |||||
// 断开 ResizeObserver | |||||
if (this.resizeObserver) { | |||||
this.resizeObserver.disconnect(); | |||||
} | |||||
if (this.trendChart) { | if (this.trendChart) { | ||||
this.trendChart.dispose(); | this.trendChart.dispose(); | ||||
} | } | ||||
this.$nextTick(() => { | this.$nextTick(() => { | ||||
this.renderTrendChart(); | this.renderTrendChart(); | ||||
this.renderRankingChart(); | this.renderRankingChart(); | ||||
// 数据更新后,确保图表能够响应 resize | |||||
setTimeout(() => { | |||||
this.handleResize(); | |||||
}, 200); | |||||
}); | }); | ||||
} else { | } else { | ||||
this.$message.error(response.message || "获取数据失败"); | this.$message.error(response.message || "获取数据失败"); | ||||
const chartDom = document.getElementById("trendChart"); | const chartDom = document.getElementById("trendChart"); | ||||
if (!chartDom) return; | 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) { | if (!this.statsData.trendData || this.statsData.trendData.length === 0) { | ||||
this.trendChart.clear(); | this.trendChart.clear(); | ||||
// 即使没有数据也要保持图表实例,以便 resize 功能正常工作 | |||||
return; | return; | ||||
} | } | ||||
const chartDom = document.getElementById("rankingChart"); | const chartDom = document.getElementById("rankingChart"); | ||||
if (!chartDom) return; | 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 ( | if ( | ||||
!this.statsData.rankingData || | !this.statsData.rankingData || | ||||
this.statsData.rankingData.length === 0 | this.statsData.rankingData.length === 0 | ||||
) { | ) { | ||||
this.rankingChart.clear(); | this.rankingChart.clear(); | ||||
// 即使没有数据也要保持图表实例,以便 resize 功能正常工作 | |||||
return; | return; | ||||
} | } | ||||
if (num === null || num === undefined) return "0"; | if (num === null || num === undefined) return "0"; | ||||
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); | 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> | </script> | ||||
<style scoped> | <style scoped> | ||||
.product-analysis-page { | |||||
height: 100vh; | |||||
overflow-y: auto; | |||||
margin: -20px; | |||||
padding: 10px; | |||||
.app-container { | |||||
display: flex; | display: flex; | ||||
flex-direction: column; | flex-direction: column; | ||||
gap: 15px; | |||||
gap: 10px; | |||||
padding: 10px; | |||||
width: 100%; | |||||
min-height: 100vh; | |||||
box-sizing: border-box; | |||||
} | } | ||||
.filter-card { | .filter-card { | ||||
background-color: #fff; | background-color: #fff; | ||||
} | } | ||||
.charts-section { | .charts-section { | ||||
width: 100%; | |||||
min-width: 0; | |||||
display: flex; | display: flex; | ||||
flex-direction: column; | flex-direction: column; | ||||
gap: 20px; | |||||
gap: 10px; | |||||
} | } | ||||
.chart-card { | .chart-card { | ||||
.card-header { | .card-header { | ||||
display: flex; | display: flex; | ||||
align-items: center; | align-items: center; | ||||
font-weight: 600; | |||||
justify-content: center; | |||||
font-size: 18px; | font-size: 18px; | ||||
padding: 15px 20px; | padding: 15px 20px; | ||||
border-bottom: 1px solid #e8e8e8; | |||||
} | } | ||||
.chart-container { | .chart-container { | ||||
padding: 20px; | padding: 20px; | ||||
width: 100%; | |||||
height: auto; | |||||
min-height: 0; | |||||
overflow: hidden; | |||||
} | } | ||||
.no-data-section { | .no-data-section { | ||||
display: flex; | display: flex; |
<template> | <template> | ||||
<div class="overall-analysis-page"> | |||||
<div class="app-container"> | |||||
<!-- 筛选 --> | <!-- 筛选 --> | ||||
<div class="filter-card"> | <div class="filter-card"> | ||||
<div class="filter-section"> | <div class="filter-section"> | ||||
</el-select> | </el-select> | ||||
</div> | </div> | ||||
<div class="filter-item"> | <div class="filter-item"> | ||||
<label>日期范围:</label> | <label>日期范围:</label> | ||||
<el-date-picker | <el-date-picker | ||||
</div> | </div> | ||||
<div class="filter-item"> | <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> | ||||
<el-button @click="resetFilter" size="small">重置筛选</el-button> | <el-button @click="resetFilter" size="small">重置筛选</el-button> | ||||
</div> | </div> | ||||
</div> | </div> | ||||
<!-- 图表区域 --> | <!-- 图表区域 --> | ||||
<div class="charts-section"> | <div class="charts-section"> | ||||
<el-row :gutter="20"> | |||||
<el-row :gutter="10"> | |||||
<el-col :span="12"> | <el-col :span="12"> | ||||
<!-- 各店铺销售金额占比 --> | <!-- 各店铺销售金额占比 --> | ||||
<div class="chart-card"> | <div class="chart-card"> | ||||
</el-col> | </el-col> | ||||
</el-row> | </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> | ||||
</div> | |||||
</div> | </div> | ||||
</template> | </template> | ||||
<script> | <script> | ||||
import * as echarts from "echarts"; | import * as echarts from "echarts"; | ||||
import { getReportFilterOptions, getShopAnalysisReport } from '@/api/sales-analysis' | |||||
import { | |||||
getReportFilterOptions, | |||||
getShopAnalysisReport, | |||||
} from "@/api/sales-analysis"; | |||||
export default { | export default { | ||||
name: "ShopAnalysis", | name: "ShopAnalysis", | ||||
shopAmountChart: null, | shopAmountChart: null, | ||||
shopQuantityChart: null, | shopQuantityChart: null, | ||||
categoryTrendChart: null, | categoryTrendChart: null, | ||||
resizeTimer: null, // 防抖定时器 | |||||
resizeObserver: null, // ResizeObserver 实例 | |||||
}; | }; | ||||
}, | }, | ||||
filters: { | filters: { | ||||
this.initDateRange(); | this.initDateRange(); | ||||
this.fetchFilterOptions(); | this.fetchFilterOptions(); | ||||
this.fetchData(); | this.fetchData(); | ||||
// 添加窗口大小变化监听 | |||||
window.addEventListener('resize', this.handleResize); | |||||
// 使用 ResizeObserver 监听容器尺寸变化 | |||||
this.initResizeObserver(); | |||||
}, | }, | ||||
beforeDestroy() { | beforeDestroy() { | ||||
// 移除窗口大小变化监听 | |||||
window.removeEventListener('resize', this.handleResize); | |||||
// 清除防抖定时器 | |||||
if (this.resizeTimer) { | |||||
clearTimeout(this.resizeTimer); | |||||
} | |||||
// 断开 ResizeObserver | |||||
if (this.resizeObserver) { | |||||
this.resizeObserver.disconnect(); | |||||
} | |||||
if (this.shopAmountChart) { | if (this.shopAmountChart) { | ||||
this.shopAmountChart.dispose(); | this.shopAmountChart.dispose(); | ||||
} | } | ||||
if (this.categoryTrendChart) { | if (this.categoryTrendChart) { | ||||
this.categoryTrendChart.dispose(); | this.categoryTrendChart.dispose(); | ||||
} | } | ||||
}, | }, | ||||
methods: { | methods: { | ||||
// 初始化日期范围(默认最近30天) | // 初始化日期范围(默认最近30天) | ||||
this.fetchData(); | this.fetchData(); | ||||
}, | }, | ||||
// 获取筛选选项 | // 获取筛选选项 | ||||
async fetchFilterOptions() { | async fetchFilterOptions() { | ||||
try { | try { | ||||
params.shop = this.filters.shop; | params.shop = this.filters.shop; | ||||
} | } | ||||
const response = await getShopAnalysisReport(params); | const response = await getShopAnalysisReport(params); | ||||
if (response.code === 200) { | if (response.code === 200) { | ||||
this.statsData = response.data; | this.statsData = response.data; | ||||
this.$nextTick(() => { | this.$nextTick(() => { | ||||
this.renderShopAmountChart(); | this.renderShopAmountChart(); | ||||
this.renderShopQuantityChart(); | this.renderShopQuantityChart(); | ||||
this.renderCategoryTrendChart(); | this.renderCategoryTrendChart(); | ||||
// 数据更新后,确保图表能够响应 resize | |||||
setTimeout(() => { | |||||
this.handleResize(); | |||||
}, 200); | |||||
}); | }); | ||||
} else { | } else { | ||||
this.$message.error(response.message || "获取数据失败"); | this.$message.error(response.message || "获取数据失败"); | ||||
} | } | ||||
this.shopAmountChart = echarts.init(chartDom); | this.shopAmountChart = echarts.init(chartDom); | ||||
// 为图表添加 resize 监听 | |||||
this.shopAmountChart.resize = this.shopAmountChart.resize.bind(this.shopAmountChart); | |||||
const data = this.statsData.shopAmountData.map((item) => ({ | const data = this.statsData.shopAmountData.map((item) => ({ | ||||
name: item.shopName, | name: item.shopName, | ||||
} | } | ||||
this.shopQuantityChart = echarts.init(chartDom); | this.shopQuantityChart = echarts.init(chartDom); | ||||
// 为图表添加 resize 监听 | |||||
this.shopQuantityChart.resize = this.shopQuantityChart.resize.bind(this.shopQuantityChart); | |||||
const data = this.statsData.shopQuantityData.map((item) => ({ | const data = this.statsData.shopQuantityData.map((item) => ({ | ||||
name: item.shopName, | name: item.shopName, | ||||
} | } | ||||
this.categoryTrendChart = echarts.init(chartDom); | this.categoryTrendChart = echarts.init(chartDom); | ||||
// 为图表添加 resize 监听 | |||||
this.categoryTrendChart.resize = this.categoryTrendChart.resize.bind(this.categoryTrendChart); | |||||
const categories = this.statsData.categoryTrendData.map( | const categories = this.statsData.categoryTrendData.map( | ||||
(item) => item.category | (item) => item.category | ||||
this.categoryTrendChart.setOption(option); | this.categoryTrendChart.setOption(option); | ||||
}, | }, | ||||
// 格式化数字 | // 格式化数字 | ||||
formatNumber(num) { | formatNumber(num) { | ||||
if (!num) return "0"; | if (!num) return "0"; | ||||
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); | 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> | </script> | ||||
<style scoped> | <style scoped> | ||||
.overall-analysis-page { | |||||
height: 100vh; | |||||
overflow-y: auto; | |||||
margin: -20px; | |||||
padding: 10px; | |||||
.app-container { | |||||
display: flex; | display: flex; | ||||
flex-direction: column; | flex-direction: column; | ||||
gap: 10px; | gap: 10px; | ||||
padding: 10px; | |||||
width: 100%; | |||||
min-height: 100vh; | |||||
box-sizing: border-box; | |||||
} | } | ||||
.filter-card { | .filter-card { | ||||
background-color: #fff; | background-color: #fff; | ||||
} | } | ||||
.charts-section { | .charts-section { | ||||
display: grid; | |||||
grid-template-columns: 1fr; | |||||
gap: 20px; | |||||
width: 100%; | |||||
min-width: 0; | |||||
display: flex; | |||||
flex-direction: column; | |||||
gap: 10px; | |||||
} | } | ||||
.chart-card { | .chart-card { | ||||
.chart-container { | .chart-container { | ||||
width: 100%; | width: 100%; | ||||
height: auto; | |||||
min-height: 0; | |||||
overflow: hidden; | |||||
} | } | ||||
.card-header { | .card-header { | ||||
justify-content: center; | justify-content: center; | ||||
} | } | ||||
.chart-card { | .chart-card { | ||||
background-color: #fff; | background-color: #fff; | ||||
border-radius: 8px; | border-radius: 8px; |