Browse Source

fix: 前端增加获取报告次数限制

周玉环 2 ngày trước cách đây
mục cha
commit
d5f40639d5

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

@@ -47,7 +47,9 @@ const getQualitativeInfo = () => {
     })
     .catch((err) => {
       fail.value = true;
-    });
+    }).finally(() => {
+      mainStore.setIsLoadOver(true);
+    })
 };
 
 getQualitativeInfo();

+ 13 - 2
xinkeaboard-promotion-portal/src/components/TopContent.vue

@@ -34,6 +34,8 @@
 <script lang="ts" setup>
 import { ref, computed } from 'vue';
 import { useMainStore } from '../store';
+import { checkLimit, incrementCount } from '@/utils/limit';
+
 import { useRouter } from 'vue-router';
 import CountrySelct from '@/components/CountrySelct.vue';
 import ProductDescription from '@/components/ProductDescription.vue';
@@ -106,7 +108,16 @@ const getFormData = () => {
   };
 };
 
-const acceptRecod = () => {
+const acceptRecod = async () => {
+  const { allowed, remaining } = await checkLimit();
+  if (!allowed) {
+    showMessage({
+      type: 'warning',
+      message: `本月次数已用完`
+    });
+    return;
+  }
+  await incrementCount();
   const formData = getFormData();
   const { competitorWebsite } = formData;
   if (!competitorWebsite) {
@@ -115,7 +126,7 @@ const acceptRecod = () => {
       message: '请输入竞品网站'
     });
   }
-  mainStore.setFormData(formData)
+  mainStore.setFormData(formData);
   router.push('/record');
 };
 </script>

+ 15 - 12
xinkeaboard-promotion-portal/src/components/competitor/RankTable.vue

@@ -5,20 +5,16 @@
       <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
-          }}
+          {{ parseNumber(scope.row.searchVolume) }}
         </template>
       </el-table-column>
-      <el-table-column prop="searchVolume" align="center" label="SEO点击次数">
+      <el-table-column prop="monthly" align="center" label="SEO点击次数(变化)">
         <template #default="scope">
-          {{
-            scope.row.searchVolume >= 1000
-              ? (scope.row.searchVolume / 1000).toFixed(1) + 'K'
-              : scope.row.searchVolume
-          }}
+          <span v-if="scope.row.monthly > 0" style="color: #036eb8">{{
+            '+ ' + parseNumber(scope.row.monthly)
+          }}</span>
+          <span v-else style="color: red">{{ '- ' + parseNumber(scope.row.monthly) }}</span>
+          {{ scope }}
         </template>
       </el-table-column>
       <el-table-column prop="dp" align="center" label="DP(关键词难度)"> </el-table-column>
@@ -27,7 +23,7 @@
           {{ '$ ' + scope.row.cpc }}
         </template>
       </el-table-column>
-      <el-table-column prop="searchVolume" align="center" label="桌面搜索"> </el-table-column>
+      <!-- <el-table-column prop="searchVolume" align="center" label="桌面搜索"> </el-table-column> -->
     </el-table>
   </div>
 </template>
@@ -44,6 +40,13 @@ const mainStore = useMainStore();
 
 const expanded = computed(() => mainStore.getExpanded);
 const tableData = expanded.value ? props.tableData : props.tableData.slice(0, 10);
+
+const parseNumber = (num: number) => {
+  if (num >= 1000) {
+    return (num / 1000).toFixed(1) + 'K';
+  }
+  return num;
+};
 // const tableData = ref([
 //   {
 //     searchVolume: 5400,

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

@@ -22,9 +22,13 @@ export const useMainStore = defineStore('main', {
       data: [] as RelatedInfoBOItem[],
       fail: false
     },
-    expanded: false
+    expanded: false,
+    isLoadOver: false
   }),
   actions: {
+    setIsLoadOver(val: boolean) {
+      this.isLoadOver = val;
+    },
     setExpanded(val: boolean) {
       this.expanded = val;
     },
@@ -97,6 +101,9 @@ export const useMainStore = defineStore('main', {
       loading: boolean;
     } {
       return this.suggestionsInfo;
+    },
+    getIsLoadOver(): boolean {
+      return this.isLoadOver;
     }
   }
 });

+ 42 - 0
xinkeaboard-promotion-portal/src/utils/db.ts

@@ -0,0 +1,42 @@
+// db.ts
+const DB_NAME = 'report-db';
+const STORE_NAME = 'report-store';
+const DB_VERSION = 1;
+
+export async function openDB(): Promise<IDBDatabase> {
+  return new Promise((resolve, reject) => {
+    const request = indexedDB.open(DB_NAME, DB_VERSION);
+
+    request.onupgradeneeded = (event) => {
+      const db = (event.target as IDBOpenDBRequest).result;
+      if (!db.objectStoreNames.contains(STORE_NAME)) {
+        db.createObjectStore(STORE_NAME, { keyPath: 'month' });
+      }
+    };
+
+    request.onsuccess = () => resolve(request.result);
+    request.onerror = () => reject(request.error);
+  });
+}
+
+export async function getMonthRecord(month: string): Promise<{ count: number } | undefined> {
+  const db = await openDB();
+  return new Promise((resolve, reject) => {
+    const tx = db.transaction(STORE_NAME, 'readonly');
+    const store = tx.objectStore(STORE_NAME);
+    const request = store.get(month);
+    request.onsuccess = () => resolve(request.result);
+    request.onerror = () => reject(request.error);
+  });
+}
+
+export async function setMonthRecord(month: string, count: number) {
+  const db = await openDB();
+  return new Promise<void>((resolve, reject) => {
+    const tx = db.transaction(STORE_NAME, 'readwrite');
+    const store = tx.objectStore(STORE_NAME);
+    store.put({ month, count });
+    tx.oncomplete = () => resolve();
+    tx.onerror = () => reject(tx.error);
+  });
+}

+ 21 - 0
xinkeaboard-promotion-portal/src/utils/limit.ts

@@ -0,0 +1,21 @@
+import { getMonthRecord, setMonthRecord } from './db';
+
+const MAX_COUNT = 3;
+const SALT = 'my_secret_salt'; // 防篡改用,可选
+
+export async function checkLimit(): Promise<{ allowed: boolean; remaining: number }> {
+  const month = new Date().toISOString().slice(0, 7); // YYYY-MM
+  const record = await getMonthRecord(month);
+  const count = record?.count || 0;
+  return {
+    allowed: count < MAX_COUNT,
+    remaining: MAX_COUNT - count,
+  };
+}
+
+export async function incrementCount(): Promise<void> {
+  const month = new Date().toISOString().slice(0, 7);
+  const record = await getMonthRecord(month);
+  const count = record?.count || 0;
+  await setMonthRecord(month, count + 1);
+}

+ 22 - 4
xinkeaboard-promotion-portal/src/utils/pdf.ts

@@ -1,5 +1,6 @@
-import { nextTick, computed } from 'vue';
+import { nextTick, computed, ref } from 'vue';
 import { useMainStore } from '@/store';
+import { ElLoading } from 'element-plus';
 import html2canvas from 'html2canvas';
 import jsPDF from 'jspdf';
 import { showMessage } from './common';
@@ -7,8 +8,21 @@ import { showMessage } from './common';
 const mainStore = useMainStore();
 
 const expanded = computed(() => mainStore.getExpanded);
+const isLoadOver = computed(() => mainStore.getIsLoadOver);
 
-export const downloadPDF = async (pdfContent: HTMLElement, callback: () => void) => {
+export const downloadPDF = async (pdfContent: HTMLElement) => {
+  if (!isLoadOver.value) {
+    showMessage({
+      type: 'warning',
+      message: '数据加载中,请稍后再试'
+    });
+    return;
+  }
+  const loading = ElLoading.service({
+    lock: true,
+    text: '下载中...',
+    background: 'rgba(0, 0, 0, 0.7)'
+  });
   if (!expanded.value) {
     mainStore.setExpanded(true);
     await nextTick();
@@ -22,7 +36,9 @@ export const downloadPDF = async (pdfContent: HTMLElement, callback: () => void)
       backgroundColor: '#fff' // 防止透明背景
     });
   } catch (error) {
-    callback && callback();
+    setTimeout(() => {
+      loading.close();
+    }, 1000);
     mainStore.setExpanded(false);
     showMessage({
       type: 'error',
@@ -47,5 +63,7 @@ export const downloadPDF = async (pdfContent: HTMLElement, callback: () => void)
   pdf.addImage(imgData, 'JPEG', x, y, imgWidth, imgHeight);
   mainStore.setExpanded(false);
   pdf.save('charts-and-table.pdf');
-  callback && callback();
+  setTimeout(() => {
+    loading.close();
+  }, 1000);
 };

+ 1 - 9
xinkeaboard-promotion-portal/src/views/Record.vue

@@ -47,7 +47,6 @@
 
 <script lang="ts" setup>
 import { ref, computed } from 'vue';
-import { ElLoading } from 'element-plus';
 
 import { useMainStore } from '../store';
 import KeywordSearch from '../components/keyword/search.vue';
@@ -71,14 +70,7 @@ const keywords = computed(() => keywordData.value?.data?.keywords?.join(','));
 const expanded = computed(() => mainStore.getExpanded);
 
 const download = () => {
-  const loading = ElLoading.service({
-    lock: true,
-    text: '下载中...',
-    background: 'rgba(0, 0, 0, 0.7)'
-  });
-  downloadPDF(pdfContent.value!, () => {
-    loading.close();
-  });
+  downloadPDF(pdfContent.value!);
 };
 </script>