Răsfoiți Sursa

fix: 优化获取报告交互

周玉环 5 zile în urmă
părinte
comite
50ebf24954

+ 6 - 2
xinkeaboard-promotion-portal/src/App.vue

@@ -1,16 +1,20 @@
 <template>
   <router-view v-slot="{ Component }">
-    <keep-alive include="Home">
+    <keep-alive :include="cacheViews">
       <component :is="Component" />
     </keep-alive>
   </router-view>
 </template>
 
 <script setup lang="ts">
-import { onMounted, onBeforeUnmount, watch, computed } from 'vue';
+import { onMounted, onBeforeUnmount, computed } from 'vue';
 import { useRouter } from 'vue-router';
+import { useMainStore } from './store';
 
 const router = useRouter();
+const mainStore = useMainStore();
+
+const cacheViews = computed(() => mainStore.getCacheViews);
 
 const handleLinkClick = (e: MouseEvent) => {
   const target = (e.target as HTMLElement).closest('a') as HTMLAnchorElement | null;

+ 4 - 5
xinkeaboard-promotion-portal/src/components/TopContent.vue

@@ -115,19 +115,21 @@ const getFormData = () => {
 };
 
 const isValidUrl = (url: string) => {
-  const pattern = /^(https?:\/\/|ftp:\/\/)([a-zA-Z0-9.-]+)(:[0-9]+)?(\/[^\s]*)?$/;
+  const pattern = /^(https?:\/\/)([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}(\/(?!.*https?:\/\/)[^\s]*)?$/;
   return pattern.test(url);
 };
 
 const validateCompetitorWebsite = (site: string) => {
   if (!site.trim()) return true;
-  const list = site.split(',');
+  // 替换中文逗号为英文逗号,再分割
+  const list = site.replace(/,/g, ",").split(",");
   return list.every((item) => isValidUrl(item));
 };
 
 const dialogConfirmCallback = () => {
   const formData = getFormData();
   btnLoading.value = false;
+  mainStore.setCacheViews(['Home'])
   mainStore.setFormData(formData);
 };
 
@@ -135,7 +137,6 @@ const acceptRecod = async () => {
   const formData = getFormData();
   const { competitorWebsite } = formData;
   const result = validateCompetitorWebsite(competitorWebsite);
-
   if (!result) {
     showMessage({
       type: 'error',
@@ -146,8 +147,6 @@ const acceptRecod = async () => {
   }
   visible.value = true;
   return;
-
-  // router.push('/record');
 };
 </script>
 <style lang="scss" scoped>

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

@@ -35,7 +35,7 @@ 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 { downloadPDF } from '@/utils/pdf';
+import { handlePDF } from '@/utils/pdf';
 
 
 import type { TrafficBO, TrafficBOItem } from '@/types';
@@ -57,7 +57,7 @@ const fail = ref<boolean>(false);
 
 const mainStore = useMainStore();
 const download = () => {
-  downloadPDF();
+  handlePDF(undefined, 'manual');
 };
 
 const expanded = computed(() => mainStore.getExpanded);

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

@@ -11,7 +11,7 @@ import CompetitorItem from "./item.vue";
 
 const mainStore = useMainStore();
 const formData = computed(() => mainStore.getFormData);
-const competitorWebsiteList = computed(() => formData.value.competitorWebsite?.split(',') || []);
+const competitorWebsiteList = computed(() => formData.value.competitorWebsite?.replace(/,/g, ",").split(",") || []);
 </script>
 
 <style lang="scss" scoped>

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

@@ -33,9 +33,13 @@ export const useMainStore = defineStore('main', {
     isLoadOver: false,
     isShowCompetitor: false,
     phone: '',
-    id: ''
+    id: '',
+    cacheViews: ['Home', 'Record'],
   }),
   actions: {
+    setCacheViews(val: string[]) {
+      this.cacheViews = val;
+    },
     setPhone(val: string) {
       this.phone = val;
     },
@@ -61,6 +65,7 @@ export const useMainStore = defineStore('main', {
     // 获取定性分析
     async getQualitative() {
       this.aiAnalysisData.loading = true;
+      this.aiAnalysisData.fail = false;
       const { productName, description = '' } = this.getFormData;
       return analysisQualitative(encodeURIComponent(productName + description)).then((res: any) => {
         const rawText = res.msg.replace(/^```[a-zA-Z]*\n?/, '').replace(/```$/, '');
@@ -109,6 +114,7 @@ export const useMainStore = defineStore('main', {
         });
     },
     initData() {
+      this.isLoadOver = false;
       this.getKeywordData();
       this.getSuggestions();
       // 判断是否输入合法的竞品网站地址
@@ -121,6 +127,9 @@ export const useMainStore = defineStore('main', {
     }
   },
   getters: {
+    getCacheViews(): string[] {
+      return this.cacheViews;
+    },
     getPhone(): string {
       return this.phone;
     },

+ 49 - 31
xinkeaboard-promotion-portal/src/utils/pdf.ts

@@ -11,23 +11,28 @@ const mainStore = useMainStore();
 const expanded = computed(() => mainStore.getExpanded);
 const phone = computed(() => mainStore.getPhone);
 const recordId = computed(() => mainStore.getRecordId);
-
 const isLoadOver = computed(() => mainStore.getIsLoadOver);
 
 /**
  * 生成 PDF Blob
  */
-async function generatePDFBlob(pdfContent?: HTMLElement, isDownload?: boolean): Promise<Blob | null> {
+async function generatePDFBlob(
+  pdfContent?: HTMLElement,
+  showLoading = false
+): Promise<Blob | null> {
   if (!isLoadOver.value) {
     showMessage({ type: 'warning', message: '数据加载中,请稍后再试' });
     return null;
   }
+  let loading: any = null;
 
-  const loading = ElLoading.service({
+  if (showLoading) {
+    loading = ElLoading.service({
       lock: true,
-      text: isDownload ? '生成报告中...' : '上传报告中...',
+      text: '生成报告中...',
       background: 'rgba(0,0,0,0.7)'
     });
+  }
 
   try {
     if (!pdfContent) pdfContent = document.querySelector('.record') as HTMLElement;
@@ -42,6 +47,7 @@ async function generatePDFBlob(pdfContent?: HTMLElement, isDownload?: boolean):
       useCORS: true,
       backgroundColor: '#fff'
     });
+
     const canvasWidth = canvas.width;
     const canvasHeight = canvas.height;
 
@@ -69,54 +75,66 @@ async function generatePDFBlob(pdfContent?: HTMLElement, isDownload?: boolean):
       if (positionY < canvasHeight) pdf.addPage();
     }
 
-    const blob = pdf.output('blob'); // ✅ 生成 Blob
-    return blob;
+    return pdf.output('blob');
   } catch (error) {
     console.error(error);
     showMessage({ type: 'error', message: '生成失败,请稍后再试' });
     return null;
   } finally {
     mainStore.setExpanded(false);
-    setTimeout(() => loading.close(), 500);
+    setTimeout(() => loading && loading.close(), 500);
   }
 }
 
 /**
- * 点击下载 PDF
+ * 下载文件
  */
-export async function downloadPDF(pdfContent?: HTMLElement) {
-  const blob = await generatePDFBlob(pdfContent, true);
-  if (!blob) return;
-
+function triggerDownload(blob: Blob, filename = 'analysis.pdf') {
   const url = URL.createObjectURL(blob);
   const a = document.createElement('a');
   a.href = url;
-  a.download = 'analysis.pdf';
+  a.download = filename;
   a.click();
   URL.revokeObjectURL(url);
 }
 
 /**
- * 上传 PDF
+ * 上传文件
  */
-export async function uploadPDF(pdfContent?: HTMLElement) {
-  const blob = await generatePDFBlob(pdfContent, false);
-  if (!blob) return;
-
-  // 转成 File 对象(后端一般习惯接收 File)
+async function uploadPDFFile(blob: Blob) {
   const file = new File([blob], 'analysis.pdf', { type: 'application/pdf' });
+  const uploadRes = await uploadFile(file, { source: 'media', mediaType: 'file' });
+  const pdfUrl = uploadRes.data.url;
 
-  try {
-    const uploadRes = await uploadFile(file, { source: 'media', mediaType: 'file' });
-    const pdfUrl = uploadRes.data.url;
-    await savePdf({
-      phone: phone.value,
-      id: recordId.value,
-      pdfUrl
-    });
-    showMessage({ type: 'success', message: '报告上传成功' });
-  } catch (err) {
-    showMessage({ type: 'error', message: '报告上传失败' });
-    throw err;
+  await savePdf({
+    phone: phone.value,
+    id: recordId.value,
+    pdfUrl
+  });
+
+  return pdfUrl;
+}
+
+/**
+ * 统一入口:生成 PDF
+ * @param pdfContent - PDF内容DOM
+ * @param mode - "manual" 手动下载 | "auto" 自动下载并上传
+ */
+export async function handlePDF(pdfContent?: HTMLElement, mode: 'manual' | 'auto' = 'manual') {
+  const blob = await generatePDFBlob(pdfContent, true);
+  if (!blob) return;
+
+  // 下载
+  triggerDownload(blob);
+
+  // 自动模式下再上传
+  if (mode === 'auto') {
+    try {
+      await uploadPDFFile(blob);
+      // showMessage({ type: 'success', message: '报告已下载并上传成功' });
+    } catch (err) {
+      // showMessage({ type: 'error', message: '报告上传失败' });
+      throw err;
+    }
   }
 }

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

@@ -74,7 +74,7 @@ import CompetitorPng from '../assets/images/competitor.png';
 import AiAnalysisPng from '../assets/images/ai-analysis.png';
 import RecordTop from '@/assets/images/record-top.png';
 import Logo from "@/assets/images/logo.png";
-import { downloadPDF, uploadPDF } from '@/utils/pdf';
+import { handlePDF } from '@/utils/pdf';
 
 const mainStore = useMainStore();
 mainStore.initData();
@@ -89,16 +89,17 @@ const isShowCompetitor = computed(() => mainStore.getIsShowCompetitor);
 const isLoadOver = computed(() => mainStore.getIsLoadOver);
 
 const download = () => {
-  downloadPDF();
+  handlePDF(undefined, 'manual');
 };
 
 onMounted(() => {
-  // mainStore.setCurrentStep(1);
+  mainStore.setCacheViews(['Home', 'Record'])
+  mainStore.setCurrentStep(1);
 });
 
 watch(isLoadOver, (val) => {
   if (val) {
-    setTimeout(() => uploadPDF(), 800)
+    setTimeout(() => handlePDF(undefined, 'auto'), 800)
   }
 });
 </script>