Browse Source

chore: 更新依赖和优化组件,添加图表自适应功能。修改了多个组件的样式和逻辑,确保在窗口大小变化时图表能够正确响应。移除不再使用的路由和组件,提升代码整洁性。

master
lizhuang 3 days ago
parent
commit
c5acc51a55

+ 13
- 11
package.json View File

] ]
}, },
"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",

+ 1
- 1
src/components/ProductCodeAutocomplete/index.vue View File

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([])

+ 0
- 72
src/router/index.js View File

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 },
},
],
},
]; ];


// 动态路由,基于用户权限动态去加载 // 动态路由,基于用户权限动态去加载

+ 1
- 0
src/views/sales-analysis/analysis-data/DataList.vue View File

: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"
/> />



+ 7
- 7
src/views/sales-analysis/analysis-data/ImportData.vue View File

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"

+ 98
- 44
src/views/sales-analysis/reports/CategoryAnalysis.vue View File

<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;
} }

+ 99
- 10
src/views/sales-analysis/reports/OverallAnalysis.vue View File

<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 {

+ 161
- 16
src/views/sales-analysis/reports/ProductAnalysis.vue View File

<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;

+ 114
- 37
src/views/sales-analysis/reports/ShopAnalysis.vue View File

<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;

Loading…
Cancel
Save