123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255 |
- <script setup lang="ts">
- /**
- * @Name: IndicatorHeatmap.vue
- * @Description: 搜索词-分析页-热力图
- * @Author: Cheney
- */
- import * as echarts from 'echarts';
- import { inject, onBeforeUnmount, Ref, ref } from 'vue';
- import * as api from './api';
- import emitter from '/@/utils/emitter';
- const filter = inject<Ref>('filter');
- const hasData = ref(true);
- const heatmapLoading = ref(false);
- const chartRef = ref<HTMLElement | null>(null);
- let chart: echarts.ECharts | null = null;
- let resizeObserver: ResizeObserver | null = null;
- onBeforeUnmount(() => {
- emitter.all.clear();
- // 清理 ResizeObserver
- if (resizeObserver) {
- resizeObserver.disconnect();
- }
- if (chart) {
- chart.dispose();
- chart = null;
- }
- });
- emitter.on('QueryCondition-sendRequest', () => {
- fetchHeatmapData();
- });
- async function fetchHeatmapData() {
- heatmapLoading.value = true;
- const query = {
- date_start: filter.value.reportDate[0],
- date_end: filter.value.reportDate[1],
- layer_type: filter.value.layerType,
- search_term: filter.value.searchTerm,
- report_range: filter.value.reportType,
- [filter.value.layerType.split('_')[0]]: filter.value.variable,
- metric: filter.value.metric,
- };
- try {
- const responseData = await api.getHeatmapData(query);
- if (!responseData.data || responseData.data.length === 0) {
- // 处理空数据的情况
- hasData.value = false;
- if (chart) {
- chart.clear();
- }
- return;
- }
- hasData.value = true;
- const days = [...new Set(responseData.data.map(item => item.Reporting_Date))]; // x轴数据
- const keywords = [...new Set(responseData.data.flatMap(item =>
- Object.keys(item).filter(key => key !== 'Reporting_Date')
- ))]; // y轴数据
- const data = [];
- // 找出所有数值的最大值,用于设置 visualMap 的 max 值
- const maxValue = Math.max(
- ...responseData.data.flatMap(item =>
- Object.values(item).filter(value => typeof value === 'number') as number[]
- )
- );
- responseData.data.forEach(item => {
- const dayIndex = days.indexOf(item.Reporting_Date);
- keywords.forEach((keyword: any, keywordIndex) => {
- if (item[keyword] !== undefined && item[keyword] !== null) {
- data.push([dayIndex, keywordIndex, item[keyword]]); // [x, y, value]
- }
- });
- });
- const option = {
- title: {
- text: '搜索词时间段对比热力图',
- },
- position: 'top',
- tooltip: {
- position: 'top',
- formatter: function(params) {
- const keyword = keywords[params.value[1]]; // 获取当前悬浮的关键词
- return `${keyword}<br>${params.seriesName}: ${params.value[2]}`;
- }
- },
- grid: {
- // height: '65%',
- top: '10%',
- bottom: '15%',
- left: '15%',
- right: '5%',
- },
- series: [
- {
- name: keywords,
- type: 'heatmap',
- data: data,
- label: {
- show: true,
- },
- emphasis: {
- itemStyle: {
- shadowBlur: 10,
- shadowColor: 'rgba(0, 0, 0, 0.5)',
- },
- },
- },
- ],
- xAxis: {
- type: 'category',
- name: '日期',
- nameGap: 20,
- nameLocation: 'start',
- nameTextStyle: {
- fontWeight: 'bold',
- fontSize: 14,
- color: '#333',
- },
- axisLabel: {
- formatter: function (value) {
- if (value.length > 10) {
- return value.substring(0, 10) + '...';
- }
- return value;
- },
- },
- data: days,
- splitArea: {
- show: true,
- },
- },
- yAxis: {
- type: 'category',
- name: '搜索词',
- nameGap: 20,
- nameTextStyle: {
- fontWeight: 'bold',
- fontSize: 14,
- color: '#333',
- },
- data: keywords,
- splitArea: {
- show: true,
- },
- axisLabel: {
- interval: 0, // 显示所有标签
- width: 280, // 增加标签区域的宽度以适应文本
- rotate: 15, // 标签旋转角度
- },
- },
- dataZoom: [
- {
- type: 'slider', // 纵向滚动条类型
- yAxisIndex: 0, // 指定作用于 Y 轴
- // start: 100, // 初始位置,百分比形式,数值越小,滚动条越靠下
- // end: 30,
- startValue: 80, // 初始位置,数值形式,数值越小,滚动条越靠上
- endValue: 100,
- zoomLock: true, // 锁定缩放,避免同时缩放两个滚动条
- showDetail: false, // 显示缩放的细节
- brushSelect: false,
- },
- {
- type: 'inside', // 内置型数据区域缩放组件(使用鼠标滚轮和拖拽)
- yAxisIndex: 0,
- zoomOnMouseWheel: false,
- moveOnMouseMove: true,
- moveOnMouseWheel: true,
- }
- ],
- visualMap: {
- min: 0,
- max: maxValue, // 使用计算出的最大值
- itemHeight: 500,
- calculable: true,
- orient: 'horizontal',
- left: 'center',
- bottom: '2%',
- },
- };
- if (!chart) {
- chart = echarts.init(chartRef.value);
- }
- chart.setOption(option);
- // 添加 ResizeObserver 以处理图表大小变化
- if (!resizeObserver) {
- resizeObserver = new ResizeObserver(() => {
- chart?.resize();
- });
- resizeObserver.observe(chartRef.value);
- }
- } catch (error) {
- hasData.value = false;
- console.error('==Error==', error);
- } finally {
- heatmapLoading.value = false;
- }
- }
- </script>
- <template>
- <el-card shadow="never" v-loading="heatmapLoading" class="flex flex-col" body-class="w-full">
- <div class="text-center flex justify-center items-center">
- <span class="font-medium mr-1.5">指标 </span>
- <el-select v-model="filter.metric" @change="fetchHeatmapData" style="width: 200px">
- <el-option label="搜索查询分数" value="Search_Query_Score"></el-option>
- <el-option label="搜索查询数量" value="Search_Query_Volume"></el-option>
- <el-option label="展示量-总数" value="Impressions_Total_Count"></el-option>
- <el-option v-if="filter.layerType === 'asin_view'" label="展示量-ASIN数量" value="Impressions_A_B_Count"></el-option>
- <el-option v-if="filter.layerType === 'brand_view'" label="展示量-品牌数量" value="Impressions_A_B_Count"></el-option>
- <el-option label="点击量-总数" value="Clicks_Total_Count"></el-option>
- <el-option v-if="filter.layerType === 'asin_view'" label="点击量-ASIN数量" value="Clicks_A_B_Count"></el-option>
- <el-option v-if="filter.layerType === 'brand_view'" label="点击量-品牌数量" value="Clicks_A_B_Count"></el-option>
- <el-option label="点击量-价格中位数" value="Clicks_Price_Median"></el-option>
- <el-option v-if="filter.layerType === 'asin_view'" label="点击量-ASIN价格中位数" value="Clicks_A_B_Price_Median"></el-option>
- <el-option v-if="filter.layerType === 'brand_view'" label="点击量-品牌价格中位数" value="Clicks_A_B_Price_Median"></el-option>
- <el-option label="加购-总数" value="Cart_Adds_Total_Count"></el-option>
- <el-option v-if="filter.layerType === 'asin_view'" label="加购-ASIN数量" value="Cart_Adds_A_B_Count"></el-option>
- <el-option v-if="filter.layerType === 'brand_view'" label="加购-品牌数量" value="Cart_Adds_A_B_Count"></el-option>
- <el-option label="加购-价格中位数" value="Cart_Adds_Price_Median"></el-option>
- <el-option v-if="filter.layerType === 'asin_view'" label="加购-ASIN价格中位数" value="Cart_Adds_A_B_Price_Median"></el-option>
- <el-option v-if="filter.layerType === 'brand_view'" label="加购-品牌价格中位数" value="Cart_Adds_A_B_Price_Median"></el-option>
- <el-option label="购买-总数" value="Purchases_Total_Count"></el-option>
- <el-option v-if="filter.layerType === 'asin_view'" label="购买-ASIN数量" value="Purchases_A_B_Count"></el-option>
- <el-option v-if="filter.layerType === 'brand_view'" label="购买-品牌数量" value="Purchases_A_B_Count"></el-option>
- <el-option label="购买-价格中位数" value="Purchases_Price_Median"></el-option>
- <el-option v-if="filter.layerType === 'asin_view'" label="购买-ASIN价格中位数" value="Purchases_A_B_Price_Median"></el-option>
- <el-option v-if="filter.layerType === 'brand_view'" label="购买-品牌价格中位数" value="Purchases_A_B_Price_Median"></el-option>
- </el-select>
- </div>
- <div class="w-full" style="min-height: 800px">
- <div v-show="!heatmapLoading && !hasData" style="min-height: 800px" class="flex justify-center items-center">
- <el-empty :image-size="300" />
- </div>
- <div v-show="hasData" ref="chartRef" style="width: 100%; height: 800px"></div>
- </div>
- </el-card>
- </template>
- <style scoped></style>
|