Browse Source

feat: 新增TopSearchTerm Rank页面; 修复table筛选栏错位bug;

WanGxC 11 tháng trước cách đây
mục cha
commit
ee1d71c66c

+ 11 - 0
src/views/keyword/topSearchTermRank/api.ts

@@ -0,0 +1,11 @@
+import { request } from '/@/utils/service';
+
+const apiPrefix = '/api/searchterm/';
+
+export function getTopSearchTermTable(query: any) {
+  return request({
+    url: apiPrefix + 'topsearchtermRank/',
+    method: 'GET',
+    params: query,
+  });
+}

+ 97 - 0
src/views/keyword/topSearchTermRank/column-chart.vue

@@ -0,0 +1,97 @@
+<script setup lang="ts">
+/**
+ * @Name: column-chart.vue
+ * @Description:
+ * @Author: Cheney
+ */
+
+import { PropType, ref, onMounted, onUnmounted, watch } from 'vue';
+import * as echarts from 'echarts';
+
+const props = defineProps({
+  rowData: {
+    type: Array as PropType<{ [key: string]: number }[]>,
+    required: true,
+  },
+});
+
+const chartRef = ref<HTMLElement | null>(null);
+let chart: echarts.ECharts | null = null;
+let resizeObserver: ResizeObserver | null = null
+
+onMounted(() => {
+  initChart();
+});
+
+onUnmounted(() => {
+  // 清理 ResizeObserver
+  if (resizeObserver) {
+    resizeObserver.disconnect()
+  }
+  if (chart) {
+    chart.dispose()
+    chart = null
+  }
+})
+
+watch(
+    () => props.rowData,
+    () => {
+      updateChart();
+    },
+    { immediate: true, deep: true }
+);
+
+function updateChart() {
+  if (!chart || !props.rowData || props.rowData.length === 0) return;
+
+  const data = props.rowData[0];
+  const dates = Object.keys(data);
+  const values = Object.values(data);
+
+  const option: echarts.EChartsOption = {
+    xAxis: {
+      type: 'category',
+      data: dates,
+      axisLabel: {
+        rotate: 45,
+        interval: 0,
+      },
+    },
+    yAxis: {
+      type: 'value',
+    },
+    series: [
+      {
+        data: values,
+        type: 'line',
+      },
+    ],
+    tooltip: {
+      trigger: 'axis',
+    },
+  };
+  chart.setOption(option);
+}
+
+function initChart() {
+  if (chartRef.value) {
+    chart = echarts.init(chartRef.value);
+    updateChart();
+
+    // 创建 ResizeObserver
+    resizeObserver = new ResizeObserver(() => {
+      chart?.resize()
+    })
+
+    // 观察 chartRef 元素
+    resizeObserver.observe(chartRef.value)
+  }
+}
+</script>
+
+<template>
+  <div ref="chartRef" style="width: 100%; height: 200px"></div>
+</template>
+
+<style scoped></style>

+ 230 - 0
src/views/keyword/topSearchTermRank/index.vue

@@ -0,0 +1,230 @@
+<script setup lang="ts">
+/**
+ * @Name: index.vue
+ * @Description:
+ * @Author: Cheney
+ */
+
+import { Key, PictureRounded, Refresh, Search, TopRight } from '@element-plus/icons-vue';
+import { onMounted, ref, watch } from 'vue';
+import dayjs from 'dayjs';
+import { useRouter } from 'vue-router';
+import { marketplaceIdEnum } from '/@/utils/marketplaceIdEnum';
+import { usePagination } from '/@/utils/usePagination';
+import { getTopSearchTermTable } from './api';
+import { ElMessage } from 'element-plus';
+import ColumnChart from '/src/views/keyword/topSearchTermRank/column-chart.vue';
+
+const router = useRouter();
+
+const date = ref([dayjs().subtract(7, 'day').format('YYYY-MM-DD'), dayjs().subtract(1, 'day').format('YYYY-MM-DD')]);
+const { tableData, total, currentPage, pageSize, handlePageChange } = usePagination(fetchTableData);
+const marketplaceSelect = ref(marketplaceIdEnum[0].value); // 当前只有美国区 默认第一个为美国
+const marketplaceOptions = marketplaceIdEnum;
+const reportTypeSelect = ref('weekly');
+const searchTermInp = ref('');
+const asinInp = ref('');
+const tableLoading = ref(false);
+
+onMounted(() => {
+  fetchTableData();
+});
+
+watch(date, () => {
+  fetchTableData();
+});
+
+async function refreshTable() {
+  currentPage.value = 1;
+  pageSize.value = 10;
+  asinInp.value = '';
+  searchTermInp.value = '';
+  reportTypeSelect.value = 'weekly';
+  marketplaceSelect.value = marketplaceIdEnum[0].value;
+  await fetchTableData();
+}
+
+async function fetchTableData() {
+  tableLoading.value = true;
+  const query = {
+    date_start: date.value[0],
+    date_end: date.value[1],
+    page: currentPage.value,
+    limit: pageSize.value,
+    asin: asinInp.value,
+    search_term: searchTermInp.value,
+    report_type: reportTypeSelect.value,
+    marketplace_Ids: marketplaceSelect.value,
+  };
+  const response = await getTopSearchTermTable(query);
+  total.value = response.total;
+  tableData.value = response.data;
+  tableLoading.value = false;
+}
+
+async function handleSelectChange() {
+  await fetchTableData();
+}
+
+async function handleQueryChange() {
+  if (!validateSearchTermInput(searchTermInp.value)) {
+    if (searchTermInp.value.length == 0) {
+      return;
+    } else {
+      ElMessage.warning({ message: '关键词只能输入数字和英文字母', plain: true });
+      return;
+    }
+  }
+  if (asinInp.value.length > 0 && !validateAsinInput(asinInp.value)) {
+    ElMessage.warning({ message: '不符合匹配规范', plain: true });
+    return;
+  }
+  await fetchTableData();
+}
+
+/**
+ * 校验SearchTerm输入是否合法
+ * @param input 输入的字符串
+ */
+function validateSearchTermInput(input: string) {
+  const regex = /^[a-zA-Z0-9\s]*$/;
+  return regex.test(input);
+}
+
+/**
+ * 校验Asin输入是否合法
+ * @param input 输入的字符串
+ */
+function validateAsinInput(input: string) {
+  const regex = /^[Bb]0[A-Za-z0-9\s]*$/i;
+  return regex.test(input);
+}
+
+function handleJump() {
+  router.push({ path: '/keyword/rootWordManage' });
+}
+</script>
+
+<template>
+  <div class="mx-3" style="margin-top: -8px">
+    <el-divider>
+      <!--<el-icon>-->
+      <!--  <star-filled />-->
+      <!--</el-icon>-->
+      <div class="font-bold text-lg">
+        <el-icon style="top: 3px">
+          <Memo />
+        </el-icon>
+        Top Search Term - Rank
+      </div>
+    </el-divider>
+  </div>
+  <el-card class="mx-3" v-loading="tableLoading" style="border: none">
+    <!-- table筛选栏 -->
+    <div class="flex justify-between">
+      <div class="flex gap-5 flex-wrap">
+        <div>
+          <span class="font-medium mr-0.5">市场 </span>
+          <el-select v-model="marketplaceSelect" @change="handleSelectChange" style="width: 130px">
+            <el-option
+              v-for="item in marketplaceOptions"
+              :disabled="item.disabled"
+              :key="item.value"
+              :value="item.value"
+              :label="item.label" />
+          </el-select>
+        </div>
+        <div>
+          <span class="font-medium mr-0.5">报告类型 </span>
+          <el-select v-model="reportTypeSelect" @change="handleSelectChange" style="width: 100px">
+            <el-option label="周度" value="weekly" />
+            <el-option label="月度" value="monthly" />
+          </el-select>
+        </div>
+        <div>
+          <span class="font-medium mr-0.5">关键词 </span>
+          <el-input
+            v-model="searchTermInp"
+            @keyup.enter="handleQueryChange"
+            :prefix-icon="Search"
+            placeholder="输入后回车查询"
+            clearable
+            style="width: 300px"></el-input>
+        </div>
+        <div>
+          <span class="font-medium mr-0.5">ASIN </span>
+          <el-input
+            v-model="asinInp"
+            @keyup.enter="handleQueryChange"
+            :prefix-icon="Search"
+            placeholder="输入后回车查询"
+            clearable
+            style="width: 180px"></el-input>
+        </div>
+        <div>
+          <span class="font-medium mr-0.5">报告日期 </span>
+          <el-date-picker
+            v-model="date"
+            type="daterange"
+            value-format="YYYY-MM-DD"
+            :popper-options="{ placement: 'bottom-end' }"
+            :clearable="false"
+            :disabled-date="(time: Date) => time > new Date()"
+            range-separator="至"
+            start-placeholder="开始日期"
+            end-placeholder="结束日期" />
+        </div>
+      </div>
+      <div class="flex">
+        <el-button type="primary" plain @click="handleJump" :icon="TopRight">关键词管理</el-button>
+        <el-button @click="refreshTable" :icon="Refresh" circle></el-button>
+      </div>
+    </div>
+    <!-- table -->
+    <el-card shadow="never" class="mt-5">
+      <div style="height: 795px; overflow: auto">
+        <el-table :data="tableData" stripe style="width: 100%">
+          <el-table-column fixed prop="searchTerm" label="关键词" width="260">
+            <template #header>
+              <el-icon style="top: 2px; margin-right: 3px">
+                <Key />
+              </el-icon>
+              <span>关键词</span>
+            </template>
+            <template #default="{ row }">
+              <el-link :underline="false" href="https://www.bilibili.com/" target="_blank" style="color: #0b3289"
+                >{{ row.searchTerm }}
+              </el-link>
+            </template>
+          </el-table-column>
+          <el-table-column prop="rank" label="关键词搜索排名" align="center">
+            <template #header>
+              <el-icon style="top: 2px; margin-right: 4px">
+                <PictureRounded />
+              </el-icon>
+              <span>Tendency</span>
+            </template>
+            <template #default="{ row }">
+              <ColumnChart :rowData="row.rank" />
+            </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>
+:deep(.el-divider__text.is-center.el-divider__text) {
+  background-color: #f8f8f8;
+}
+</style>

+ 0 - 8
src/views/keyword/topSearchTermTable/api.ts

@@ -9,11 +9,3 @@ export function getTopSearchTermTable(query: any) {
     params: query,
   });
 }
-
-// export function postCreateSearchTerm(body: any) {
-//   return request({
-//     url: apiPrefix + 'topsearchtermroot/',
-//     method: 'POST',
-//     data: body,
-//   });
-// }

+ 6 - 26
src/views/keyword/topSearchTermTable/index.vue

@@ -18,7 +18,7 @@ import {
   Pointer,
   Rank,
   Reading,
-  RefreshRight,
+  Refresh,
   Search,
   Switch,
   TopRight,
@@ -89,12 +89,10 @@ async function handleQueryChange() {
       return;
     }
   }
-
   if (asinInp.value.length > 0 && !validateAsinInput(asinInp.value)) {
     ElMessage.warning({ message: '不符合匹配规范', plain: true });
     return;
   }
-
   await fetchTableData();
 }
 
@@ -136,25 +134,8 @@ function getTagStyle(clickShareRank: number): Record<string, string> {
 </script>
 
 <template>
-  <!--<div class="mt-3 mx-1.5" style="background-color: #f7f7f7">-->
-  <!--  <div class="flex justify-between mt-1.5 mx-2">-->
-  <!--    <div class="font-bold text-lg">-->
-  <!--      <el-icon style="top: 3px">-->
-  <!--        <Memo />-->
-  <!--      </el-icon>-->
-  <!--      Top Search Term - Table-->
-  <!--    </div>-->
-  <!--    <div>-->
-  <!--      <el-button type="primary" plain @click="handleJump" :icon="TopRight">关键词管理</el-button>-->
-  <!--      <el-button type="success" plain round :icon="Download">下载表格</el-button>-->
-  <!--    </div>-->
-  <!--  </div>-->
-  <!--</div>-->
   <div class="mx-3" style="margin-top: -8px">
     <el-divider>
-      <!--<el-icon>-->
-      <!--  <star-filled />-->
-      <!--</el-icon>-->
       <div class="font-bold text-lg">
         <el-icon style="top: 3px">
           <Memo />
@@ -219,13 +200,12 @@ function getTagStyle(clickShareRank: number): Record<string, string> {
             end-placeholder="结束日期" />
         </div>
       </div>
-      <div>
+      <div class="flex">
         <el-button type="primary" plain @click="handleJump" :icon="TopRight">关键词管理</el-button>
         <el-button type="success" plain round :icon="Download">下载表格</el-button>
-      <el-button @click="refreshTable" :icon="RefreshRight" circle></el-button>
+        <el-button @click="refreshTable" :icon="Refresh" circle></el-button>
       </div>
     </div>
-
     <!-- table -->
     <el-card shadow="never" class="mt-5">
       <div style="height: 795px; overflow: auto">
@@ -238,9 +218,9 @@ function getTagStyle(clickShareRank: number): Record<string, string> {
               <span>关键词</span>
             </template>
             <template #default="{ row }">
-              <el-link :underline="false" href="https://www.bilibili.com/" target="_blank" style="color: #0b3289">{{
-                row.searchTerm
-              }}</el-link>
+              <el-link :underline="false" href="https://www.bilibili.com/" target="_blank" style="color: #0b3289"
+                >{{ row.searchTerm }}
+              </el-link>
             </template>
           </el-table-column>
           <el-table-column prop="searchFrequencyRank" label="关键词搜索排名" align="center" width="150">