IndicatorFunnel.vue 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  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. // 根据条件有选择性地添加 [filter.value.layerType.split('_')[0]]: filter.value.variable
  48. if (funnelFilter.layerSelect === 'asin_view' || funnelFilter.layerSelect === 'brand_view') {
  49. query[funnelFilter.layerSelect.split('_')[0]] = filter.value.variable;
  50. }
  51. try {
  52. const response = await api.getFunnelData(query);
  53. if (!response.data || Object.keys(response.data).length === 0) {
  54. hasData.value = false;
  55. if (chart) {
  56. chart.clear();
  57. }
  58. return;
  59. }
  60. hasData.value = true;
  61. // 转换数据
  62. const rawData = response.data;
  63. const funnelData = Object.entries(rawData)
  64. .filter(([key]) => key.endsWith('log2'))
  65. .map(([key, value]) => {
  66. const name =
  67. key.replace('_Total_Countlog2', '').charAt(0).toUpperCase() +
  68. key.replace('_Total_Countlog2', '').slice(1).toLowerCase();
  69. const originalKey = key.replace('log2', '');
  70. return {
  71. value: value,
  72. name: name,
  73. originalValue: rawData[originalKey],
  74. };
  75. });
  76. const option = {
  77. title: {
  78. text: '购买旅程漏斗图',
  79. },
  80. tooltip: {
  81. trigger: 'item',
  82. formatter: function (params) {
  83. return `${params.name}: ${params.data.originalValue.toLocaleString()}`;
  84. },
  85. },
  86. // toolbox: {
  87. // feature: {
  88. // dataView: { readOnly: false },
  89. // },
  90. // },
  91. series: [
  92. {
  93. type: 'funnel',
  94. left: '10%',
  95. top: 50,
  96. bottom: 0,
  97. width: '80%',
  98. min: 0,
  99. max: 100,
  100. minSize: '0%',
  101. maxSize: '100%',
  102. sort: 'descending',
  103. gap: 2,
  104. labelLine: {
  105. length: 10,
  106. lineStyle: {
  107. width: 1,
  108. type: 'solid',
  109. },
  110. },
  111. itemStyle: {
  112. borderColor: '#fff',
  113. borderWidth: 1,
  114. },
  115. emphasis: {
  116. label: {
  117. fontSize: 20,
  118. },
  119. },
  120. data: funnelData,
  121. },
  122. ],
  123. };
  124. initChart(option);
  125. } catch (error) {
  126. console.error('==Error==', error);
  127. } finally {
  128. funnelLoading.value = false;
  129. }
  130. }
  131. function initChart(opt) {
  132. if (!chart) {
  133. chart = echarts.init(chartRef.value);
  134. }
  135. chart.setOption(opt);
  136. // 添加 ResizeObserver 以处理图表大小变化
  137. if (!resizeObserver) {
  138. resizeObserver = new ResizeObserver(() => {
  139. chart?.resize();
  140. });
  141. resizeObserver.observe(chartRef.value);
  142. }
  143. }
  144. </script>
  145. <template>
  146. <el-card shadow="never" v-loading="funnelLoading" class="mt-5">
  147. <div class="flex gap-5 mb-4 justify-center">
  148. <div>
  149. <span class="font-medium mr-0.5">层级 </span>
  150. <el-select v-model="funnelFilter.layerSelect" @change="fetchFunnelData" style="width: 130px">
  151. <el-option label="Asin View" value="asin_view" />
  152. <el-option label="Brand View" value="brand_view" />
  153. <el-option label="All Asin" value="all_asin" />
  154. <el-option label="All Brand" value="all_brand" />
  155. </el-select>
  156. </div>
  157. <div>
  158. <span class="font-medium mr-0.5">搜索词 </span>
  159. <el-input v-model="funnelFilter.searchTermInp" :prefix-icon="Search" @change="fetchFunnelData" style="width: 200px" />
  160. </div>
  161. </div>
  162. <div class="w-full" style="min-height: 500px">
  163. <div v-show="!funnelLoading && !hasData" class="no-data-message" style="min-height: 500px">
  164. <el-empty :image-size="300" />
  165. </div>
  166. <div v-show="hasData" ref="chartRef" style="width: 100%; height: 500px"></div>
  167. </div>
  168. </el-card>
  169. </template>
  170. <style scoped></style>