소스 검색

✨ feat: 搜索词-分析页初步完成

WanGxC 9 달 전
부모
커밋
e8e8453a6e

+ 2 - 2
.env.development

@@ -4,8 +4,8 @@ ENV = 'development'
 
 # 本地环境接口地址
 # VITE_API_URL = 'http://127.0.0.1:8000'
- VITE_API_URL = 'http://192.168.1.225/'
-# VITE_API_URL = 'http://192.168.1.16:8080/'
+# VITE_API_URL = 'http://192.168.1.225/'
+ VITE_API_URL = 'http://192.168.1.19:8080/'
 # VITE_API_URL = 'http://amzads.zositechc.cn'
 
 # 是否启用按钮权限

+ 2 - 2
src/views/searchTerm/analysisPage/IndicatorChart.vue

@@ -6,7 +6,7 @@
  */
 import IndicatorHeatmap from './IndicatorHeatmap.vue';
 import IndicatorFunnel from './IndicatorFunnel.vue';
-import IndicatorXBar from './IndicatorXBar.vue';
+import WordCloud from './WordCloud.vue';
 </script>
 
 <template>
@@ -14,7 +14,7 @@ import IndicatorXBar from './IndicatorXBar.vue';
     <IndicatorHeatmap />
     <div class="flex gap-5">
       <IndicatorFunnel class="flex-1" />
-      <IndicatorXBar class="flex-1" />
+      <WordCloud class="flex-1" />
     </div>
   </el-card>
 </template>

+ 0 - 191
src/views/searchTerm/analysisPage/IndicatorXBar.vue

@@ -1,191 +0,0 @@
-<script setup lang="ts">
-/**
- * @Name: IndicatorXBar.vue
- * @Description: 搜索词-分析页-柱状图
- * @Author: Cheney
- */
-import { inject, onBeforeUnmount, reactive, Ref, ref, watch } from 'vue';
-import * as api from '/@/views/searchTerm/analysisPage/api';
-import * as echarts from 'echarts';
-import emitter from '/@/utils/emitter';
-
-const filter = inject<Ref>('filter');
-const barLoading = ref(false);
-
-const barFilter = reactive({
-  layerSelect: 'Impressions_A_B_Count',
-  searchTermInp: '',
-});
-
-let responseData = null;
-const chartRef = ref<HTMLElement | null>(null);
-let chart: echarts.ECharts | null = null;
-let resizeObserver: ResizeObserver | null = null;
-const hasData = ref(true);
-
-onBeforeUnmount(() => {
-  emitter.all.clear();
-  // 清理 ResizeObserver
-  if (resizeObserver) {
-    resizeObserver.disconnect();
-  }
-  if (chart) {
-    chart.dispose();
-    chart = null;
-  }
-});
-
-emitter.on('QueryCondition-sendRequest', () => {
-  fetchBarData();
-});
-
-// watch(filter.value,() => {
-//   barFilter.layerSelect = filter.value.layerType;
-// })
-
-async function fetchBarData() {
-  barLoading.value = true;
-  const query = {
-    date_start: filter.value.reportDate[0],
-    date_end: filter.value.reportDate[1],
-    report_range: filter.value.reportType,
-    layer_type: filter.value.layerType,
-    search_term: barFilter.searchTermInp,
-    [filter.value.layerType.split('_')[0]]: filter.value.variable,
-    metrics: barFilter.layerSelect
-  };
-  try {
-    responseData = await api.getBarData(query);
-    if (!responseData.data || responseData.data.length === 0) {
-      // 处理空数据的情况
-      hasData.value = false;
-      if (chart) {
-        chart.clear();
-      }
-      return;
-    }
-
-    hasData.value = true;
-
-    const asinList = Object.keys(responseData.data[0]).filter(key => key !== 'Search_Query');
-
-// 准备图表数据
-    const searchQueries = responseData.data.map(item => item.Search_Query);
-    const seriesData = asinList.map(asin => ({
-      name: asin,
-      type: 'bar',
-      stack: 'total',
-      label: {
-        show: true,
-        position: 'inside',
-        formatter: '{c}'
-      },
-      emphasis: {
-        focus: 'series'
-      },
-      data: responseData.data.map(item => item[asin] || 0)
-    }));
-
-    const option = {
-      tooltip: {
-        trigger: 'axis',
-        axisPointer: {
-          type: 'shadow'
-        }
-      },
-      legend: {
-        orient: 'horizontal',
-        top: 'bottom'
-      },
-      grid: {
-        left: '3%',
-        right: '4%',
-        bottom: '15%',
-        containLabel: true
-      },
-      xAxis: {
-        type: 'value'
-      },
-      yAxis: {
-        type: 'category',
-        data: searchQueries,
-        axisLabel: {
-          interval: 0,
-          formatter: function (value) {
-            return value.length > 20 ? value.substring(0, 20) + '...' : value;
-          }
-        }
-      },
-      series: seriesData
-    };
-
-    initChart(option);
-  } catch (error) {
-    console.error('==Error==', error);
-  } finally {
-    barLoading.value = false;
-  }
-}
-
-function initChart(opt: echarts.EChartsCoreOption) {
-  if (!chart) {
-    chart = echarts.init(chartRef.value);
-  }
-  chart.setOption(opt);
-
-  // 添加 ResizeObserver 以处理图表大小变化
-  if (!resizeObserver) {
-    resizeObserver = new ResizeObserver(() => {
-      chart?.resize();
-    });
-    resizeObserver.observe(chartRef.value);
-  }
-}
-
-function changeChart() {
-  const option = {
-    title: {
-      text: 'Asin/Brand 关键词情况',
-    },
-    tooltip: {
-      trigger: 'axis',
-      axisPointer: {
-        type: 'shadow',
-      },
-    },
-    legend: {},
-    // grid: {
-    //   left: '3%',
-    //   right: '4%',
-    //   bottom: '3%',
-    //   containLabel: true,
-    // },
-    dataset: {
-      dimensions: ['Search_Query', 'Impressions_A_B_Count', 'Clicks_Total_Count', 'Cart_Adds_Total_Count', 'Purchases_Total_Count'],
-      source: responseData.data
-    }
-  }
-}
-</script>
-
-<template>
-  <el-card shadow="never" v-loading="barLoading" class="mt-5">
-    <div class="flex gap-5 mb-4 justify-center">
-      <div>
-        <span class="font-medium mr-0.5">层级 </span>
-        <el-select v-model="barFilter.layerSelect" @change="changeChart" style="width: 130px">
-          <el-option label="曝光次数" value="Impressions_A_B_Count" />
-          <el-option label="点击次数" value="Clicks_Total_Count" />
-          <el-option label="购买次数" value="Purchases_Total_Count" />
-          <el-option label="加购次数" value="Cart_Total_Count" />
-        </el-select>
-      </div>
-    </div>
-    <div v-show="!barLoading && !hasData" class="no-data-message" style="min-height: 500px">
-      <el-empty />
-    </div>
-    <div v-show="hasData" ref="chartRef" style="width: 100%; height: 500px"></div>
-  </el-card>
-</template>
-
-<style scoped></style>

+ 58 - 20
src/views/searchTerm/analysisPage/QuerySummary.vue

@@ -32,7 +32,7 @@ async function fetchTableData() {
     layer_type: filter.value.layerType,
     searchTerm_type: typeSelect.value,
     page: currentPage.value,
-    limit: pageSize
+    limit: pageSize.value
   };
   try {
     const response = await api.getTableData(query);
@@ -48,35 +48,73 @@ async function fetchTableData() {
   }
 }
 
-function changeType() {}
+function changeType() {
+  fetchTableData();
+}
 </script>
 
 <template>
-  <el-card shadow="never" v-loading="loading">
-    <div class="flex gap-5 mb-4 justify-center">
-      <div>
-        <span class="font-medium mr-0.5">类型 </span>
+  <el-card v-loading="loading" style="border: none;">
+    <div class="relative flex gap-5 mb-5">
+      <span class="absolute left-7 font-bold text-xl mb-4 text-center" style="color: #464646">搜索词-分析页-Query汇总</span>
+      <div class="flex justify-center w-full items-center">
+        <span class="font-medium mr-1.5">类型 </span>
         <el-select v-model="typeSelect" @change="changeType" style="width: 130px">
           <el-option label="positive" value="positive" />
           <el-option label="negative" value="negative" />
         </el-select>
       </div>
     </div>
-    <div style="height: 100%; overflow: auto">
-      <el-table :data="tableData" height="550" stripe style="width: 100%">
-        <!--<el-table-column prop="keyword" label="搜索词" width="200" />-->
-      </el-table>
-    </div>
-    <div class="mt-3.5 flex justify-end">
-      <el-pagination
-        v-model:current-page="currentPage"
-        v-model:page-size="pageSize"
-        :page-sizes="[10, 20, 30, 50, 100, 200]"
-        layout="sizes, prev, pager, next"
-        :total="total"
-        @change="handlePageChange" />
-    </div>
+    <el-card shadow="never">
+      <div style="height: 100%; overflow: auto">
+        <el-table :data="tableData" height="600" stripe style="width: 100%">
+          <el-table-column type="index" width="55" />
+          <el-table-column prop="Search_Query" label="搜索词" min-width="200">
+            <template #default="{ row }">
+              <el-link :underline="false" target="_blank" style="color: #5a6fc0">
+                {{ row.Search_Query }}
+              </el-link>
+            </template>
+          </el-table-column>
+          <el-table-column prop="count" label="出现次数" align="center" min-width="200">
+            <template #default="{ row }">
+              <span class="font-medium">{{ row.count }}</span>
+            </template>
+          </el-table-column>
+          <el-table-column prop="Impressions_Total_Count" label="曝光次数" align="center" min-width="200">
+            <template #default="{ row }">
+              <span class="font-medium">{{ row.Impressions_Total_Count ? row.Impressions_Total_Count : '--' }}</span>
+            </template>
+          </el-table-column>
+          <el-table-column prop="Clicks_Total_Count" label="点击次数" align="center" min-width="200">
+            <template #default="{ row }">
+              <span class="font-medium">{{ row.Clicks_Total_Count ? row.Clicks_Total_Count : '--' }}</span>
+            </template>
+          </el-table-column>
+          <el-table-column prop="Cart_Adds_Total_Count" label="加购次数" align="center" min-width="200">
+            <template #default="{ row }">
+              <span class="font-medium">{{ row.Cart_Adds_Total_Count ? row.Cart_Adds_Total_Count : '--' }}</span>
+            </template>
+          </el-table-column>
+          <el-table-column prop="Purchases_Total_Count" label="购买次数" align="center" min-width="200">
+            <template #default="{ row }">
+              <span class="font-medium">{{ row.Purchases_Total_Count ? row.Purchases_Total_Count : '--' }}</span>
+            </template>
+          </el-table-column>
+        </el-table>
+      </div>
+      <div class="mt-3.5 flex justify-end">
+        <el-pagination
+            v-model:current-page="currentPage"
+            v-model:page-size="pageSize"
+            :page-sizes="[10, 20, 30, 50, 100, 200]"
+            layout="sizes, prev, pager, next"
+            :total="total"
+            @change="handlePageChange" />
+      </div>
+    </el-card>
   </el-card>
+
 </template>
 
 <style scoped></style>

+ 191 - 0
src/views/searchTerm/analysisPage/WordCloud.vue

@@ -0,0 +1,191 @@
+<script setup lang="ts">
+/**
+ * @Name: WordCloud.vue
+ * @Description: 搜索词-分析页-词云图
+ * @Author: Cheney
+ */
+import { inject, onBeforeUnmount, reactive, Ref, ref } from 'vue';
+import * as api from '/@/views/searchTerm/analysisPage/api';
+import * as echarts from 'echarts';
+import 'echarts-wordcloud';
+import emitter from '/@/utils/emitter';
+
+const filter = inject<Ref>('filter');
+const loading = ref(false);
+
+const wordCloudFilter = reactive({
+  typeSelect1: 'positive',
+  typeSelect2: 'Impressions_Total_Count',
+});
+
+let responseData = null;
+const chartRef = ref<HTMLElement | null>(null);
+let chart: echarts.ECharts | null = null;
+let resizeObserver: ResizeObserver | null = null;
+const hasData = ref(true);
+
+onBeforeUnmount(() => {
+  emitter.all.clear();
+  // 清理 ResizeObserver
+  if (resizeObserver) {
+    resizeObserver.disconnect();
+  }
+  if (chart) {
+    chart.dispose();
+    chart = null;
+  }
+});
+
+emitter.on('QueryCondition-sendRequest', () => {
+  fetchWordCloudData();
+});
+
+async function fetchWordCloudData() {
+  loading.value = true;
+  const query = {
+    layer_type: filter.value.layerType,
+    report_range: filter.value.reportType,
+    searchTerm_type: wordCloudFilter.typeSelect1,
+  };
+  try {
+    responseData = await api.getWordCloudData(query);
+    if (!responseData.data || responseData.data.length === 0) {
+      // 处理空数据的情况
+      hasData.value = false;
+      if (chart) {
+        chart.clear();
+      }
+      return;
+    }
+    hasData.value = true;
+
+    const selectedMetric = wordCloudFilter.typeSelect2;
+    // 生成词云数据
+    const wordCloudData = responseData.data.map((item) => ({
+      name: item.root_search_term,
+      value: item[selectedMetric],
+    }));
+
+    let option = {
+      title: {
+        text: '搜索词词云图',
+      },
+      tooltip: {
+        show: true,
+      },
+      series: [
+        {
+          type: 'wordCloud',
+          // 词云图的形状
+          shape: 'circle', // 可以是 'circle', 'cardioid', 'diamond', 'triangle-forward', 'triangle', 'pentagon', 'star'
+          // 控制字体大小
+          sizeRange: [12, 45],
+          // 文本旋转角度
+          rotationRange: [-90, 90],
+          data: wordCloudData,
+          emphasis: {
+            focus: 'self', // 聚焦悬浮项
+            textStyle: {
+              fontSize: 50, // 放大效果的字体大小
+            },
+          },
+        },
+      ],
+      backgroundColor: '#fff',
+    };
+    // console.log('selectedMetric', selectedMetric)
+
+    console.log('wordCloudData', wordCloudData);
+    initChart(option);
+  } catch (error) {
+    console.error('==Error==', error);
+  } finally {
+    loading.value = false;
+  }
+}
+
+function initChart(opt: echarts.EChartsCoreOption) {
+  if (!chart) {
+    chart = echarts.init(chartRef.value);
+  }
+  chart.setOption(opt);
+
+  // 添加 ResizeObserver 以处理图表大小变化
+  if (!resizeObserver) {
+    resizeObserver = new ResizeObserver(() => {
+      chart?.resize();
+    });
+    resizeObserver.observe(chartRef.value);
+  }
+}
+
+function handleSelectChange() {
+  fetchWordCloudData();
+}
+
+function changeChart() {
+  const selectedMetric = wordCloudFilter.typeSelect2;
+  // 生成词云数据
+  const wordCloudData = responseData.data.map((item) => ({
+    name: item.root_search_term,
+    value: item[selectedMetric],
+  }));
+
+  let option = {
+    tooltip: {
+      show: true,
+    },
+    series: [
+      {
+        type: 'wordCloud',
+        // 词云图的形状
+        shape: 'circle', // 可以是 'circle', 'cardioid', 'diamond', 'triangle-forward', 'triangle', 'pentagon', 'star'
+        // 控制字体大小
+        sizeRange: [12, 45],
+        // 文本旋转角度
+        rotationRange: [-90, 90],
+        data: wordCloudData,
+        emphasis: {
+          focus: 'self', // 聚焦悬浮项
+          textStyle: {
+            fontSize: 50, // 放大效果的字体大小
+          },
+        },
+      },
+    ],
+    backgroundColor: '#fff',
+  };
+
+  initChart(option);
+}
+</script>
+
+<template>
+  <el-card shadow="never" v-loading="loading" class="mt-5">
+    <div class="flex gap-5 mb-4 justify-center">
+      <div>
+        <span class="font-medium mr-0.5">类型 </span>
+        <el-select v-model="wordCloudFilter.typeSelect1" @change="handleSelectChange" style="width: 130px">
+          <el-option label="positive" value="positive" />
+          <el-option label="negative" value="negative" />
+        </el-select>
+      </div>
+      <div>
+        <span class="font-medium mr-0.5">指标 </span>
+        <el-select v-model="wordCloudFilter.typeSelect2" @change="changeChart" style="width: 130px">
+          <el-option label="曝光次数" value="Impressions_Total_Count" />
+          <el-option label="点击次数" value="Clicks_Total_Count" />
+          <el-option label="加购次数" value="Cart_Adds_Total_Count" />
+          <el-option label="购买次数" value="Purchases_Total_Count" />
+        </el-select>
+      </div>
+    </div>
+    <!--</div>-->
+    <div v-show="!loading && !hasData" class="no-data-message" style="min-height: 500px">
+      <el-empty />
+    </div>
+    <div v-show="hasData" ref="chartRef" style="width: 100%; height: 500px"></div>
+  </el-card>
+</template>
+
+<style scoped></style>

+ 2 - 2
src/views/searchTerm/analysisPage/api.ts

@@ -26,9 +26,9 @@ export function getFunnelData(query: any) {
   });
 }
 
-export function getBarData(query: any) {
+export function getWordCloudData(query: any) {
   return request({
-    url: apiPrefix + 'barchart/',
+    url: apiPrefix + 'wordcloud/',
     method: 'GET',
     params: query,
   });

+ 1 - 4
src/views/searchTerm/analysisPage/index.vue

@@ -27,10 +27,7 @@ provide('filter', filter);
     <QueryCondition />
     <IndicatorOverview />
     <IndicatorChart />
-    <el-card shadow="hover" body-class="flex gap-5"  style="border: none;">
-      <QuerySummary class="flex-1" />
-      <QuerySummary class="flex-1" />
-    </el-card>
+    <QuerySummary />
 
   </div>
 </template>

+ 1 - 1
src/views/searchTerm/topSearchTermRank/index.vue

@@ -197,7 +197,7 @@ function handleJump() {
               <span>搜索词</span>
             </template>
             <template #default="{ row }">
-              <el-link :underline="false" href="https://www.bilibili.com/" target="_blank" style="color: #0b3289">
+              <el-link :underline="false" target="_blank" style="color: #5a6fc0">
                 {{ row.searchTerm }}
               </el-link>
             </template>

+ 1 - 1
src/views/searchTerm/topSearchTermTable/index.vue

@@ -338,7 +338,7 @@ function arraySpanMethod({ row, column, rowIndex, columnIndex }) {
               <span>搜索词</span>
             </template>
             <template #default="{ row }">
-              <el-link :underline="false" href="https://www.bilibili.com/" target="_blank" style="color: #0b3289"
+              <el-link :underline="false" target="_blank" style="color: #5a6fc0"
                 >{{ row.searchTerm }}
               </el-link>
             </template>