IndicatorFunnel.vue 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. <script setup lang="ts">
  2. /**
  3. * @Name: IndicatorFunnel.vue
  4. * @Description: 搜索词-分析页-漏斗图
  5. * @Author: Cheney
  6. */
  7. import { Search } from '@element-plus/icons-vue';
  8. import { inject, onBeforeUnmount, reactive, ref, Ref, watch } from 'vue';
  9. import * as api from './api';
  10. import * as echarts from 'echarts';
  11. import emitter from '/@/utils/emitter';
  12. const filter = inject<Ref>('filter');
  13. const funnelFilter = reactive({
  14. layerSelect: 'all_asin',
  15. searchTermInp: '',
  16. });
  17. const funnelLoading = ref(false);
  18. const chartRef = ref<HTMLElement | null>(null);
  19. let chart: echarts.ECharts | null = null;
  20. let resizeObserver: ResizeObserver | null = null;
  21. const hasData = ref(true);
  22. onBeforeUnmount(() => {
  23. emitter.all.clear();
  24. // 清理 ResizeObserver
  25. if (resizeObserver) {
  26. resizeObserver.disconnect();
  27. }
  28. if (chart) {
  29. chart.dispose();
  30. chart = null;
  31. }
  32. });
  33. emitter.on('QueryCondition-sendRequest', () => {
  34. fetchFunnelData();
  35. });
  36. watch(filter.value, () => {
  37. funnelFilter.layerSelect = filter.value.layerType;
  38. });
  39. async function fetchFunnelData() {
  40. funnelLoading.value = true;
  41. const query = {
  42. date_start: filter.value.reportDate[0],
  43. date_end: filter.value.reportDate[1],
  44. report_range: filter.value.reportType,
  45. layer_type: funnelFilter.layerSelect,
  46. };
  47. try {
  48. const response = await api.getFunnelData(query);
  49. if (!response.data || Object.keys(response.data).length === 0) {
  50. hasData.value = false;
  51. if (chart) {
  52. chart.clear();
  53. }
  54. return;
  55. }
  56. hasData.value = true;
  57. // 转换数据
  58. const rawData = response.data;
  59. const funnelData = Object.entries(rawData)
  60. .filter(([key]) => key.endsWith('log2'))
  61. .map(([key, value]) => {
  62. const name =
  63. key.replace('_Total_Countlog2', '').charAt(0).toUpperCase() +
  64. key.replace('_Total_Countlog2', '').slice(1).toLowerCase();
  65. const originalKey = key.replace('log2', '');
  66. return {
  67. value: value,
  68. name: name,
  69. originalValue: rawData[originalKey],
  70. };
  71. });
  72. const option = {
  73. title: {
  74. text: '购买旅程漏斗图',
  75. },
  76. tooltip: {
  77. trigger: 'item',
  78. formatter: function (params) {
  79. return `${params.name}: ${params.data.originalValue.toLocaleString()}`;
  80. },
  81. },
  82. // toolbox: {
  83. // feature: {
  84. // dataView: { readOnly: false },
  85. // },
  86. // },
  87. series: [
  88. {
  89. type: 'funnel',
  90. left: '10%',
  91. top: 50,
  92. bottom: 0,
  93. width: '80%',
  94. min: 0,
  95. max: 100,
  96. minSize: '0%',
  97. maxSize: '100%',
  98. sort: 'descending',
  99. gap: 2,
  100. labelLine: {
  101. length: 10,
  102. lineStyle: {
  103. width: 1,
  104. type: 'solid',
  105. },
  106. },
  107. itemStyle: {
  108. borderColor: '#fff',
  109. borderWidth: 1,
  110. },
  111. emphasis: {
  112. label: {
  113. fontSize: 20,
  114. },
  115. },
  116. data: funnelData,
  117. },
  118. ],
  119. };
  120. initChart(option);
  121. } catch (error) {
  122. console.error('==Error==', error);
  123. } finally {
  124. funnelLoading.value = false;
  125. }
  126. }
  127. function initChart(opt) {
  128. if (!chart) {
  129. chart = echarts.init(chartRef.value);
  130. }
  131. chart.setOption(opt);
  132. // 添加 ResizeObserver 以处理图表大小变化
  133. if (!resizeObserver) {
  134. resizeObserver = new ResizeObserver(() => {
  135. chart?.resize();
  136. });
  137. resizeObserver.observe(chartRef.value);
  138. }
  139. }
  140. </script>
  141. <template>
  142. <el-card shadow="never" v-loading="funnelLoading" class="mt-5">
  143. <div class="flex gap-5 mb-4 justify-center">
  144. <div>
  145. <span class="font-medium mr-0.5">层级 </span>
  146. <el-select v-model="funnelFilter.layerSelect" @change="fetchFunnelData" style="width: 130px">
  147. <el-option label="Asin View" value="asin_view" />
  148. <el-option label="Brand View" value="brand_view" />
  149. <el-option label="All Asin" value="all_asin" />
  150. <el-option label="All Brand" value="all_brand" />
  151. </el-select>
  152. </div>
  153. <div>
  154. <span class="font-medium mr-0.5">搜索词 </span>
  155. <el-input v-model="funnelFilter.searchTermInp" :prefix-icon="Search" @change="fetchFunnelData" style="width: 200px" />
  156. </div>
  157. </div>
  158. <div v-show="!funnelLoading && !hasData" class="no-data-message" style="min-height: 500px">
  159. <el-empty />
  160. </div>
  161. <div v-show="hasData" ref="chartRef" style="width: 100%; height: 500px"></div>
  162. </el-card>
  163. </template>
  164. <style scoped></style>