Browse Source

fix: 商情列表以及商家列表字段调整

周玉环 3 days ago
parent
commit
383a05d374

+ 5 - 5
xinkeaboard-promotion-portal/src/App.vue

@@ -1,7 +1,7 @@
 <template>
   <router-view v-slot="{ Component }">
-    <keep-alive include="Home,Record">
-      <component :is="Component"/>
+    <keep-alive include="Home">
+      <component :is="Component" />
     </keep-alive>
   </router-view>
 </template>
@@ -13,16 +13,16 @@ import { useRouter } from 'vue-router';
 const router = useRouter();
 
 const handleLinkClick = (e: MouseEvent) => {
-  const target = (e.target as HTMLElement).closest("a") as HTMLAnchorElement | null;
+  const target = (e.target as HTMLElement).closest('a') as HTMLAnchorElement | null;
   if (target && target.tagName === 'A' && target.href) {
     e.preventDefault();
     window.open(target.href, '_blank', 'noopener,noreferrer');
   }
-}
+};
 
 onMounted(() => {
   if (location.pathname !== '/') {
-    router.push('/')
+    router.push('/');
   }
   document.addEventListener('click', handleLinkClick);
 });

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


BIN
xinkeaboard-promotion-portal/src/assets/images/record-1.png


BIN
xinkeaboard-promotion-portal/src/assets/images/record-2.png


BIN
xinkeaboard-promotion-portal/src/assets/images/record-3.png


BIN
xinkeaboard-promotion-portal/src/assets/images/record-top.png


File diff suppressed because it is too large
+ 0 - 8
xinkeaboard-promotion-portal/src/assets/images/record-top.svg


+ 36 - 3
xinkeaboard-promotion-portal/src/components/CommonEmpty.vue

@@ -1,22 +1,29 @@
 <template>
   <div class="loading-wrapper">
     <div class="loading-content">
-      <div :class="{ 'loading-text': true, fail: fail }">{{ fail ? '生成失败' : text }}</div>
+      <div :class="{ 'loading-text': true }" v-if="!fail">{{ text }}</div>
       <div class="progress-bar" v-if="!fail">
         <div
           class="progress-fill"
           :style="{
             width: progress + '%',
-            backgroundColor: fail ? '#FF3C1C' : '#036EB8'
+            backgroundColor: '#036EB8'
           }"
         ></div>
       </div>
+      <div class="loading-wrapper-error" v-if="fail">
+        <div class="error-icon">
+          <img :src="EmptyIcon" />
+        </div>
+        <div class="error-tip">暂无数据</div>
+      </div>
     </div>
   </div>
 </template>
 
 <script lang="ts" setup>
 import { ref, onMounted, onUnmounted, watch } from 'vue';
+import EmptyIcon from '@/assets/images/icon_none.png';
 
 const props = defineProps({
   text: {
@@ -85,7 +92,7 @@ watch(
 );
 </script>
 
-<style scoped>
+<style lang="scss" scoped>
 .loading-wrapper {
   width: 100%;
   height: 100%;
@@ -94,6 +101,32 @@ watch(
   justify-content: center;
 }
 
+.loading-wrapper-error {
+  width: 100%;
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  align-items: center;
+
+  .error-icon {
+    width: 216px;
+    height: 216px;
+
+    img {
+      width: 100%;
+      height: 100%;
+      object-fit: cover;
+    }
+  }
+
+  .error-tip {
+    font-size: 16px;
+    font-weight: bold;
+    color: var(--promotion--color-primary);
+  }
+}
+
 .loading-content {
   text-align: center;
 }

+ 37 - 30
xinkeaboard-promotion-portal/src/components/LoginDialog.vue

@@ -49,7 +49,9 @@
 
     <template #footer>
       <div class="dialog-footer">
-        <el-button @click="submitForm(ruleFormRef)">查看并下载报告</el-button>
+        <el-button :loading="sumbitLoading" @click="submitForm(ruleFormRef)"
+          >查看并下载报告</el-button
+        >
       </div>
     </template>
   </el-dialog>
@@ -71,8 +73,11 @@
 
 <script lang="ts" setup>
 import { ref, computed, reactive } from 'vue';
+import { useMainStore } from '@/store';
+import { useRouter } from 'vue-router';
 import SliderVerify from '@/components/SliderVerify.vue';
 import { showMessage, startCountdown } from '@/utils/common';
+import { verificationCode, getRecord } from '@/utils/api';
 import User from '@/assets/images/login/User.png';
 import Code from '@/assets/images/login/Code.png';
 import Phone from '@/assets/images/login/Phone.png';
@@ -97,8 +102,14 @@ const validatePhone = (rule: any, value: any, callback: any) => {
   }
 };
 
+const router = useRouter();
+
+const mainStore = useMainStore();
+
 const emits = defineEmits(['update:visible']);
 
+const sumbitLoading = ref<boolean>(false);
+
 const ruleFormRef = ref<FormInstance>();
 // 人机验证弹窗显示标识
 const modalVisible = ref(false);
@@ -110,7 +121,7 @@ const isReacquireCode = ref(false);
 
 const getVerifyCodeLoading = ref(false);
 
-const props = defineProps(['visible']);
+const props = defineProps(['visible', 'callback']);
 
 const visible = computed({
   get() {
@@ -156,33 +167,14 @@ const closed = () => {
 // 获取验证码
 const getVerifyCode = () => {
   getVerifyCodeLoading.value = true;
-  // 设置倒计时
-  startCountdown(60, (time) => (countDownNumer.value = time));
-  isReacquireCode.value = true;
-  //   post('v3/member/front/active/verification/code', {
-  //     email: email.value,
-  //     source: 1,
-  //     type: 1
-  //   })
-  //     .then((res) => {
-  //       if (res.state === 200) {
-  //         showMessage({
-  //           message: L['register']['验证码已发送'],
-  //           type: 'success'
-  //         });
-  //         // 设置倒计时
-  //         startCountdown(60, (time) => (countDownNumer.value = time));
-  //         isReacquireCode.value = true;
-  //       } else {
-  //         showMessage({
-  //           message: res.msg,
-  //           type: 'warning'
-  //         });
-  //       }
-  //     })
-  //     .finally(() => {
-  //       getVerifyCodeLoading.value = false;
-  //     });
+  return verificationCode(form.phone)
+    .then((res) => {
+      startCountdown(60, (time) => (countDownNumer.value = time));
+      isReacquireCode.value = true;
+    })
+    .finally(() => {
+      getVerifyCodeLoading.value = false;
+    });
 };
 
 // 人机验证成功
@@ -207,7 +199,22 @@ const submitForm = (formEl: FormInstance | undefined) => {
   if (!formEl) return;
   formEl.validate((valid) => {
     if (valid) {
-      console.log('submit!');
+      sumbitLoading.value = true;
+      return getRecord({
+        ...form
+      })
+        .then((res) => {
+          visible.value = false;
+          mainStore.setPhone(form.phone);
+          mainStore.setRecordId(res.data);
+          props.callback && props.callback();
+          router.push({
+            path: `/record`
+          });
+        })
+        .finally(() => {
+          sumbitLoading.value = false;
+        });
     } else {
       console.log('error submit!');
     }

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

@@ -30,7 +30,7 @@
         >
       </div>
     </div>
-    <LoginDialog :visible="visible" @update:visible="(val) => (visible = val)"></LoginDialog>
+    <LoginDialog :visible="visible" @update:visible="(val) => (visible = val)" :callback="dialogConfirmCallback"></LoginDialog>
   </div>
 </template>
 
@@ -125,6 +125,12 @@ const validateCompetitorWebsite = (site: string) => {
   return list.every((item) => isValidUrl(item));
 };
 
+const dialogConfirmCallback = () => {
+  const formData = getFormData();
+  btnLoading.value = false;
+  mainStore.setFormData(formData);
+};
+
 const acceptRecod = async () => {
   const formData = getFormData();
   const { competitorWebsite } = formData;
@@ -140,9 +146,6 @@ const acceptRecod = async () => {
   }
   visible.value = true;
   return;
-  btnLoading.value = true;
-
-  // mainStore.setFormData(formData);
 
   // router.push('/record');
 };

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

@@ -31,9 +31,17 @@ export const useMainStore = defineStore('main', {
     },
     expanded: false,
     isLoadOver: false,
-    isShowCompetitor: false
+    isShowCompetitor: false,
+    phone: '',
+    id: ''
   }),
   actions: {
+    setPhone(val: string) {
+      this.phone = val;
+    },
+    setRecordId(val: string) {
+      this.id = val;
+    },
     setClearCache(val: boolean) {
       this.clearCache = val;
     },
@@ -113,6 +121,12 @@ export const useMainStore = defineStore('main', {
     }
   },
   getters: {
+    getPhone(): string {
+      return this.phone;
+    },
+    getRecordId(): string {
+      return this.id;
+    },
     getClearCache(): boolean {
       return this.clearCache;
     },

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

@@ -69,3 +69,9 @@ export interface TrafficBO {
   paids: number[];
   organic: number[];
 }
+
+export interface GenerateRecord {
+  phone: string;
+  companyName: string;
+  verifyCode: string;
+}

+ 20 - 1
xinkeaboard-promotion-portal/src/utils/api.ts

@@ -18,6 +18,18 @@ interface AnalysisRival {
   locationName: string;
 }
 
+interface GenerateRecord {
+  phone: string;
+  companyName: string;
+  verifyCode: string;
+}
+
+interface SavePdf {
+  phone: string;
+  id: string;
+  pdfUrl: string;
+}
+
 export const analysisKeyword = (payload: AnalysisKeyword) =>
   http.post('/analysis/keyword', payload);
 
@@ -33,4 +45,11 @@ export const analysisQualitative = (payload: string) =>
   http.get(`/analysis/qualitative?keyword=${payload}`);
 
 // 获取验证码
-export const verificationCode = () => http.post('v3/member/front/active/verification/code');
+export const verificationCode = (phone: string) =>
+  http.get(`v3/msg/front/commons/sendSmsCode?phone=${phone}`);
+
+// 生成报告
+export const getRecord = (payload: GenerateRecord) => http.post('/analysis/record', payload);
+
+// 更新pdf
+export const savePdf = (payload: SavePdf) => http.post('/analysis/savePdf', payload);

+ 17 - 0
xinkeaboard-promotion-portal/src/utils/http.ts

@@ -36,6 +36,7 @@ http.interceptors.response.use(
       //   type: 'error',
       //   message: response.msg || '请求失败'
       // });
+      return Promise.reject(res.msg);
     }
     return response.data; // 正常返回数据
   },
@@ -67,4 +68,20 @@ http.interceptors.response.use(
   }
 );
 
+export function uploadFile(file: File, extraData: Record<string, any> = {}) {
+  const formData = new FormData();
+  formData.append('file', file);
+
+  // 如果还要附带额外参数
+  Object.keys(extraData).forEach((key) => {
+    formData.append(key, extraData[key]);
+  });
+
+  return http.post('/v3/oss/common/upload', formData, {
+    headers: {
+      'Content-Type': 'multipart/form-data'
+    }
+  });
+}
+
 export default http;

+ 70 - 16
xinkeaboard-promotion-portal/src/utils/pdf.ts

@@ -4,49 +4,61 @@ import { ElLoading } from 'element-plus';
 import html2canvas from 'html2canvas';
 import jsPDF from 'jspdf';
 import { showMessage } from './common';
+import { uploadFile } from '@/utils/http';
+import { savePdf } from '@/utils/api';
 
 const mainStore = useMainStore();
 const expanded = computed(() => mainStore.getExpanded);
+const phone = computed(() => mainStore.getPhone);
+const recordId = computed(() => mainStore.getRecordId);
+
 const isLoadOver = computed(() => mainStore.getIsLoadOver);
 
-export const downloadPDF = async (pdfContent?: HTMLElement) => {
+/**
+ * 生成 PDF Blob
+ */
+async function generatePDFBlob(pdfContent?: HTMLElement, mask?: boolean): Promise<Blob | null> {
   if (!isLoadOver.value) {
     showMessage({ type: 'warning', message: '数据加载中,请稍后再试' });
-    return;
+    return null;
   }
 
-  const loading = ElLoading.service({
-    lock: true,
-    text: '生成中...',
-    background: 'rgba(0,0,0,0.7)',
-  });
+  let loading: any = null;
+  if (mask) {
+    loading = ElLoading.service({
+      lock: true,
+      text: '生成中...',
+      background: 'rgba(0,0,0,0.7)'
+    });
+  }
 
   try {
     if (!pdfContent) pdfContent = document.querySelector('.record') as HTMLElement;
-
     if (!expanded.value) {
       mainStore.setExpanded(true);
       await nextTick();
     }
     await nextTick();
 
-    // 高分辨率 canvas
-    const canvas = await html2canvas(pdfContent, { scale: 2, useCORS: true, backgroundColor: '#fff' });
+    const canvas = await html2canvas(pdfContent, {
+      scale: 2,
+      useCORS: true,
+      backgroundColor: '#fff'
+    });
     const canvasWidth = canvas.width;
     const canvasHeight = canvas.height;
 
     const pdf = new jsPDF('p', 'mm', 'a4');
     const pageWidth = pdf.internal.pageSize.getWidth();
     const pageHeight = pdf.internal.pageSize.getHeight();
-    const ratio = pageWidth / canvasWidth; // 宽度缩放比例
-    const pagePixelHeight = pageHeight / ratio; // PDF 一页对应 canvas 的高度
+    const ratio = pageWidth / canvasWidth;
+    const pagePixelHeight = pageHeight / ratio;
 
     let positionY = 0;
 
     while (positionY < canvasHeight) {
       const h = Math.min(pagePixelHeight, canvasHeight - positionY);
 
-      // 创建临时 canvas 截取当前页
       const pageCanvas = document.createElement('canvas');
       pageCanvas.width = canvasWidth;
       pageCanvas.height = h;
@@ -60,12 +72,54 @@ export const downloadPDF = async (pdfContent?: HTMLElement) => {
       if (positionY < canvasHeight) pdf.addPage();
     }
 
-    pdf.save('analysis.pdf');
+    const blob = pdf.output('blob'); // ✅ 生成 Blob
+    return 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;
+
+  const url = URL.createObjectURL(blob);
+  const a = document.createElement('a');
+  a.href = url;
+  a.download = 'analysis.pdf';
+  a.click();
+  URL.revokeObjectURL(url);
+}
+
+/**
+ * 上传 PDF
+ */
+export async function uploadPDF(pdfContent?: HTMLElement) {
+  const blob = await generatePDFBlob(pdfContent, false);
+  if (!blob) return;
+
+  // 转成 File 对象(后端一般习惯接收 File)
+  const file = new File([blob], 'analysis.pdf', { type: 'application/pdf' });
+
+  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;
   }
-};
+}

+ 66 - 10
xinkeaboard-promotion-portal/src/views/Record.vue

@@ -1,6 +1,20 @@
 <template>
   <div class="record">
-    <div class="record-head"></div>
+    <div
+      class="record-head"
+      :style="{
+        // backgroundImage: `url(${RecordTop})`
+        backgroundImage: expanded
+          ? `url(${RecordTop})`
+          : `url('https://assets.njnaexport.com//images/lookeen/record-top.png')`
+      }"
+    >
+      <div class="record-head-logo">
+        <img :src="Logo"/>
+      </div>
+      <div class="record-head-title">出海分析报告</div>
+      <div class="record-head-tip">全方位洞察全球市场动态,助力品牌布局海外</div>
+    </div>
     <div class="record-wrap" ref="pdfContent">
       <div class="record-wrap-content">
         <div class="record-wrap-content__overview" v-loading="loading">
@@ -48,7 +62,7 @@
 </template>
 
 <script lang="ts" setup>
-import { ref, computed, onMounted } from 'vue';
+import { ref, computed, onMounted, watch } from 'vue';
 
 import { useMainStore } from '../store';
 import KeywordSearch from '../components/keyword/search.vue';
@@ -58,10 +72,12 @@ import AiAnalysis from '@/components/AiAnalysis.vue';
 import keywordPng from '../assets/images/keyword.png';
 import CompetitorPng from '../assets/images/competitor.png';
 import AiAnalysisPng from '../assets/images/ai-analysis.png';
-import { downloadPDF } from '@/utils/pdf';
+import RecordTop from '@/assets/images/record-top.png';
+import Logo from "@/assets/images/logo.png";
+import { downloadPDF, uploadPDF } from '@/utils/pdf';
 
 const mainStore = useMainStore();
-// mainStore.initData();
+mainStore.initData();
 
 const pdfContent = ref<HTMLElement | null>(null);
 const keywordData = computed(() => mainStore.getKeywordInfo);
@@ -70,13 +86,20 @@ const keywordEn = computed(() => keywordData.value.data?.keywordEn);
 const keywords = computed(() => keywordData.value?.data?.keywords?.join(','));
 const expanded = computed(() => mainStore.getExpanded);
 const isShowCompetitor = computed(() => mainStore.getIsShowCompetitor);
+const isLoadOver = computed(() => mainStore.getIsLoadOver);
 
 const download = () => {
-  downloadPDF(pdfContent.value!);
+  downloadPDF();
 };
 
 onMounted(() => {
-  mainStore.setCurrentStep(1);
+  // mainStore.setCurrentStep(1);
+});
+
+watch(isLoadOver, (val) => {
+  if (val) {
+    uploadPDF();
+  }
 });
 </script>
 
@@ -89,16 +112,49 @@ onMounted(() => {
   background-color: #fff;
 
   &-head {
+    position: relative;
     height: 1330px;
-    background-image: url('https://assets.njnaexport.com//images/lookeen/record-top.svg');
     background-repeat: no-repeat; /* 不重复 */
     background-position: center center; /* 居中显示 */
     background-size: cover;
+
+    &-logo {
+      position: absolute;
+      left: 340px;
+      top: 40px;
+      width: 320px;
+      height: 88px;
+
+      img {
+        width: 100%;
+        height: 100%;
+        object-fit: contain;
+      }
+    }
+
+    &-title {
+      position: absolute;
+      left: 349px;
+      top: 200px;
+      font-weight: bold;
+      font-size: 40px;
+      color: #282e30;
+    }
+
+    &-tip {
+      position: absolute;
+      left: 349px;
+      top: 270px;
+      font-weight: 400;
+      font-size: 16px;
+      color: #282e30;
+    }
   }
 
   &-wrap {
-    position: absolute;
-    top: 460px;
+    // position: absolute;
+    // top: 460px;
+    margin-top: -920px;
     width: 100%;
     display: flex;
     justify-content: center;
@@ -142,7 +198,7 @@ onMounted(() => {
         box-shadow: 0px 10px 30px 0px rgba(0, 98, 165, 0.2);
         border-radius: 8px;
         font-weight: bold;
-        font-size: 32px;
+        font-size: 25px;
         color: #282e30;
         z-index: 2000;
       }

Some files were not shown because too many files changed in this diff