Browse Source

feat: 招商门户页面开发

周玉环 2 days ago
parent
commit
7b6aa91784

+ 7 - 0
xinkeaboard-promotion-portal/components.d.ts

@@ -10,6 +10,7 @@ declare module 'vue' {
   export interface GlobalComponents {
     ColorLevel: typeof import('./src/components/ColorLevel.vue')['default']
     CommonEmpty: typeof import('./src/components/CommonEmpty.vue')['default']
+    CompetitorList: typeof import('./src/components/CompetitorList.vue')['default']
     CompetitorWebsite: typeof import('./src/components/CompetitorWebsite.vue')['default']
     ConfirmInfoDialog: typeof import('./src/components/ConfirmInfoDialog.vue')['default']
     CountrySelct: typeof import('./src/components/CountrySelct.vue')['default']
@@ -24,15 +25,21 @@ declare module 'vue' {
     ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
     Empty: typeof import('./src/components/Empty.vue')['default']
     Home: typeof import('./src/components/Home.vue')['default']
+    Item: typeof import('./src/components/competitor/item.vue')['default']
     KeywordSearch: typeof import('./src/components/KeywordSearch.vue')['default']
     Level: typeof import('./src/components/Level.vue')['default']
+    LineChart: typeof import('./src/components/competitor/LineChart.vue')['default']
+    List: typeof import('./src/components/competitor/list.vue')['default']
     ProductDescription: typeof import('./src/components/ProductDescription.vue')['default']
+    RankTable: typeof import('./src/components/competitor/RankTable.vue')['default']
+    RecommendTable: typeof import('./src/components/competitor/RecommendTable.vue')['default']
     RouterLink: typeof import('vue-router')['RouterLink']
     RouterView: typeof import('vue-router')['RouterView']
     Search: typeof import('./src/components/keyword/search.vue')['default']
     Table: typeof import('./src/components/keyword/table.vue')['default']
     TopContent: typeof import('./src/components/TopContent.vue')['default']
     TopHeaderContent: typeof import('./src/components/TopHeaderContent.vue')['default']
+    TrafficChart: typeof import('./src/components/competitor/TrafficChart.vue')['default']
   }
   export interface GlobalDirectives {
     vLoading: typeof import('element-plus/es')['ElLoadingDirective']

+ 1 - 0
xinkeaboard-promotion-portal/package.json

@@ -9,6 +9,7 @@
   "dependencies": {
     "axios": "^1.11.0",
     "crypto-js": "^4.2.0",
+    "echarts": "^6.0.0",
     "element-plus": "^2.10.7",
     "pinia": "^2.0.0",
     "vue": "^3.2.0",

+ 23 - 0
xinkeaboard-promotion-portal/pnpm-lock.yaml

@@ -14,6 +14,9 @@ importers:
       crypto-js:
         specifier: ^4.2.0
         version: 4.2.0
+      echarts:
+        specifier: ^6.0.0
+        version: 6.0.0
       element-plus:
         specifier: ^2.10.7
         version: 2.10.7(vue@3.5.18(typescript@5.9.2))
@@ -658,6 +661,9 @@ packages:
     resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
     engines: {node: '>= 0.4'}
 
+  echarts@6.0.0:
+    resolution: {integrity: sha512-Tte/grDQRiETQP4xz3iZWSvoHrkCQtwqd6hs+mifXcjrCuo2iKWbajFObuLJVBlDIJlOzgQPd1hsaKt/3+OMkQ==}
+
   element-plus@2.10.7:
     resolution: {integrity: sha512-bL4yhepL8/0NEQA5+N2Q6ZVKLipIDkiQjK2mqtSmGh6CxJk1yaBMdG5HXfYkbk1htNcT3ULk9g23lzT323JGcA==}
     peerDependencies:
@@ -1237,6 +1243,9 @@ packages:
   tslib@1.14.1:
     resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==}
 
+  tslib@2.3.0:
+    resolution: {integrity: sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==}
+
   tsutils@3.21.0:
     resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==}
     engines: {node: '>= 6'}
@@ -1386,6 +1395,9 @@ packages:
     resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
     engines: {node: '>=10'}
 
+  zrender@6.0.0:
+    resolution: {integrity: sha512-41dFXEEXuJpNecuUQq6JlbybmnHaqqpGlbH1yxnA5V9MMP4SbohSVZsJIwz+zdjQXSSlR1Vc34EgH1zxyTDvhg==}
+
 snapshots:
 
   '@babel/helper-string-parser@7.27.1': {}
@@ -1918,6 +1930,11 @@ snapshots:
       es-errors: 1.3.0
       gopd: 1.2.0
 
+  echarts@6.0.0:
+    dependencies:
+      tslib: 2.3.0
+      zrender: 6.0.0
+
   element-plus@2.10.7(vue@3.5.18(typescript@5.9.2)):
     dependencies:
       '@ctrl/tinycolor': 3.6.1
@@ -2517,6 +2534,8 @@ snapshots:
 
   tslib@1.14.1: {}
 
+  tslib@2.3.0: {}
+
   tsutils@3.21.0(typescript@5.9.2):
     dependencies:
       tslib: 1.14.1
@@ -2650,3 +2669,7 @@ snapshots:
   xml-name-validator@4.0.0: {}
 
   yocto-queue@0.1.0: {}
+
+  zrender@6.0.0:
+    dependencies:
+      tslib: 2.3.0

+ 3 - 1
xinkeaboard-promotion-portal/src/App.vue

@@ -1,9 +1,11 @@
 <template>
-  <router-view></router-view>
+  <router-view :key="route.path"></router-view>
 </template>
 
 <script setup lang="ts">
+import { useRoute } from 'vue-router';
 // 这里写你的业务逻辑
+const route = useRoute();
 </script>
 
 <style>

BIN
xinkeaboard-promotion-portal/src/assets/images/competitor.png


+ 6 - 2
xinkeaboard-promotion-portal/src/components/CommonEmpty.vue

@@ -1,8 +1,8 @@
 <template>
   <div class="loading-wrapper">
     <div class="loading-content">
-      <div class="loading-text">{{ text }}</div>
-      <div class="progress-bar">
+      <div :class="{ 'loading-text': true, fail: fail }">{{ fail ? '生成失败' : text }}</div>
+      <div class="progress-bar" v-if="!fail">
         <div
           class="progress-fill"
           :style="{
@@ -103,6 +103,10 @@ watch(
   font-size: 20px;
   color: #282e30;
   margin-bottom: 20px;
+
+  &.fail {
+    color: red;
+  }
 }
 
 .progress-bar {

+ 4 - 0
xinkeaboard-promotion-portal/src/components/CountrySelct.vue

@@ -98,6 +98,10 @@ defineExpose({
     &-filter {
       height: 48px;
 
+      .el-input {
+        height: 48px;
+      }
+
       .el-input__prefix {
         img {
           width: 16px;

+ 105 - 0
xinkeaboard-promotion-portal/src/components/competitor/RankTable.vue

@@ -0,0 +1,105 @@
+<template>
+  <div class="rank-table">
+    <div class="rank-table-head">最有价值的关键词排名</div>
+    <el-table :data="tableData">
+      <el-table-column prop="keyword" align="center" label="关键词"> </el-table-column>
+      <el-table-column prop="searchVolume" align="center" label="搜索量">
+        <template #default="scope">
+          {{
+            scope.row.searchVolume >= 1000
+              ? (scope.row.searchVolume / 1000).toFixed(1) + 'K'
+              : scope.row.searchVolume
+          }}
+        </template>
+      </el-table-column>
+      <el-table-column prop="searchVolume" align="center" label="SEO点击次数">
+        <template #default="scope">
+          {{
+            scope.row.searchVolume >= 1000
+              ? (scope.row.searchVolume / 1000).toFixed(1) + 'K'
+              : scope.row.searchVolume
+          }}
+        </template>
+      </el-table-column>
+      <el-table-column prop="dp" align="center" label="DP(关键词难度)"> </el-table-column>
+      <el-table-column prop="cpc" align="center" label="CPC(每次点击费用)">
+        <template #default="scope">
+          {{ '$ ' + scope.row.cpc }}
+        </template>
+      </el-table-column>
+      <el-table-column prop="searchVolume" align="center" label="桌面搜索"> </el-table-column>
+    </el-table>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref } from 'vue';
+
+const tableData = ref([
+  {
+    searchVolume: 5400,
+    cpc: 0.16,
+    monthly: 50,
+    keyword: 'popmart.com',
+    quarterly: 50,
+    dp: 92,
+    yearly: 10471
+  },
+  {
+    searchVolume: 70,
+    cpc: 0.96,
+    monthly: 27,
+    keyword: 'pop art usa',
+    quarterly: 27,
+    dp: 6,
+    yearly: 600
+  }
+]);
+</script>
+
+<style lang="scss" scoped>
+.rank-table {
+  width: 100%;
+  min-height: 300px;
+
+  &-head {
+    height: 66px;
+    padding-left: 30px;
+    font-weight: bold;
+    font-size: 20px;
+    color: #282e30;
+    box-sizing: border-box;
+  }
+
+  :deep(.el-table) {
+    width: 100%;
+    height: 317px;
+
+    .el-table__header {
+      height: 52px;
+
+      .el-table__cell {
+        background: rgba(3, 110, 184, 0.1) !important;
+        font-weight: bold;
+        font-size: 16px;
+        color: #282e30;
+      }
+    }
+
+    .el-table__row {
+      height: 52px;
+      font-weight: bold;
+      font-size: 14px;
+      color: #282e30;
+    }
+  }
+  :deep(.el-table__cell) {
+    img {
+      width: 22px;
+      height: 22px;
+      position: relative;
+      top: 5px;
+    }
+  }
+}
+</style>

+ 86 - 0
xinkeaboard-promotion-portal/src/components/competitor/RecommendTable.vue

@@ -0,0 +1,86 @@
+<template>
+  <div class="recommend-table">
+    <div class="recommend-table-head">推荐关键词</div>
+    <el-table :data="tableData">
+      <el-table-column prop="keywords" align="center" label="关键词"> </el-table-column>
+      <el-table-column prop="searchVolume" align="center" label="搜索量">
+        <template #default="scope">
+          {{
+            scope.row.searchVolume >= 1000
+              ? (scope.row.searchVolume / 1000).toFixed(1) + 'K'
+              : scope.row.searchVolume
+          }}
+        </template>
+      </el-table-column>
+      <el-table-column prop="price" align="center" label="价格">
+        <template #default="scope">
+          {{ '$ ' + scope.row.price }}
+        </template>
+      </el-table-column>
+    </el-table>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref } from 'vue';
+
+const tableData = ref([
+  {
+    searchVolume: 1500000,
+    keywords: 'pop mart',
+    price: 0.11
+  },
+  {
+    searchVolume: 480,
+    keywords: 'popmart global',
+    price: 0.57
+  }
+]);
+</script>
+
+<style lang="scss" scoped>
+.recommend-table {
+  width: 100%;
+  min-height: 300px;
+
+  &-head {
+    height: 66px;
+    padding-left: 30px;
+    font-weight: bold;
+    font-size: 20px;
+    color: #282e30;
+    box-sizing: border-box;
+  }
+
+  :deep(.el-table) {
+    width: 100%;
+    height: 317px;
+
+    .el-table__header {
+      height: 52px;
+
+      .el-table__cell {
+        background: rgba(3, 110, 184, 0.1) !important;
+        font-weight: bold;
+        font-size: 16px;
+        color: #282e30;
+      }
+    }
+
+    .el-table__row {
+      height: 52px;
+      font-weight: bold;
+      font-size: 14px;
+      color: #282e30;
+    }
+  }
+  :deep(.el-table__cell) {
+    img {
+      width: 22px;
+      height: 22px;
+      position: relative;
+      top: 5px;
+    }
+  }
+}
+</style>

+ 215 - 0
xinkeaboard-promotion-portal/src/components/competitor/TrafficChart.vue

@@ -0,0 +1,215 @@
+<template>
+  <div ref="chartRef" class="chart"></div>
+</template>
+
+<script lang="ts" setup>
+import * as echarts from 'echarts';
+import { ref, onMounted, onBeforeUnmount, watch } from 'vue';
+
+const props = defineProps({
+  dates: { type: Array, required: true }, // x轴日期
+  natural: { type: Array, required: true }, // 自然流量
+  paid: { type: Array, required: true }, // 付费流量
+  centerText: { type: String, default: '自然流量' } // 环形中间文字
+});
+
+const chartRef = ref(null);
+let chartInstance: any = null;
+
+const initChart = () => {
+  if (!chartRef.value) return;
+  chartInstance = echarts.init(chartRef.value);
+
+  const option = {
+    title: {
+      text: '网站历史数据',
+      left: 18,
+      top: 0,
+      textStyle: { fontSize: 20, fontWeight: 'bold', color: '#282E30' }
+    },
+    legend: {
+      data: ['自然', '付费'],
+      right: 'center',
+      itemGap: 30,
+      top: 0,
+      textStyle: {
+        color: '#282E30', // 图例文字颜色
+        fontSize: 24
+      }
+    },
+    graphic: [
+      // {
+      //   type: 'text',
+      //   right: 17, // 整体靠右
+      //   top: 12,
+      //   style: {
+      //     text: `{a|自}{b|人工}{c|以来的完整历史数据 }`, // 使用富文本标记
+      //     rich: {
+      //       a: { color: '#000000', font: '12px Arial', padding: [0, 0, 0, 0] },
+      //       b: { color: '#036EB8', font: '12px Arial', padding: [0, 0, 0, 0] },
+      //       c: { color: '#000000', font: '12px Arial', padding: [0, 0, 0, 0] }
+      //     }
+      //   }
+      // },
+      {
+        type: 'text',
+        left: 'center', // 居中
+        bottom: 0, // legend 下方位置,根据 legend.top 调整
+        style: {
+          text: '来自 Google 的流量-自然与付费',
+          fill: 'rgba(40,46,48,0.6)', // 文字颜色
+          font: '16px Arial'
+        }
+      }
+    ],
+
+    grid: {
+      left: '9%',
+      right: '3%'
+      // top: '15%',
+      // bottom: '15%'
+    },
+    xAxis: {
+      type: 'category',
+      data: props.dates,
+      // boundaryGap: false,
+      // axisTick: { alignWithLabel: true },
+      splitLine: {
+        // x 轴竖直方向虚线
+        show: true,
+        lineStyle: {
+          type: 'dashed',
+          color: '#ccc'
+        }
+      },
+      axisLabel: {
+        color: '#282E30', // 字体颜色
+        fontSize: 12, // 字体大小
+        fontWeight: '400' // 字体粗细 normal | bold | bolder | lighter
+      },
+      boundaryGap: true // 开启后类目会在刻度中间,而不是在刻度线上
+    },
+    yAxis: {
+      type: 'value',
+      min: 0,
+      splitLine: {
+        // y 轴水平方向虚线
+        show: true,
+        lineStyle: {
+          type: 'dashed',
+          color: '#ccc'
+        }
+      }
+    },
+    series: [
+      {
+        name: '自然',
+        type: 'line',
+        data: props.natural,
+        smooth: false,
+        symbol: 'circle',
+        symbolSize: 12,
+        lineStyle: {
+          color: '#7ECEF4'
+        },
+        itemStyle: {
+          color: '#7ECEF4',
+          borderColor: '#fff',
+          borderWidth: 2,
+          shadowColor: 'rgba(126, 206, 244, 0.4)',
+          shadowBlur: 10
+        },
+        areaStyle: null,
+      },
+      {
+        name: '付费',
+        type: 'line',
+        data: props.paid,
+        smooth: false,
+        symbol: 'circle',
+        symbolSize: 10,
+        lineStyle: {
+          color: '#036EB8'
+        },
+        itemStyle: {
+          color: '#036EB8',
+          borderColor: '#fff',
+          borderWidth: 2,
+          shadowColor: 'rgba(3, 110, 184, 0.4)',
+          shadowBlur: 10
+        },
+        areaStyle: null,
+      },
+      // 白色圆形背景
+      {
+        type: 'pie',
+        radius: ['48%'], // 半径比主环形图小一点或相同
+        center: ['50%', '50%'],
+        data: [{ value: 1, name: 'background' }],
+        silent: true, // 不响应鼠标事件
+        label: { show: false }, // 不显示文字
+        itemStyle: { color: '#fff' }, // 白色
+        z: 8 // 放在折线下方,但环形上方
+      },
+      // 中心环形图
+      {
+        type: 'pie',
+        radius: ['35%', '45%'],
+        center: ['50%', '50%'],
+        data: [{ value: 100, name: props.centerText }],
+        label: {
+          show: true,
+          position: 'center',
+          formatter: props.centerText,
+          fontSize: 16,
+          fontWeight: 'bold'
+        },
+        itemStyle: {
+          color: '#7ECEF4'
+        },
+        z: 10
+        // tooltip: { show: false }
+      }
+    ]
+  };
+
+  chartInstance.setOption(option);
+};
+
+// 自动 resize
+const resizeHandler = () => {
+  chartInstance && chartInstance.resize();
+};
+
+onMounted(() => {
+  initChart();
+  window.addEventListener('resize', resizeHandler);
+});
+
+onBeforeUnmount(() => {
+  window.removeEventListener('resize', resizeHandler);
+  chartInstance && chartInstance.dispose();
+});
+
+// 监听数据变化,实时更新
+watch(
+  () => [props.dates, props.natural, props.paid, props.centerText],
+  () => {
+    initChart();
+  },
+  { deep: true }
+);
+</script>
+
+<style lang="scss" scoped>
+.chart {
+  width: 100%;
+  height: 100%;
+  position: relative;
+  // left: 30px;
+
+  :deep(canvas) {
+    // left: 30px !important;
+  }
+}
+</style>

+ 134 - 0
xinkeaboard-promotion-portal/src/components/competitor/item.vue

@@ -0,0 +1,134 @@
+<template>
+  <div class="competitor-item">
+    <div class="competitor-item-head">
+      <span class="competitor-item-head__label">竞品网站:</span>
+      <span class="competitor-item-head__value">{{ siteName }}</span>
+    </div>
+    <div class="empty-wrap" v-if="loading">
+      <Empty :autoFinish="autoFinish" :fail="fail"></Empty>
+    </div>
+    <div class="competitor-item-show" v-else>
+      <div class="competitor-item-show__chart">
+        <TrafficChart
+          :dates="trafficBOData.x_axis"
+          :natural="trafficBOData.organic"
+          :paid="trafficBOData.paids"
+          centerText="自然流量"
+        />
+      </div>
+      <div class="competitor-item-show__line"></div>
+      <div class="competitor-item-show__sort">
+        <RankTable></RankTable>
+      </div>
+      <div class="competitor-item-suggestion">
+        <RecommendTable></RecommendTable>
+      </div>
+      <div class="competitor-item-show__bottom">立即下载报告,查看更多</div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { useMainStore } from '@/store';
+import { reactive, ref } from 'vue';
+import TrafficChart from './TrafficChart.vue';
+import Empty from '@/components/CommonEmpty.vue';
+import RankTable from '@/components/competitor/RankTable.vue';
+import RecommendTable from '@/components/competitor/RecommendTable.vue';
+
+import type { TrafficBO, TrafficBOItem } from '@/types';
+
+const props = defineProps<{
+  siteName: string;
+}>();
+
+const trafficBOData = reactive<TrafficBO>({} as TrafficBO);
+const loading = ref<boolean>(false);
+const autoFinish = ref<boolean>(false);
+const fail = ref<boolean>(false);
+
+const mainStore = useMainStore();
+
+const getRivalData = () => {
+  loading.value = true;
+  return mainStore
+    .getRival(props.siteName)
+    .then((res) => {
+      const trafficBOList: TrafficBOItem[] = res.competitorBOS.trafficBOList;
+      trafficBOList.forEach((item) => {
+        trafficBOData.x_axis.push(item.x_axis);
+        trafficBOData.organic.push(item.organic);
+        trafficBOData.paids.push(item.paid);
+      });
+      autoFinish.value = true;
+      setTimeout(() => {
+        loading.value = false;
+      }, 300);
+    })
+    .catch(() => {
+      fail.value = true;
+    });
+};
+
+// getRivalData();
+</script>
+
+<style lang="scss" scoped>
+.competitor-item {
+  margin-bottom: 40px;
+  &-head {
+    display: flex;
+    align-items: center;
+    width: 100%;
+    height: 110px;
+    font-weight: 400;
+    font-size: 24px;
+    font-weight: bold;
+    color: var(--promotion--color-primary);
+    padding-left: 80px;
+    box-sizing: border-box;
+    background: linear-gradient(90deg, rgba(3, 110, 184, 0.1) 0%, rgba(126, 206, 244, 0.1) 100%);
+  }
+
+  &-show {
+    width: 100%;
+    background-color: #fff;
+    padding: 40px;
+    box-sizing: border-box;
+
+    &__chart {
+      display: flex;
+      justify-content: center;
+      align-items: center;
+      width: 1256px;
+      height: 548px;
+      background-color: #fff;
+    }
+
+    &__line {
+      width: 100%;
+      height: 1px;
+      background: #dde1e6;
+      margin: 20px 0;
+    }
+
+    &__bottom {
+      display: flex;
+      justify-content: center;
+      align-items: center;
+      width: 100%;
+      height: 66px;
+      margin-top: 20px;
+      font-weight: bold;
+      font-size: 20px;
+      color: var(--promotion--color-primary);
+      cursor: pointer;
+    }
+  }
+
+  .empty-wrap {
+    width: 100%;
+    height: 376px;
+  }
+}
+</style>

+ 21 - 0
xinkeaboard-promotion-portal/src/components/competitor/list.vue

@@ -0,0 +1,21 @@
+<template>
+  <div class="competitor-list">
+    <CompetitorItem v-for="(item, index) in competitorWebsiteList" :key="index" :siteName="item" />
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { computed } from 'vue';
+import { useMainStore } from '@/store';
+import CompetitorItem from "./item.vue";
+
+const mainStore = useMainStore();
+const formData = computed(() => mainStore.getFormData);
+const competitorWebsiteList = computed(() => formData.value.competitorWebsite?.split(',') || []);
+</script>
+
+<style lang="scss" scoped>
+.competitor-list {
+  
+}
+</style>

+ 1 - 1
xinkeaboard-promotion-portal/src/components/keyword/search.vue

@@ -34,7 +34,7 @@ const month = computed(() => data.value.monthlySearchesBO?.month);
 const competition = computed(() => data.value.competition ?? 0);
 
 const searchVolume = computed(() => {
-  const search_volume = data.value.monthlySearchesBO.search_volume ?? 0;
+  const search_volume = data.value.monthlySearchesBO?.search_volume ?? 0;
 
   return search_volume.toLocaleString();
 });

+ 7 - 1
xinkeaboard-promotion-portal/src/components/keyword/table.vue

@@ -1,6 +1,11 @@
 <template>
   <div class="keyword-table">
-    <Empty :autoFinish="autoFinish" v-if="loading"></Empty>
+    <Empty
+      :autoFinish="autoFinish"
+      :fail="fail"
+      :text="fail ? '生成失败' : 'AI数据生成中...'"
+      v-if="loading"
+    ></Empty>
     <el-table :data="tableData" v-else>
       <el-table-column prop="keyword" align="right">
         <template #header>
@@ -35,6 +40,7 @@ const mainStore = useMainStore();
 const tableData = computed(() => mainStore.suggestionsInfo.data);
 const loading = computed(() => mainStore.suggestionsInfo.loading);
 const autoFinish = computed(() => mainStore.suggestionsInfo.autoFinish);
+const fail = computed(() => mainStore.suggestionsInfo.fail);
 </script>
 
 <style lang="scss" scoped>

+ 9 - 6
xinkeaboard-promotion-portal/src/store/index.ts

@@ -1,7 +1,7 @@
 import { defineStore } from 'pinia';
 import { safeJsonParse } from '../utils/common';
-import { analysisKeyword, analysisSuggestions } from '../utils/api';
-import type { FormDataInfo, KeywordInfo, RelatedInfoBOItem } from '@/types';
+import { analysisKeyword, analysisSuggestions, analysisRival } from '../utils/api';
+import type { FormDataInfo, KeywordInfo, RelatedInfoBOItem, CompetitorBOSItem } from '@/types';
 
 export const useMainStore = defineStore('main', {
   state: () => ({
@@ -14,7 +14,8 @@ export const useMainStore = defineStore('main', {
     suggestionsInfo: {
       loading: true,
       autoFinish: false,
-      data: [] as RelatedInfoBOItem[]
+      data: [] as RelatedInfoBOItem[],
+      fail: false
     }
   }),
   actions: {
@@ -28,7 +29,10 @@ export const useMainStore = defineStore('main', {
     // 获取定性分析
     getQualitative() {},
     // 获取竞品
-    getRival() {},
+    getRival(website: string): Promise<any> {
+      const { locationName } = this.getFormData;
+      return analysisRival({ competitorWebsite: website, locationName });
+    },
     // 获取推荐
     async getSuggestions() {
       const { productName, locationName } = this.getFormData;
@@ -40,7 +44,7 @@ export const useMainStore = defineStore('main', {
             this.suggestionsInfo.loading = false;
           }, 300);
         })
-        .finally(() => {});
+        .catch(() => this.suggestionsInfo.fail = true);
     },
     // 获取关键词
     async getKeywordData() {
@@ -56,7 +60,6 @@ export const useMainStore = defineStore('main', {
     initData() {
       this.getKeywordData();
       this.getSuggestions();
-      // this.getRival();
       // this.getQualitative();
     }
   },

+ 35 - 0
xinkeaboard-promotion-portal/src/types/index.ts

@@ -34,3 +34,38 @@ export interface RelatedInfoBOItem {
   competition: number;
   keyword: string;
 }
+
+export interface TrafficBOItem {
+  x_axis: number;
+  paid: number;
+  organic: number;
+}
+
+interface RecommendationBOItem {
+  searchVolume: number;
+  keywords: string;
+  price: number;
+}
+
+interface RankBOItem {
+  searchVolume: number;
+  cpc: number;
+  monthly: number;
+  keyword: string;
+  quarterly: number;
+  dp: number;
+  yearly: number;
+}
+
+export interface CompetitorBOSItem {
+  website: string;
+  trafficBOList: TrafficBOItem[];
+  recommendationBOList: RecommendationBOItem[];
+  rankBOList: RankBOItem[];
+}
+
+export interface TrafficBO {
+  x_axis: number[];
+  paids: number[];
+  organic: number[];
+}

+ 1 - 1
xinkeaboard-promotion-portal/src/utils/http.ts

@@ -35,7 +35,7 @@ http.interceptors.response.use(
     if (res.state !== 200) {
       showMessage({
         type: 'error',
-        message: response.msg || '请求出错'
+        message: response.msg || '请求失败'
       });
     }
     return response.data; // 正常返回数据

+ 28 - 2
xinkeaboard-promotion-portal/src/views/Record.vue

@@ -24,6 +24,12 @@
             </div>
           </div>
         </div>
+        <div class="record-wrap-content__competitor">
+          <img :src="CompetitorPng" />
+          <div class="list">
+            <CompetitorList></CompetitorList>
+          </div>
+        </div>
       </div>
     </div>
   </div>
@@ -34,7 +40,9 @@ import { onMounted, computed } from 'vue';
 import { useMainStore } from '../store';
 import KeywordSearch from '../components/keyword/search.vue';
 import KeywordTable from '../components/keyword/table.vue';
+import CompetitorList from '../components/competitor/list.vue';
 import keywordPng from '../assets/images/keyword.png';
+import CompetitorPng from '../assets/images/competitor.png';
 
 const mainStore = useMainStore();
 mainStore.initData();
@@ -43,7 +51,6 @@ const keywordData = computed(() => mainStore.getKeywordInfo);
 const loading = computed(() => keywordData.value.loading);
 const keywordEn = computed(() => keywordData.value.data?.keywordEn);
 const keywords = computed(() => keywordData.value?.data?.keywords?.join(','));
-
 </script>
 
 <style lang="scss" scoped>
@@ -51,7 +58,7 @@ const keywords = computed(() => keywordData.value?.data?.keywords?.join(','));
   position: relative;
   width: 1920px;
   height: 100%;
-  overflow: auto;
+  // overflow: auto;
   background-color: #fff;
 
   &-head {
@@ -68,6 +75,7 @@ const keywords = computed(() => keywordData.value?.data?.keywords?.join(','));
     width: 100%;
     display: flex;
     justify-content: center;
+    background-color: rgba(246, 248, 250, 0.5);
 
     &-content {
       position: relative;
@@ -163,6 +171,24 @@ const keywords = computed(() => keywordData.value?.data?.keywords?.join(','));
           }
         }
       }
+
+      &__competitor {
+        display: flex;
+        flex-direction: column;
+        justify-content: center;
+        align-items: center;
+        margin-top: 65px;
+
+        img {
+          width: 624px;
+          height: 166px;
+        }
+
+        .list {
+          width: 100%;
+          margin-top: 20px;
+        }
+      }
     }
   }
 }