Browse Source

feat: 招商门户页面开发

周玉环 1 day ago
parent
commit
f15d288f40

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

@@ -8,20 +8,31 @@ export {}
 /* prettier-ignore */
 declare module 'vue' {
   export interface GlobalComponents {
+    ColorLevel: typeof import('./src/components/ColorLevel.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']
+    DifficultyBar: typeof import('./src/components/DifficultyBar.vue')['default']
     ElButton: typeof import('element-plus/es')['ElButton']
     ElDropdown: typeof import('element-plus/es')['ElDropdown']
     ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
     ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
     ElIcon: typeof import('element-plus/es')['ElIcon']
     ElInput: typeof import('element-plus/es')['ElInput']
+    ElTable: typeof import('element-plus/es')['ElTable']
+    ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
     Home: typeof import('./src/components/Home.vue')['default']
+    KeywordSearch: typeof import('./src/components/KeywordSearch.vue')['default']
+    Level: typeof import('./src/components/Level.vue')['default']
     ProductDescription: typeof import('./src/components/ProductDescription.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']
   }
+  export interface GlobalDirectives {
+    vLoading: typeof import('element-plus/es')['ElLoadingDirective']
+  }
 }

+ 0 - 1
xinkeaboard-promotion-portal/postcss.config.cjs

@@ -10,7 +10,6 @@ module.exports = {
       selectorBlackList: [], 
       minPixelValue: 1,
       mediaQuery: true,
-      exclude: [/node_modules/],
     })
   ]
 }

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


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


+ 77 - 0
xinkeaboard-promotion-portal/src/components/DifficultyBar.vue

@@ -0,0 +1,77 @@
+<template>
+  <div class="difficulty-bar">
+    <div v-for="(color, index) in colors" :key="index" class="bar-segment">
+      <!-- 背景灰色 -->
+      <div class="bar-bg"></div>
+      <!-- 前景彩色,宽度根据 value 决定 -->
+      <div
+        class="bar-fill"
+        :style="{
+          backgroundColor: color,
+          width: getFillWidth(index) + '%'
+        }"
+      ></div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+const props = defineProps({
+  value: {
+    type: Number,
+    required: true // 0 - 100
+  }
+});
+
+// 每段占比(5 段 → 每段 20%)
+const segmentPercent = 20;
+
+// 彩色配置
+const colors = [
+  '#00B7D4', // 蓝
+  '#6CD12A', // 绿
+  '#FFE53B', // 黄
+  '#FF9F1C', // 橙
+  '#FF3C1C' // 红
+];
+
+// 计算某一段的填充宽度(0–100%)
+const getFillWidth = (index) => {
+  const start = index * segmentPercent;
+  const end = (index + 1) * segmentPercent;
+  if (props.value >= end) return 100;
+  if (props.value <= start) return 0;
+  return ((props.value - start) / segmentPercent) * 100;
+};
+</script>
+
+<style scoped>
+.difficulty-bar {
+  display: flex;
+  height: 10px;
+  overflow: hidden;
+}
+
+.bar-segment {
+  width: 108px;
+  margin-right: 2px;
+  position: relative;
+
+  &:last-child {
+    margin-right: 0;
+  }
+}
+
+.bar-bg {
+  background-color: #dde1e6; /* 默认灰色 */
+  height: 100%;
+  width: 100%;
+}
+
+.bar-fill {
+  position: absolute;
+  top: 0;
+  left: 0;
+  bottom: 0;
+}
+</style>

+ 3 - 1
xinkeaboard-promotion-portal/src/components/TopContent.vue

@@ -112,13 +112,15 @@ const getFormData = () => {
 };
 
 const acceptRecod = () => {
-  const { locationName, productName, description, competitorWebsite } = getFormData();
+  const formData = getFormData();
+  const { competitorWebsite } = formData;
   if (!competitorWebsite) {
     return showMessage({
       type: 'error',
       message: '请输入竞品网站'
     });
   }
+  mainStore.setFormData(formData)
   router.push('/record');
 
   // console.log(locationName, productName, description, competitorWebsite, '-=-=-=-=');

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

@@ -0,0 +1,89 @@
+<template>
+  <div class="keyword-search">
+    <div class="keyword-search-title">{{ year }}年{{ month }}月核心关键词搜索量</div>
+    <div class="keyword-search-value">
+      <span v-if="loading">数据加载中...</span>
+      <span v-else>{{ searchVolume }}</span>
+    </div>
+    <div class="keyword-search-line"></div>
+    <div class="keyword-search-tip">
+      <span>关键词难度</span>
+      <img :src="Help" />
+    </div>
+    <div class="keyword-search-bar">
+      <DifficultyBar :value="competition"></DifficultyBar>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { computed } from 'vue';
+import { useMainStore } from '@/store';
+import Help from '@/assets/images/help.png';
+
+const mainStore = useMainStore();
+
+const keywordInfo = computed(() => mainStore.getKeywordInfo);
+
+const loading = computed(() => keywordInfo.value.loading);
+
+const data = computed(() => keywordInfo.value.data);
+
+const year = computed(() => data.value.monthlySearchesBO.year);
+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;
+
+  return search_volume.toLocaleString();
+});
+</script>
+
+<style lang="scss" scoped>
+.keyword-search {
+  width: 100%;
+  height: 100%;
+
+  &-title {
+    font-weight: bold;
+    font-size: 20px;
+    color: var(--promotion--color-primary);
+  }
+
+  &-value {
+    margin-top: 20px;
+    font-weight: bold;
+    font-size: 46px;
+    color: #282e30;
+  }
+
+  &-line {
+    width: 546px;
+    height: 1px;
+    margin: 36px 0;
+    background: linear-gradient(
+      90deg,
+      var(--promotion--color-primary) 0%,
+      rgba(3, 110, 184, 0) 100%
+    );
+  }
+
+  &-tip {
+    display: flex;
+    align-items: center;
+    font-weight: bold;
+    font-size: 20px;
+    color: var(--promotion--color-primary);
+
+    img {
+      width: 22px;
+      height: 22px;
+    }
+  }
+
+  &-bar {
+    margin-top: 20px;
+  }
+}
+</style>

+ 90 - 0
xinkeaboard-promotion-portal/src/components/keyword/table.vue

@@ -0,0 +1,90 @@
+<template>
+  <div class="keyword-table">
+    <el-table :data="tableData">
+      <el-table-column prop="date" align="right">
+        <template #header>
+          <img :src="Help" />
+          关键词
+        </template>
+      </el-table-column>
+      <el-table-column prop="name" align="center">
+        <template #header>
+          <img :src="Help" />
+          搜索量
+        </template>
+      </el-table-column>
+      <el-table-column prop="address" align="center">
+        <template #header>
+          <img :src="Help" />
+          关键词难度
+        </template>
+      </el-table-column>
+    </el-table>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import Help from '../../assets/images/help.png';
+const tableData = [
+  {
+    date: '2016-05-03',
+    name: 'Tom',
+    address: '189'
+  },
+  {
+    date: '2016-05-02',
+    name: 'Tom',
+    address: '189'
+  },
+  {
+    date: '2016-05-04',
+    name: 'Tom',
+    address: '189'
+  },
+  {
+    date: '2016-05-01',
+    name: 'Tom',
+    address: '189'
+  },
+  {
+    date: '2016-05-01',
+    name: 'Tom',
+    address: '189'
+  }
+];
+</script>
+
+<style lang="scss" scoped>
+.keyword-table {
+  :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>

+ 47 - 1
xinkeaboard-promotion-portal/src/store/index.ts

@@ -1,17 +1,63 @@
 import { defineStore } from 'pinia';
+import { safeJsonParse } from '../utils/common';
+import { analysisKeyword } from '../utils/api';
+import type { FormDataInfo, KeywordInfo } from '@/types';
 
 export const useMainStore = defineStore('main', {
   state: () => ({
-    currentStep: 1
+    currentStep: 1,
+    formData: {} as FormDataInfo,
+    keywordInfo: {
+      loading: true,
+      data: {} as KeywordInfo
+    }
   }),
   actions: {
     setCurrentStep(val: number) {
       this.currentStep = val;
+    },
+    setFormData(data: FormDataInfo) {
+      sessionStorage.setItem('formData', JSON.stringify(data));
+      this.formData = data;
+    },
+    // 获取定性分析
+    getQualitative() {},
+    // 获取竞品
+    getRival() {},
+    // 获取推荐
+    getSuggestions() {},
+    // 获取关键词
+    async getKeywordData() {
+      const { productName, locationName } = this.getFormData;
+      console.log(productName, 'productName')
+      return analysisKeyword({ productName, locationName })
+        .then((res) => {
+          this.keywordInfo.data = res.data;
+        })
+        .finally(() => {
+          this.keywordInfo.loading = false;
+        });
+    },
+    initData() {
+      this.getKeywordData();
+      // this.getSuggestions();
+      // this.getRival();
+      // this.getQualitative();
     }
   },
   getters: {
     getCurrentStep(): number {
       return this.currentStep;
+    },
+    getFormData(): FormDataInfo {
+      if (sessionStorage.getItem('formData')) {
+        const data = sessionStorage.getItem('formData') ?? '';
+        return safeJsonParse(data);
+      }
+      return this.formData;
+    },
+    getKeywordInfo(): { data: KeywordInfo } & { loading: boolean } {
+      return this.keywordInfo;
     }
   }
 });

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

@@ -10,3 +10,21 @@ export interface AnalysisKeywordReq {
   productName: string;
   locationName: string;
 }
+
+export interface FormDataInfo {
+  locationName: string;
+  productName: string;
+  description: string;
+  competitorWebsite: string;
+}
+
+export interface KeywordInfo {
+  keywords: string[];
+  keywordEn: string;
+  competition: number;
+  monthlySearchesBO: {
+    month: number;
+    year: number;
+    search_volume: number;
+  };
+}

+ 8 - 0
xinkeaboard-promotion-portal/src/utils/common.ts

@@ -17,3 +17,11 @@ export const showMessage = (params: {
   }
   msgBoxInstance = ElMessage(params as any);
 };
+
+export const safeJsonParse = (val: string, defaultValue?: any) => {
+  try {
+    return JSON.parse(val);
+  } catch {
+    return defaultValue ?? {};
+  }
+};

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

@@ -38,7 +38,7 @@ http.interceptors.response.use(
         message: response.msg || '请求出错'
       });
     }
-    return response; // 正常返回数据
+    return response.data; // 正常返回数据
   },
   (error) => {
     let message: string = '';

+ 115 - 5
xinkeaboard-promotion-portal/src/views/Record.vue

@@ -3,15 +3,48 @@
     <div class="record-head"></div>
     <div class="record-wrap">
       <div class="record-wrap-content">
-        <div class="record-wrap-content__overview"></div>
-        <div class="overview-left"></div>
-        <div class="overview-right"></div>
+        <div class="record-wrap-content__overview" v-loading="loading">
+          <span>概述关于你的产品的描述,通常的英语表述为</span>
+          <span class="active">{{ keywordEn }},</span>
+          <span>在互联网上搜索的关键词为</span>
+          <span class="active">{{ keywords }}</span>
+        </div>
+        <div class="overview-left">概述</div>
+        <div class="overview-right">
+          <el-button>下载报告</el-button>
+        </div>
+        <div class="record-wrap-content__keyword">
+          <img :src="keywordPng" />
+          <div class="content">
+            <div class="content-left">
+              <KeywordSearch></KeywordSearch>
+            </div>
+            <div class="content-right">
+              <KeywordTable></KeywordTable>
+            </div>
+          </div>
+        </div>
       </div>
     </div>
   </div>
 </template>
 
-<script lang="ts" setup></script>
+<script lang="ts" setup>
+import { onMounted, computed } from 'vue';
+import { useMainStore } from '../store';
+import KeywordSearch from '../components/keyword/search.vue';
+import KeywordTable from '../components/keyword/table.vue';
+import keywordPng from '../assets/images/keyword.png';
+
+const mainStore = useMainStore();
+mainStore.initData();
+
+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>
 .record {
@@ -40,18 +73,95 @@
       position: relative;
       width: 1336px;
       &__overview {
-        width: 100%;
+        display: flex;
+        // justify-content: center;
+        align-items: center;
         height: 179px;
+        padding: 0 58px;
         background: rgba(255, 255, 255, 0.65);
         box-shadow: -9px 13px 22px 0px rgba(5, 112, 183, 0.06);
         border-radius: 4px;
         margin-top: 35px;
+        font-weight: bold;
+        font-size: 20px;
+        color: #282e30;
+
+        .active {
+          color: var(--promotion--color-primary);
+        }
       }
 
       .overview-left {
+        position: absolute;
+        top: 0;
+        left: 30px;
+        width: 136px;
+        height: 70px;
+        background: #ffffff;
+        text-align: center;
+        line-height: 70px;
+        box-shadow: 0px 10px 30px 0px rgba(0, 98, 165, 0.2);
+        border-radius: 8px;
+        font-weight: bold;
+        font-size: 32px;
+        color: #282e30;
+        z-index: 2000;
       }
 
       .overview-right {
+        position: absolute;
+        top: 14px;
+        right: 30px;
+        z-index: 2000;
+
+        :deep(.el-button) {
+          width: 126px;
+          height: 48px;
+          font-weight: 400;
+          font-size: 16px;
+          color: #ffffff;
+          border-radius: 0;
+          border: none;
+          background: var(--promotion--color-primary);
+        }
+      }
+
+      &__keyword {
+        display: flex;
+        flex-direction: column;
+        justify-content: center;
+        align-items: center;
+        margin-top: 40px;
+
+        img {
+          width: 584px;
+          height: 166px;
+        }
+
+        .content {
+          width: 100%;
+
+          display: flex;
+          justify-content: space-between;
+          margin-top: 20px;
+
+          &-left {
+            width: 666px;
+            height: 317px;
+            padding: 45px 60px;
+            box-sizing: border-box;
+            background: linear-gradient(
+              90deg,
+              rgba(255, 255, 255, 0.65) 0%,
+              rgba(255, 255, 255, 0) 100%
+            );
+          }
+
+          &-right {
+            width: 650px;
+            height: 317px;
+          }
+        }
       }
     }
   }