IndicatorHeatmap.vue 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255
  1. <script setup lang="ts">
  2. /**
  3. * @Name: IndicatorHeatmap.vue
  4. * @Description: 搜索词-分析页-热力图
  5. * @Author: Cheney
  6. */
  7. import * as echarts from 'echarts';
  8. import { inject, onBeforeUnmount, Ref, ref } from 'vue';
  9. import * as api from './api';
  10. import emitter from '/@/utils/emitter';
  11. const filter = inject<Ref>('filter');
  12. const hasData = ref(true);
  13. const heatmapLoading = ref(false);
  14. const chartRef = ref<HTMLElement | null>(null);
  15. let chart: echarts.ECharts | null = null;
  16. let resizeObserver: ResizeObserver | null = null;
  17. onBeforeUnmount(() => {
  18. emitter.all.clear();
  19. // 清理 ResizeObserver
  20. if (resizeObserver) {
  21. resizeObserver.disconnect();
  22. }
  23. if (chart) {
  24. chart.dispose();
  25. chart = null;
  26. }
  27. });
  28. emitter.on('QueryCondition-sendRequest', () => {
  29. fetchHeatmapData();
  30. });
  31. async function fetchHeatmapData() {
  32. heatmapLoading.value = true;
  33. const query = {
  34. date_start: filter.value.reportDate[0],
  35. date_end: filter.value.reportDate[1],
  36. layer_type: filter.value.layerType,
  37. search_term: filter.value.searchTerm,
  38. report_range: filter.value.reportType,
  39. [filter.value.layerType.split('_')[0]]: filter.value.variable,
  40. metric: filter.value.metric,
  41. };
  42. try {
  43. const responseData = await api.getHeatmapData(query);
  44. if (!responseData.data || responseData.data.length === 0) {
  45. // 处理空数据的情况
  46. hasData.value = false;
  47. if (chart) {
  48. chart.clear();
  49. }
  50. return;
  51. }
  52. hasData.value = true;
  53. const days = [...new Set(responseData.data.map(item => item.Reporting_Date))]; // x轴数据
  54. const keywords = [...new Set(responseData.data.flatMap(item =>
  55. Object.keys(item).filter(key => key !== 'Reporting_Date')
  56. ))]; // y轴数据
  57. const data = [];
  58. // 找出所有数值的最大值,用于设置 visualMap 的 max 值
  59. const maxValue = Math.max(
  60. ...responseData.data.flatMap(item =>
  61. Object.values(item).filter(value => typeof value === 'number') as number[]
  62. )
  63. );
  64. responseData.data.forEach(item => {
  65. const dayIndex = days.indexOf(item.Reporting_Date);
  66. keywords.forEach((keyword: any, keywordIndex) => {
  67. if (item[keyword] !== undefined && item[keyword] !== null) {
  68. data.push([dayIndex, keywordIndex, item[keyword]]); // [x, y, value]
  69. }
  70. });
  71. });
  72. const option = {
  73. title: {
  74. text: '搜索词时间段对比热力图',
  75. },
  76. position: 'top',
  77. tooltip: {
  78. position: 'top',
  79. formatter: function(params) {
  80. const keyword = keywords[params.value[1]]; // 获取当前悬浮的关键词
  81. return `${keyword}<br>${params.seriesName}: ${params.value[2]}`;
  82. }
  83. },
  84. grid: {
  85. // height: '65%',
  86. top: '10%',
  87. bottom: '15%',
  88. left: '15%',
  89. right: '5%',
  90. },
  91. series: [
  92. {
  93. name: keywords,
  94. type: 'heatmap',
  95. data: data,
  96. label: {
  97. show: true,
  98. },
  99. emphasis: {
  100. itemStyle: {
  101. shadowBlur: 10,
  102. shadowColor: 'rgba(0, 0, 0, 0.5)',
  103. },
  104. },
  105. },
  106. ],
  107. xAxis: {
  108. type: 'category',
  109. name: '日期',
  110. nameGap: 20,
  111. nameLocation: 'start',
  112. nameTextStyle: {
  113. fontWeight: 'bold',
  114. fontSize: 14,
  115. color: '#333',
  116. },
  117. axisLabel: {
  118. formatter: function (value) {
  119. if (value.length > 10) {
  120. return value.substring(0, 10) + '...';
  121. }
  122. return value;
  123. },
  124. },
  125. data: days,
  126. splitArea: {
  127. show: true,
  128. },
  129. },
  130. yAxis: {
  131. type: 'category',
  132. name: '搜索词',
  133. nameGap: 20,
  134. nameTextStyle: {
  135. fontWeight: 'bold',
  136. fontSize: 14,
  137. color: '#333',
  138. },
  139. data: keywords,
  140. splitArea: {
  141. show: true,
  142. },
  143. axisLabel: {
  144. interval: 0, // 显示所有标签
  145. width: 280, // 增加标签区域的宽度以适应文本
  146. rotate: 15, // 标签旋转角度
  147. },
  148. },
  149. dataZoom: [
  150. {
  151. type: 'slider', // 纵向滚动条类型
  152. yAxisIndex: 0, // 指定作用于 Y 轴
  153. // start: 100, // 初始位置,百分比形式,数值越小,滚动条越靠下
  154. // end: 30,
  155. startValue: 80, // 初始位置,数值形式,数值越小,滚动条越靠上
  156. endValue: 100,
  157. zoomLock: true, // 锁定缩放,避免同时缩放两个滚动条
  158. showDetail: false, // 显示缩放的细节
  159. brushSelect: false,
  160. },
  161. {
  162. type: 'inside', // 内置型数据区域缩放组件(使用鼠标滚轮和拖拽)
  163. yAxisIndex: 0,
  164. zoomOnMouseWheel: false,
  165. moveOnMouseMove: true,
  166. moveOnMouseWheel: true,
  167. }
  168. ],
  169. visualMap: {
  170. min: 0,
  171. max: maxValue, // 使用计算出的最大值
  172. itemHeight: 500,
  173. calculable: true,
  174. orient: 'horizontal',
  175. left: 'center',
  176. bottom: '2%',
  177. },
  178. };
  179. if (!chart) {
  180. chart = echarts.init(chartRef.value);
  181. }
  182. chart.setOption(option);
  183. // 添加 ResizeObserver 以处理图表大小变化
  184. if (!resizeObserver) {
  185. resizeObserver = new ResizeObserver(() => {
  186. chart?.resize();
  187. });
  188. resizeObserver.observe(chartRef.value);
  189. }
  190. } catch (error) {
  191. hasData.value = false;
  192. console.error('==Error==', error);
  193. } finally {
  194. heatmapLoading.value = false;
  195. }
  196. }
  197. </script>
  198. <template>
  199. <el-card shadow="never" v-loading="heatmapLoading" class="flex flex-col" body-class="w-full">
  200. <div class="text-center flex justify-center items-center">
  201. <span class="font-medium mr-1.5">指标 </span>
  202. <el-select v-model="filter.metric" @change="fetchHeatmapData" style="width: 200px">
  203. <el-option label="搜索查询分数" value="Search_Query_Score"></el-option>
  204. <el-option label="搜索查询数量" value="Search_Query_Volume"></el-option>
  205. <el-option label="展示量-总数" value="Impressions_Total_Count"></el-option>
  206. <el-option v-if="filter.layerType === 'asin_view'" label="展示量-ASIN数量" value="Impressions_A_B_Count"></el-option>
  207. <el-option v-if="filter.layerType === 'brand_view'" label="展示量-品牌数量" value="Impressions_A_B_Count"></el-option>
  208. <el-option label="点击量-总数" value="Clicks_Total_Count"></el-option>
  209. <el-option v-if="filter.layerType === 'asin_view'" label="点击量-ASIN数量" value="Clicks_A_B_Count"></el-option>
  210. <el-option v-if="filter.layerType === 'brand_view'" label="点击量-品牌数量" value="Clicks_A_B_Count"></el-option>
  211. <el-option label="点击量-价格中位数" value="Clicks_Price_Median"></el-option>
  212. <el-option v-if="filter.layerType === 'asin_view'" label="点击量-ASIN价格中位数" value="Clicks_A_B_Price_Median"></el-option>
  213. <el-option v-if="filter.layerType === 'brand_view'" label="点击量-品牌价格中位数" value="Clicks_A_B_Price_Median"></el-option>
  214. <el-option label="加购-总数" value="Cart_Adds_Total_Count"></el-option>
  215. <el-option v-if="filter.layerType === 'asin_view'" label="加购-ASIN数量" value="Cart_Adds_A_B_Count"></el-option>
  216. <el-option v-if="filter.layerType === 'brand_view'" label="加购-品牌数量" value="Cart_Adds_A_B_Count"></el-option>
  217. <el-option label="加购-价格中位数" value="Cart_Adds_Price_Median"></el-option>
  218. <el-option v-if="filter.layerType === 'asin_view'" label="加购-ASIN价格中位数" value="Cart_Adds_A_B_Price_Median"></el-option>
  219. <el-option v-if="filter.layerType === 'brand_view'" label="加购-品牌价格中位数" value="Cart_Adds_A_B_Price_Median"></el-option>
  220. <el-option label="购买-总数" value="Purchases_Total_Count"></el-option>
  221. <el-option v-if="filter.layerType === 'asin_view'" label="购买-ASIN数量" value="Purchases_A_B_Count"></el-option>
  222. <el-option v-if="filter.layerType === 'brand_view'" label="购买-品牌数量" value="Purchases_A_B_Count"></el-option>
  223. <el-option label="购买-价格中位数" value="Purchases_Price_Median"></el-option>
  224. <el-option v-if="filter.layerType === 'asin_view'" label="购买-ASIN价格中位数" value="Purchases_A_B_Price_Median"></el-option>
  225. <el-option v-if="filter.layerType === 'brand_view'" label="购买-品牌价格中位数" value="Purchases_A_B_Price_Median"></el-option>
  226. </el-select>
  227. </div>
  228. <div class="w-full" style="min-height: 800px">
  229. <div v-show="!heatmapLoading && !hasData" style="min-height: 800px" class="flex justify-center items-center">
  230. <el-empty :image-size="300" />
  231. </div>
  232. <div v-show="hasData" ref="chartRef" style="width: 100%; height: 800px"></div>
  233. </div>
  234. </el-card>
  235. </template>
  236. <style scoped></style>