فهرست منبع

政策解读和政策对比思维链显示

songxy 2 ماه پیش
والد
کامیت
4f4a165aba

+ 2 - 0
ais_search_zj/web/components.d.ts

@@ -14,6 +14,7 @@ declare module 'vue' {
     BasicCurdPage: typeof import('./src/components/curd/BasicCurdPage.vue')['default']
     BasicCurdPageFrame: typeof import('./src/components/curd/frame/BasicCurdPageFrame.vue')['default']
     BasicQueryForm: typeof import('./src/components/query/BasicQueryForm.vue')['default']
+    copy: typeof import('./src/components/pdf/PDFViewerSearch copy.vue')['default']
     HomeCard: typeof import('./src/components/home-card/HomeCard.vue')['default']
     IframePage: typeof import('./src/components/iframe-page/IframePage.vue')['default']
     MarkdownToc: typeof import('./src/components/markdown-toc/MarkdownToc.vue')['default']
@@ -23,6 +24,7 @@ declare module 'vue' {
     PDFReader: typeof import('./src/components/pdf/PDFReader.vue')['default']
     PDFViewer: typeof import('./src/components/pdf/PDFViewer.vue')['default']
     PDFViewerSearch: typeof import('./src/components/pdf/PDFViewerSearch.vue')['default']
+    'PDFViewerSearch copy': typeof import('./src/components/pdf/PDFViewerSearch copy.vue')['default']
     ProgressBar: typeof import('./src/components/ProgressBar.vue')['default']
     RouterLink: typeof import('vue-router')['RouterLink']
     RouterView: typeof import('vue-router')['RouterView']

+ 810 - 0
ais_search_zj/web/src/components/pdf/PDFViewerSearch copy.vue

@@ -0,0 +1,810 @@
+<template>
+  <div style="width: 100%; height: 100%; position: relative; overflow: hidden">
+    <iframe
+      :src="src"
+      width="100%"
+      scrolling="no"
+      height="100%"
+      style="position: relative; border: medium none;"
+      ref="iframeRef"
+      @load="onLoad"
+    >
+    </iframe>
+    <div id="pdf"></div>
+    <div class="close-icon" @click="emits('close')" v-if="false">
+      <CloseOutlined />
+    </div>
+  </div>
+</template>
+<script setup>
+import { CloseOutlined } from '@ant-design/icons-vue';
+import { message } from 'ant-design-vue';
+import { nextTick, onMounted } from 'vue';
+import * as PDFJS from 'pdfjs-dist/build/pdf.js'
+import PdfjsWorker from 'pdfjs-dist/build/pdf.worker.js?worker';
+console.log("PdfjsWorker---------------------")
+console.log(PDFJS)
+
+PDFJS.GlobalWorkerOptions.workerSrc = PdfjsWorker
+const iframeRef = ref(null);
+const emits = defineEmits(['close', 'outline', 'search', 'load']);
+const searchList = ref([]);
+const pageContent = ref([]);
+const pageText = ref([]);
+const props = defineProps({
+  src: String
+});
+const type = ref('pdf');
+watch(
+  () => props.src,
+  (v) => {
+    if (v.endsWith('.pdf')) {
+      type.value = 'pdf';
+    } else if (v.endsWith('.txt')) {
+      type.value = 'txt';
+    }
+  },
+  { immediate: true }
+);
+
+const onLoad = () => {
+  const doc = iframeRef.value.contentDocument || iframeRef.value.contentWindow.document;
+  //待定
+  // var toolbar = doc.getElementsByClassName('toolbar');
+  // if (toolbar && toolbar.length > 0) {
+  // toolbar[0].style.display = 'none';
+  // }
+  setTimeout(() => {
+    const script = doc.createElement('script');
+    script.textContent = `
+  window.addEventListener('message', (event) => {
+  const message = event.data;
+  if (message.action === 'locateElement') {
+    const element= document.getElementById('hightlight-'+message.index);
+    debugger;
+    if(element!=null&&element.offsetParent){
+    setTimeout(() => {
+        element.scrollIntoView({ behavior: 'smooth', block: 'center' })
+    }, 10);
+    }
+  }
+  if (message.action === 'mlLocateElement') {
+
+    const elements= document.getElementsByClassName(message.className);
+    if(elements!=null){
+     setTimeout(() => {
+        elements[message.index].scrollIntoView({ behavior: 'smooth', block: 'center' })
+      }, 50);
+    }
+  }
+  });`;
+    doc.body.appendChild(script);
+    const style = doc.createElement('style');
+    style.textContent = `
+  #outerContainer{
+   overflow: hidden;
+  }
+   #page-container{
+  overflow: hidden;
+   }
+   #viewerContainer{
+     overflow-y: scroll;
+   }
+  .canvasWrapper {
+  position: relative;
+  /* display: flex;
+  justify-content: center; */
+}
+
+.textLayer {
+  text-align: initial;
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  overflow: hidden;
+  opacity: 1;
+  line-height: 1;
+  text-size-adjust: none;
+  forced-color-adjust: none;
+}
+.textLayer span,
+.textLayer br {
+  color: transparent;
+  position: absolute;
+  white-space: pre;
+  cursor: text;
+  transform-origin: 0% 0%;
+}
+.highlight-layer {
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  pointer-events: none;
+  opacity: 0.15;
+  overflow: hidden;
+}
+.flash {
+  animation: flashing 0.6s;
+  animation-iteration-count: 2;
+}
+@keyframes flashing {
+  0% {
+    opacity: 0;
+  }
+  100% {
+    opacity: 0.15;
+  }
+}
+.highlight {
+  background-color: #0076fa;
+  position: absolute;
+}
+  `;
+    doc.head.appendChild(style);
+  }, 400);
+  setTimeout(() => {
+    var el = doc.getElementsByClassName('page');
+    // var singleHeight = 1263;
+       var singleHeight = 1000;
+    if (iframeRef.value.height != singleHeight * el.length) {
+      // iframeRef.value.height = singleHeight * el.length;
+    }
+    setTimeout(() => {
+      pageText.value = [];
+      pageContent.value = [];
+      //获取每页数据
+      var textDivs = doc.getElementsByClassName('textLayer');
+      if (textDivs) {
+        for (var i = 0; i < textDivs.length; i++) {
+          var item = { page: i + 1, txt: textDivs[i].innerText };
+          var items = [];
+          for (var j = 0; j < textDivs[i].children.length; j++) {
+            var citem = {
+              str: textDivs[i].children[j].innerText,
+              width: textDivs[i].children[j].getBoundingClientRect().width,
+              top: textDivs[i].children[j].getBoundingClientRect().top,
+              height: textDivs[i].children[j].getBoundingClientRect().height
+            };
+            items.push(citem);
+          }
+          pageContent.value.push({ page: i + 1, items: items });
+          pageText.value.push(item);
+        }
+      }
+    }, 1000);
+  }, 1500);
+};
+
+const loadAndDisplayPdfByBlobUrl = async (blobUrl, container) => {
+  // 加载PDF文件
+  PDFJS.getDocument(blobUrl).promise.then(async (pdf) => {
+    const totalPages = pdf.numPages
+
+    // 循环绘制每个页面
+    for (let pageNum = 1; pageNum <= totalPages; pageNum++) {
+      let page = await pdf.getPage(pageNum)
+      const viewport = page.getViewport({ scale: 1 })
+      const canvas = createCanvasDom(viewport.width, viewport.height)
+      container.appendChild(canvas)
+      const context = canvas.getContext('2d')
+
+      // 渲染页面到canvas
+      await page.render({
+        canvasContext: context,
+        viewport: viewport,
+      })
+      await sleep(100)
+    }
+  })
+}
+
+const createCanvasDom = (width, height) => {
+  const canvas = document.createElement('canvas')
+  canvas.width = width
+  canvas.height = height
+  return canvas
+}
+
+const sleep = (time) => {
+  return new Promise((resolve) => {
+    setTimeout(() => {
+      resolve(time)
+    }, time)
+  })
+}
+onMounted(() => {
+  loadAndDisplayPdfByBlobUrl(props.src, document.querySelector('#pdf'))
+})
+const src = computed(() => {
+  // return `/lib/pdfjs/web/viewer.html?file=${props.src}&t=` + new Date().getTime();
+  return `/lib/pdfjs/web/viewer.html?file=${props.src}`;
+  // return `/aisearch/lib/pdfjs/web/viewer.html?file=${props.src}&t=` + new Date().getTime();
+
+  // return `/lib/pdfjs/web/viewer.html?file=http://121.40.148.47:8530/doc/knowledge_base/download_doc/国土资源部 国家发展和改革委员会+财政部+住房和城乡建设部农业部+中国人民银行+国家林业局+中国银行业监督管理委员会关于扩大国有土地有偿使用范围的意见%28279-283%29.pdf`
+});
+const scrollTo = (item) => {
+  nextTick(() => {
+    setTimeout(() => {
+      if (item.top) {
+        locateElement(item.cs, item.index, 1);
+      } else {
+        //搜索
+        locateElement(item.cs, item.index);
+        //改变选中颜色 其他黄色
+        searchList.value.forEach((citem, cindex) => {
+          const iframeDocument =
+            iframeRef.value.contentDocument || iframeRef.value.contentWindow.document;
+          const highlightedSpan = iframeDocument.getElementById('hightlight-' + cindex);
+          if (cindex == item.index) {
+            highlightedSpan.parentNode.innerHTML = highlightedSpan.parentNode.innerHTML.replace(
+              'lightblue',
+              'yellow'
+            );
+          } else {
+            highlightedSpan.parentNode.innerHTML = highlightedSpan.parentNode.innerHTML.replace(
+              'yellow',
+              'lightblue'
+            );
+          }
+        });
+      }
+    }, 10);
+  });
+};
+const locateElement = (className, index, type) => {
+  const message = {
+    action: type ? 'mlLocateElement' : 'locateElement',
+    className: className,
+    index: index
+  };
+  iframeRef.value.contentWindow.postMessage(message, '*');
+};
+const gotoElement = (element) => {
+  nextTick(() => {
+    element.scrollIntoView({ behavior: 'smooth', block: 'center' });
+  });
+};
+const highlightText = (str) => {
+  setTimeout(() => {
+    removeHighlight();
+    heightSingle(str);
+  }, 100);
+};
+const goLocation = (txt, flag) => {
+  removeHighlight();
+  searchList.value = [];
+  heightSingle(txt, flag);
+};
+//ai 问答
+const goSourceLocation = (content, type) => {
+  //获取页面
+  removeSourceHightLight();
+  const doc = iframeRef.value.contentDocument || iframeRef.value.contentWindow.document;
+
+  var pageNumber = 0;
+  if (content.indexOf('-') > -1 && content.indexOf('-') < 3) {
+    content = content.substring(content.indexOf('-') + 1);
+  }
+  var text = content;
+  var ptText = text.replaceAll('\n', '');
+  ptText = ptText.replaceAll(' ', '');
+  pageText.value.forEach((item) => {
+    item.txt = item.txt.replaceAll('\n', '');
+    item.txt = item.txt.replaceAll(' ', '');
+    console.log('==页=' + item.txt);
+    console.log('==t=' + ptText);
+    if (ptText.length > 30) {
+      if (item.txt.indexOf(ptText.substring(30, 48)) > -1) {
+        pageNumber = item.page;
+      }
+    } else {
+      if (item.txt.indexOf(ptText.substring(0, 18)) > -1) {
+        pageNumber = item.page;
+      }
+    }
+  });
+  text = text.replaceAll('\n', '');
+  text = text.replaceAll(' ', '');
+  if (text.endsWith('。') || text.endsWith(',') || text.endsWith('、')) {
+    text = text.substring(0, text.length - 1);
+  }
+  if (pageNumber == 0) {
+    message.info('未匹配到相关内容!');
+    return;
+  }
+  //'left: 133.17px; top: 152.408px; font-size: 31.3597px; font-family: sans-serif; transform: scaleX(1.01878);'
+  var arr = doc.getElementsByClassName('textLayer')[pageNumber - 1].getElementsByTagName('span');
+  if (arr) {
+    var hightList = [];
+    var startIndex = 0;
+    var endIndex = 0;
+    var thirdIndex = 0;
+    startIndex = getStartIndex(arr, text, text.length);
+    endIndex = getEndIndex(arr, text, text.length, startIndex);
+    if (startIndex && endIndex && endIndex < startIndex) {
+      endIndex = startIndex + text.length;
+    }
+    var currentPageIndex = 0;
+    var nextArr = [];
+    var thirdArr = [];
+    if (endIndex == undefined) {
+      currentPageIndex = arr.length;
+      if (doc.getElementsByClassName('textLayer')[pageNumber]) {
+        nextArr = doc.getElementsByClassName('textLayer')[pageNumber].getElementsByTagName('span');
+        endIndex = getEndIndex(nextArr, text, text.length, startIndex);
+        if (endIndex == undefined && doc.getElementsByClassName('textLayer')[pageNumber + 1]) {
+          thirdArr = doc
+            .getElementsByClassName('textLayer')
+            [pageNumber + 1].getElementsByTagName('span');
+          thirdIndex = getEndIndex(thirdArr, text, text.length, 0);
+        }
+      } else {
+        endIndex = undefined;
+      }
+    } else {
+      currentPageIndex = endIndex;
+      if (type == 1) {
+        if (endIndex && startIndex && endIndex > startIndex + content.length) {
+          currentPageIndex = startIndex + content.length;
+        }
+        //中间标点匹配问题
+        // var len = startIndex + content.length + 5;
+        // if (startIndex && endIndex && endIndex > (startIndex + content.length + 5)) {
+        //   currentPageIndex = len;
+        // }
+      }
+    }
+    if (!startIndex) {
+      startIndex = 0;
+    }
+    for (var i = startIndex; i < currentPageIndex + 1; i++) {
+      var element = arr[i];
+      if (!element) {
+        break;
+      }
+      var hItem = {};
+      var cssText = element.style.cssText;
+
+      var spanText = element.innerText;
+      var s = spanText.replaceAll(' ', '');
+      var d = spanText.endsWith('。') ? spanText.replaceAll('。', '') : spanText;
+
+      console.log(spanText);
+      if (
+        text.includes(spanText) ||
+        (s && text.includes(s)) ||
+        (d && text.includes(d)) ||
+        (spanText.substring(2) && text.includes(spanText.substring(2)))
+      ) {
+        var topLabel = cssText.split('top: ')[1].split(';')[0];
+        var leftLabel = cssText.split('left: ')[1].split(';')[0];
+        var currentItems = [];
+        pageContent.value.forEach((item) => {
+          if (item.page == pageNumber) {
+            currentItems = item.items;
+          }
+        });
+        for (var m = 0; m < currentItems.length; m++) {
+          //  citem.str=citem.str.replaceAll(' ','');
+          var citem = currentItems[m];
+          if (citem.str == spanText) {
+            hItem.width = parseFloat(citem.width);
+            hItem.height = parseFloat(citem.height);
+            hItem.text = spanText;
+            hItem.left = leftLabel;
+            hItem.top = topLabel;
+            hightList.push(hItem);
+            if (hightList.length == 1) {
+              gotoElement(element);
+            }
+            if (text.endsWith(citem.str)) {
+              break;
+            }
+          }
+        }
+      }
+    }
+    if (type == 1 && hightList.length == 0) {
+      message.info('未匹配到相关内容!');
+    }
+    // if (type == 2) {
+    var nextHightList = [];
+    //跨页 目前只考虑跨1页
+    if (endIndex != undefined) {
+      for (var i = 0; i < nextArr.length; i++) {
+        var element = nextArr[i];
+        var hItem = {};
+        var cssText = element.style.cssText;
+        var spanText = element.innerText;
+        console.log(spanText);
+        if (text.includes(spanText)) {
+          var topLabel = cssText.split('top: ')[1].split(';')[0];
+          var leftLabel = cssText.split('left: ')[1].split(';')[0];
+          var currentItems = [];
+          pageContent.value.forEach((item) => {
+            if (item.page == pageNumber + 1) {
+              currentItems = item.items;
+            }
+          });
+          for (var l = 0; l < currentItems.length; l++) {
+            var citem = currentItems[l];
+            if (citem.str == spanText) {
+              hItem.width = parseFloat(citem.width);
+              hItem.height = parseFloat(citem.height);
+              hItem.text = spanText;
+              hItem.left = leftLabel;
+              hItem.top = topLabel;
+              nextHightList.push(hItem);
+              if (text.endsWith(citem.str)) {
+                break;
+              }
+            }
+          }
+        }
+        if (text.endsWith(spanText)) {
+          break;
+        }
+      }
+    } else {
+      //跨页 目前只考虑跨2页
+      for (var i = 0; i < nextArr.length; i++) {
+        var element = nextArr[i];
+        var hItem = {};
+        var cssText = element.style.cssText;
+        var spanText = element.innerText;
+        console.log(spanText);
+
+        var topLabel = cssText.split('top: ')[1].split(';')[0];
+        var leftLabel = cssText.split('left: ')[1].split(';')[0];
+        var currentItems = [];
+        pageContent.value.forEach((item) => {
+          if (item.page == pageNumber + 1) {
+            currentItems = item.items;
+          }
+        });
+        for (var l = 0; l < currentItems.length; l++) {
+          var citem = currentItems[l];
+          if (citem.str == spanText) {
+            hItem.width = parseFloat(citem.width);
+            hItem.height = parseFloat(citem.height);
+            hItem.text = spanText;
+            hItem.left = leftLabel;
+            hItem.top = topLabel;
+            nextHightList.push(hItem);
+            if (text.endsWith(citem.str)) {
+              break;
+            }
+          }
+        }
+      }
+      var thirdHightList = [];
+      if (thirdIndex != 0 && thirdIndex != undefined) {
+        for (var z = 0; z < thirdIndex + 1; z++) {
+          var element = thirdArr[z];
+          var hItem = {};
+          var cssText = element.style.cssText;
+          var spanText = element.innerText;
+          console.log(spanText);
+          if (text.includes(spanText)) {
+            var topLabel = cssText.split('top: ')[1].split(';')[0];
+            var leftLabel = cssText.split('left: ')[1].split(';')[0];
+            var currentItems = [];
+            pageContent.value.forEach((item) => {
+              if (item.page == pageNumber + 2) {
+                currentItems = item.items;
+              }
+            });
+            for (var l = 0; l < currentItems.length; l++) {
+              var citem = currentItems[l];
+              if (citem.str == spanText) {
+                hItem.width = parseFloat(citem.width);
+                hItem.height = parseFloat(citem.height);
+                hItem.text = spanText;
+                hItem.left = leftLabel;
+                hItem.top = topLabel;
+                thirdHightList.push(hItem);
+                if (text.endsWith(citem.str)) {
+                  break;
+                }
+              }
+            }
+          }
+          if (text.endsWith(spanText)) {
+            break;
+          }
+        }
+      }
+    }
+
+    //渲染
+    var canvas = doc.getElementsByClassName(`page`)[pageNumber - 1];
+    const hightDiv = document.createElement('div');
+    hightDiv.className = 'highlight-layer flash';
+    hightList.forEach((it) => {
+      const hDiv = document.createElement('div');
+      hDiv.className = 'highlight';
+      // 设置内联样式
+      hDiv.style.width = it.width + 'px';
+      hDiv.style.height = it.height * 1.4 + 'px';
+      hDiv.style.left = it.left;
+      hDiv.style.top = parseFloat(it.top.split('%')[0]) - 0.5 + '%';
+      hightDiv.appendChild(hDiv);
+    });
+    canvas.appendChild(hightDiv);
+    if (nextHightList && nextHightList.length > 0) {
+      //渲染
+      var nextCanvas = doc.getElementsByClassName(`page`)[pageNumber];
+      const hightDiv = document.createElement('div');
+      hightDiv.className = 'highlight-layer flash';
+      nextHightList.forEach((it) => {
+        const hDiv = document.createElement('div');
+        hDiv.className = 'highlight';
+        // 设置内联样式
+        hDiv.style.width = it.width + 'px';
+        hDiv.style.height = it.height * 1.4 + 'px';
+        hDiv.style.left = it.left;
+        hDiv.style.top = parseFloat(it.top.split('%')[0]) - 0.5 + '%';
+        hightDiv.appendChild(hDiv);
+      });
+      nextCanvas.appendChild(hightDiv);
+    }
+    if (thirdHightList && thirdHightList.length > 0) {
+      //渲染
+      var thirdCanvas = doc.getElementsByClassName(`page`)[pageNumber + 1];
+      const hightDiv = document.createElement('div');
+      hightDiv.className = 'highlight-layer flash';
+      thirdHightList.forEach((it) => {
+        const hDiv = document.createElement('div');
+        hDiv.className = 'highlight';
+        // 设置内联样式
+        hDiv.style.width = it.width + 'px';
+        hDiv.style.height = it.height * 1.4 + 'px';
+        hDiv.style.left = it.left;
+        hDiv.style.top = parseFloat(it.top.split('%')[0]) - 0.5 + '%';
+        hightDiv.appendChild(hDiv);
+      });
+      thirdCanvas.appendChild(hightDiv);
+    }
+    // }
+  }
+};
+//交集
+function commonContent(str1, str2) {
+  let result = '';
+  for (let i = 0; i < str1.length; i++) {
+    if (str2.includes(str1[i])) {
+      result += str1[i];
+    }
+  }
+  return result;
+}
+const getStartIndex = (arr, text, total) => {
+  var indexs = [];
+  for (var i = 0; i < arr.length; i++) {
+    var element = arr[i];
+    var spanText = element.innerText;
+    console.log('===' + spanText);
+
+    if (
+      (spanText && text.startsWith(spanText)) ||
+      (spanText && spanText.substring(2) && spanText && text.startsWith(spanText.substring(2))) ||
+      (spanText.indexOf('。') > -1 &&
+        spanText.split('。')[1] &&
+        text.startsWith(spanText.split('。')[1])) ||
+      (spanText.endsWith('。') &&
+        spanText.split('。')[0] &&
+        text.startsWith(spanText.split('。')[0]))
+    ) {
+      indexs.push(i);
+      //   return i;
+    }
+  }
+  if (indexs.length == 0) {
+    return 0;
+  } else if (indexs.length == 2) {
+    console.log(JSON.stringify(indexs));
+    return indexs[0] == 0 ? indexs[1] : indexs[0];
+  } else {
+    return indexs[0];
+  }
+  //todo 有问题
+  // if (count == arr.length) {
+  //   start.value = start.value + 1;
+  //   text = text.substring(start.value, total);
+  //   getStartIndex(arr, text, total);
+  // }
+};
+const getEndIndex = (arr, text, total, startIndex) => {
+  for (var i = 0; i < arr.length; i++) {
+    var element = arr[i];
+    var spanText = element.innerText;
+    var e = '';
+    if (spanText.endsWith('。')) {
+      spanText = spanText.substring(0, spanText.lastIndexOf('。'));
+    }
+    if (spanText.indexOf('。') > -1) {
+      e = spanText.substring(0, spanText.lastIndexOf('。'));
+    }
+    console.log('===' + spanText);
+    if (
+      (spanText && text.endsWith(spanText) && spanText.length > 0) ||
+      (spanText && spanText.indexOf(text) > -1) ||
+      (e && text.endsWith(e))
+    ) {
+      return i;
+    }
+  }
+};
+const heightSingle = (str, flag) => {
+  const iframeDocument = iframeRef.value.contentDocument || iframeRef.value.contentWindow.document;
+
+  const searchText = str;
+  var highlightColor = 'lightblue';
+
+  // 使用DOM操作在iframe中搜索并高亮文本
+  const textNodes = iframeDocument.evaluate(
+    ".//text()[contains(., '" + searchText + "')]",
+    iframeDocument,
+    null,
+    XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,
+    null
+  );
+  var firstFlag = false;
+  for (let i = 0, length = textNodes.snapshotLength; i < length; i++) {
+    const content = textNodes.snapshotItem(i);
+    const textNode = content.textContent;
+
+    const regex = new RegExp(searchText, 'gi');
+    var newContent = textNode.replace(regex, (match) => {
+      if (match == searchText) {
+        if (!firstFlag) {
+          firstFlag = true;
+          highlightColor = 'yellow';
+        } else {
+          highlightColor = 'lightblue';
+        }
+        return `<span  id='hightlight-${searchList.value.length}' style="background-color: ${highlightColor};color: #333;margin-top:-3px;border-radius: 3px;">${match}</span>`;
+      }
+    });
+    if (content.parentNode) {
+      var n = content.parentNode;
+      console.log('====' + n.offsetTop);
+      newContent = '<' + newContent.split('<')[1] + '</span>';
+      var h = content.parentNode.innerHTML;
+      if (!flag) {
+        content.parentNode.innerHTML = h.replace(searchText, newContent);
+      }
+      if (n) {
+        searchList.value.push({
+          cs: n.parentNode.className,
+          index: searchList.value.length,
+          n: n.parentNode
+        });
+        if (searchList.value.length == 1) {
+          // if (!flag) {
+          //   locateElement(searchList.value[0].cs, 0);
+          // }
+          gotoElement(n);
+        }
+        //大纲只定位一次
+        if (flag) {
+          break;
+        }
+      }
+    }
+    if (!flag) {
+      if (i == textNodes.snapshotLength - 1) {
+        //查询结束
+        emits('search', searchList.value);
+      }
+    }
+  }
+};
+const removeSourceHightLight = () => {
+  //获取页面 ai定位移除
+  const doc = iframeRef.value.contentDocument || iframeRef.value.contentWindow.document;
+
+  const arr = doc.getElementsByClassName('highlight-layer');
+  const l = arr.length;
+  for (let i = l - 1; i >= 0; i--) {
+    if (arr[i] != null) {
+      arr[i].parentNode.removeChild(arr[i]);
+    }
+  }
+};
+const removeHighlight = () => {
+  removeSourceHightLight();
+  if (searchList.value) {
+    searchList.value.forEach((item, index) => {
+      const iframeDocument =
+        iframeRef.value.contentDocument || iframeRef.value.contentWindow.document;
+      const highlightedSpan = iframeDocument.getElementById('hightlight-' + index);
+      if (highlightedSpan != null) {
+        const span = highlightedSpan;
+        console.log(span.innerHTML);
+        setTimeout(() => {
+          span.parentNode.replaceChild(document.createTextNode(span.textContent), span);
+        }, 10);
+      }
+    });
+  }
+  searchList.value = [];
+  emits('search', searchList.value);
+};
+defineExpose({ highlightText, removeHighlight, scrollTo, goLocation, goSourceLocation, location });
+</script>
+
+<style scoped lang="scss">
+.close-icon {
+  position: absolute;
+
+  top: 8px;
+  right: 12px;
+  cursor: pointer;
+}
+</style>
+<style>
+/* 不能用scoped,因为动态创建的元素不会被编译带上hash */
+/* 不加上这些css修饰,元素会错位,样式来自vue-pdf-embed这个库 */
+.canvasWrapper {
+  position: relative;
+  /* display: flex;
+  justify-content: center; */
+}
+.page {
+  position: relative;
+  box-shadow: 0 4px 10px 0 rgba(0, 0, 0, 0.08);
+  margin: 30px auto;
+}
+.textLayer {
+  text-align: initial;
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  overflow: hidden;
+  opacity: 0.2;
+  line-height: 1;
+  text-size-adjust: none;
+  forced-color-adjust: none;
+}
+.textLayer span,
+.textLayer br {
+  color: transparent;
+  position: absolute;
+  white-space: pre;
+  cursor: text;
+  transform-origin: 0% 0%;
+}
+.highlight-layer {
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  pointer-events: none;
+  opacity: 0.15;
+  overflow: hidden;
+}
+.flash {
+  animation: flashing 0.6s;
+  animation-iteration-count: 2;
+}
+@keyframes flashing {
+  0% {
+    opacity: 0;
+  }
+  100% {
+    opacity: 0.15;
+  }
+}
+.highlight {
+  background-color: #0076fa;
+  position: absolute;
+}
+</style>

+ 11 - 29
ais_search_zj/web/src/views/ai-home/index.vue

@@ -95,11 +95,11 @@
                                           <img src="/images/icon-ds.png" />
                                         </div>
                                         <div class="ds-panel">
-                                          <div class="ds-loading" v-if="history.currentResponse.hintTxt" @click="dsUp = !dsUp">
+                                          <div class="ds-loading" v-if="history.currentResponse.hintTxt" @click="history.currentResponse.showChain = !history.currentResponse.showChain">
                                             {{ history.currentResponse.hintTxt }}
-                                            <DownOutlined class="icon-arrow" :class="{ rotate: dsUp }" />
+                                            <DownOutlined class="icon-arrow" :class="{ rotate: history.currentResponse.showChain }" />
                                           </div>
-                                          <div class="ds-con" v-if="!dsUp">
+                                          <div class="ds-con" v-if="history.currentResponse.showChain">
                                             <vue-markdown-it
                                               id="dsMarkdown"
                                               :source="
@@ -457,12 +457,17 @@ const onSendHandle = (status = true) => {
       streamMock: false,
       msg: '',
       originAnswer: '',
+      showChain: true,
       oDocs: '',
       docs: []
     }
   })
   ask(decodeURIComponent(cQuestion.value));
   cQuestion.value = '';
+  const timer = setTimeout(() => {
+    scrollToBottom()
+    clearTimeout(timer)
+  }, 50)
 }
 
 const toToolPage = (path) => {
@@ -495,7 +500,6 @@ const pdfNum = ref(1);
 const fileType = ref('pdf');
 const startTime = ref(0);
 const endTime = ref(0);
-const dsUp = ref(false);
 const times = ref(0);
 const timers = ref([]);
 let streamMockInterval = null;
@@ -670,6 +674,7 @@ const quest = async (isFllow) => {
     streamMock: false,
     streamMsg: '',
     oDocs: '',
+    showChain: true,
     docs: []
   };
   activeIndex.value = 0;
@@ -976,7 +981,6 @@ const openDoc = (doc, i) => {
   var link = doc.link;
   var type = doc.type;
   pdfSrc.value = window.formatDocUrl(link, modelType.value)
-  console.log(pdfSrc.value)
   showDoc.value = true;
   fileType.value = type;
   pdfContent.value = doc.content;
@@ -1021,30 +1025,7 @@ const openDocByIndex = (ind, id) => {
     pdfNum.value = historys.value[historyIndex].currentResponse.docs[ind - 1].num;
   }
   pdfNum.value = ind;
-  const knowledgeDocUrl = window.AppGlobalConfig.knowledgeDocUrl.replace(
-      '=policy&',
-      activeTab.value === 'paper' ? '=compose_paper_material_total&' : '=policy&'
-  )
-  if(link.indexOf(knowledgeDocUrl) != -1) {
-    pdfSrc.value = link.replace(knowledgeDocUrl,
-      window.AppGlobalConfig.knowledgeDocUrlProxy.replace(
-        '=policy&',
-        activeTab.value === 'paper' ? '=compose_paper_material_total&' : '=policy&'
-      )
-    );
-  } else {
-    pdfSrc.value = link.replace(
-      window.AppGlobalConfig.knowledgeDocUrl2.replace(
-        '=policy&',
-        activeTab.value === 'paper' ? '=compose_paper_material_total&' : '=policy&'
-      ),
-      window.AppGlobalConfig.knowledgeDocUrlProxy2.replace(
-        '=policy&',
-        activeTab.value === 'paper' ? '=compose_paper_material_total&' : '=policy&'
-      )
-    );
-  }
-  console.log(pdfSrc.value)
+  pdfSrc.value = window.formatDocUrl(link, modelType.value)
   showDoc.value = true;
 };
 window.openDocByIndex = openDocByIndex;
@@ -1148,6 +1129,7 @@ const switchSession = async (item) => {
         hintTxt: `已深度思考(用时 ${item['thinkTime']} 秒)`,
         msg: msg,
         sourceVisible: false,
+        showChain: true,
         docs: []
       }
     })

+ 31 - 8
ais_search_zj/web/src/views/zcdb/components/aiAssistant.vue

@@ -32,15 +32,24 @@
               <div class="ai-panel">
                 <img src="/images/zczk/icon-ai-title.png" />
                 <div class="desc" v-if="item.content">
+                  <div class="chain">
+                    <vue-markdown-it
+                      :source="item.isChain === '0' ? item.content : item.content.substring(0, item.content.indexOf('###'))"
+                      :options="{
+                        html: true,
+                        linkify: true
+                      }"
+                    />
+                  </div>
                   <vue-markdown-it
-                    :source="item.content"
+                    :source="item.isChain === '1' ? item.content : ''"
                     :options="{
                       html: true,
                       linkify: true
                     }"
                   />
                 </div>
-                <div class="desc animation" v-else>正在思考中<span class="dots"></span></div>
+                <div class="desc animation" v-else>{{ status === 2 ? '网络错误': '正在思考中' }}<span class="dots" v-if="status !== 2"></span></div>
               </div>
             </div>
           </div>
@@ -75,6 +84,7 @@ const props = defineProps({
     default: ()=>[]
   }
 });
+const status = ref(0)
 const { data } = toRefs(props);
 onMounted(() => {});
 
@@ -118,7 +128,7 @@ const send = () => {
   }
   form.value.query = form.value.keyoword;
   form.value.keyoword = '';
-  const item = { name: form.value.query, content: '' };
+  const item = { name: form.value.query, content: '', isChain: '0' };
   form.value.chatDesc.push(item);
   query();
 };
@@ -135,6 +145,7 @@ const query = async () => {
   params.append('query', form.value.query);
   timers.value = []
   timerIndex.value = 0
+  status.value = 0
   await fetchEventSource(
     (window)?.AppGlobalConfig?.aiServer + '/chat/complete_file_chat',
     {
@@ -151,7 +162,6 @@ const query = async () => {
           }
           try {
             const rData = JSON.parse(msg.data);
-            console.log(msg.data);
             if (rData) {
               setTimeout(() => {
                 form.value.aiLoading = false;
@@ -160,6 +170,9 @@ const query = async () => {
               if (rData.status == 2) {
                 let timer = setTimeout(() => {
                   form.value.chatDesc[form.value.chatDesc.length - 1].content += con;
+                  if (form.value.chatDesc[form.value.chatDesc.length - 1].content.indexOf('###') !== -1) {
+                    form.value.chatDesc[form.value.chatDesc.length - 1].isChain = '1'
+                  }
                   scrollToBottom();
                   clearTimeout(timer)
                 }, timerIndex.value * 15)
@@ -172,11 +185,13 @@ const query = async () => {
                 let nums = getNumAll(con);
                 nums.forEach(num => {
                   con = con.replace(
-                        `[[${num}]]`,
-                        `<span onclick="window.openDocByIndex(${num})" class="poi" style="cursor: pointer; display: inline-block; width: 20px; height: 20px; font-size: 14px; line-height: 20px; text-align: center; margin: 0 5px; border-radius: 4px;background: #fff;color:#435C80;border: 1px solid #BACAE3">${num}</span>`
+                    `[[${num}]]`,
+                        ''
+                        // `<span onclick="window.openDocByIndex(${num})" class="poi" style="cursor: pointer; display: inline-block; width: 20px; height: 20px; font-size: 14px; line-height: 20px; text-align: center; margin: 0 5px; border-radius: 4px;background: #fff;color:#435C80;border: 1px solid #BACAE3">${num}</span>`
                     )
                 })
-                form.value.chatDesc[form.value.chatDesc.length - 1].content = con;
+                form.value.chatDesc[form.value.chatDesc.length - 1].isChain = '1'
+                form.value.chatDesc[form.value.chatDesc.length - 1].content = con + '\n\n  #### 以上是我思考的内容!\n\n';
                 scrollToBottom();
                 if (rData.docs) {
                   handleDocs(rData.docs);
@@ -188,9 +203,12 @@ const query = async () => {
           }
         }
       },
-      onclose() {},
+      onclose() {
+        status.value = 1
+      },
       onerror(err) {
         form.value.aiLoading = false;
+      status.value = 2
         throw err;
       }
     }
@@ -417,6 +435,11 @@ const scrollToBottom = async () => {
             font-size: 14px;
             color: #333333;
             line-height: 28px;
+            >.chain {
+              color: #666;
+              border-left: 1px solid #ccc;
+              padding-left: 15px;
+            }
           }
           .animation {
             @keyframes dot {

+ 51 - 8
ais_search_zj/web/src/views/zjjd/index.vue

@@ -43,7 +43,7 @@
               <div class="check">
                 <a-radio-group v-model:value="checkType" button-style="solid">
                   <a-radio-button value="interpretation">AI解读</a-radio-button>
-                  <a-radio-button value="question">AI问答</a-radio-button>
+                  <a-radio-button value="question">AI解读</a-radio-button>
                 </a-radio-group>
               </div>
             </div>
@@ -52,8 +52,18 @@
                 <a-spin class="spin" v-if="aiLoading"></a-spin>
                 <template v-else>
                   <div class="desc">
+                    <div class="chain">
+                      <vue-markdown-it
+                        :source="isChain === '0' ? content : content.substring(0, content.indexOf('###'))"
+                        v-if="content"
+                        :options="{
+                          html: true,
+                          linkify: true
+                        }"
+                      />
+                    </div>
                     <vue-markdown-it
-                      :source="content"
+                      :source="isChain === '1' ? content.substring(content.indexOf('###')) : ''"
                       v-if="content"
                       :options="{
                         html: true,
@@ -91,15 +101,24 @@
                       <div class="ai-panel">
                         <img src="/images/zczk/icon-ai-title.png" />
                         <div class="desc" v-if="item.content" id="scrollArea-1">
+                          <div class="chain">
+                            <vue-markdown-it
+                              :source="item.isChain === '0' ? item.content : item.content.substring(0, item.content.indexOf('###'))"
+                              :options="{
+                                html: true,
+                                linkify: true
+                              }"
+                            />
+                          </div>
                           <vue-markdown-it
-                            :source="item.content"
+                            :source="item.isChain === '1' ? item.content.substring(item.content.indexOf('###')) : ''"
                             :options="{
                               html: true,
                               linkify: true
                             }"
                           />
                         </div>
-                        <div class="desc animation" v-else>正在思考中<span class="dots"></span></div>
+                        <div class="desc animation" v-else>{{ status === 2 ? '网络错误': '正在思考中' }}<span class="dots" v-if="status !== 2"></span></div>
                       </div>
                     </div>
                   </div>
@@ -230,7 +249,7 @@ const send = () => {
   }
   queryText.value = keyword.value;
   keyword.value = '';
-  var item = { name: queryText.value, content: '' };
+  var item = { name: queryText.value, content: '', isChain: '0' };
   chatDesc.value.push(item);
   query();
   scrollToBottom('chat_box')
@@ -331,6 +350,8 @@ const handleDocs = (data) => {
 };
 const timers = ref([])  //缓存定时器
 const timerIndex = ref(0)
+const status = ref('0');  //0 未请求 1 请求成功 2 请求错误
+const isChain = ref(0); //0 未开始思维链 1 已经开始思维链
 const query = async () => {
   var params = new FormData();
   aiLoading.value = true;
@@ -339,6 +360,8 @@ const query = async () => {
   params.append('query', queryText.value);
   timers.value = []
   timerIndex.value = 0
+  status.value = 0
+  isChain.value = '0'
   await fetchEventSource(window.AppGlobalConfig.aiServer + '/chat/complete_file_chat', {
     method: 'POST',
     openWhenHidden: true,
@@ -358,15 +381,17 @@ const query = async () => {
               let nums = getNumAll(con);
               nums.forEach(num => {
                 con = con.replace(
-                      `[[${num}]]`,
-                      `<span onclick="window.openDocByIndex(${num})" class="poi" style="    cursor: pointer; display: inline-block; width: 20px; height: 20px; font-size: 14px; line-height: 20px; text-align: center; margin: 0 5px; border-radius: 4px;background: #fff;color:#435C80;border: 1px solid #BACAE3">${num}</span>`
+                    `[[${num}]]`,
+                    ''
+                      // `<span onclick="window.openDocByIndex(${num})" class="poi" style="    cursor: pointer; display: inline-block; width: 20px; height: 20px; font-size: 14px; line-height: 20px; text-align: center; margin: 0 5px; border-radius: 4px;background: #fff;color:#435C80;border: 1px solid #BACAE3">${num}</span>`
                   )
               })
               if (checkType.value == 'interpretation') {
                 content.value = con;
                 scrollToBottom('scrollArea');
               } else {
-                chatDesc.value[chatDesc.value.length - 1].content = con + '\n\n #### 以上是我思考的内容!\n\n';
+                chatDesc.value[chatDesc.value.length - 1].isChain = '1'
+                chatDesc.value[chatDesc.value.length - 1].content = con + '\n\n  #### 以上是我思考的内容!\n\n';
                 scrollToBottom('chat_box');
               }
               if (rData.docs) {
@@ -377,12 +402,18 @@ const query = async () => {
               if (checkType.value == 'interpretation') {
                 timer = setTimeout(() => {
                   content.value += con;
+                  if (content.value.indexOf('###') !== -1) {
+                    isChain.value = '1'
+                  }
                   scrollToBottom('scrollArea');
                   clearTimeout(timer)
                 }, timerIndex.value * 15)
               } else {
                 timer = setTimeout(() => {
                   chatDesc.value[chatDesc.value.length - 1].content += con;
+                  if (chatDesc.value[chatDesc.value.length - 1].content.indexOf('###') !== -1) {
+                    chatDesc.value[chatDesc.value.length - 1].isChain = '1'
+                  }
                   scrollToBottom('chat_box');
                   clearTimeout(timer)
                 }, timerIndex.value * 15)
@@ -398,9 +429,11 @@ const query = async () => {
     },
     onclose() {
       aiLoading.value = false;
+      status.value = 1
     },
     onerror(err) {
       aiLoading.value = false;
+      status.value = 2
       throw err;
     },
     onopen() {}
@@ -494,6 +527,11 @@ const query = async () => {
               //color: #85909e;
               line-height: 28px;
               margin-bottom: 36px;
+              >.chain {
+                color: #666;
+                border-left: 1px solid #ececec;
+                padding-left: 15px;
+              }
             }
             .spin {
               height: 100%;
@@ -627,6 +665,11 @@ const query = async () => {
                     font-size: 14px;
                     color: #333333;
                     line-height: 28px;
+                    >.chain {
+                      color: #666;
+                      border-left: 1px solid #ccc;
+                      padding-left: 15px;
+                    }
                   }
                   .animation {
                     @keyframes dot {

+ 2 - 1
ais_search_zj/web/vite.config.js

@@ -63,7 +63,8 @@ export default defineConfig({
       },
       '/aiServer': {
         // target: 'http://localhost:9999/',
-          target: 'https://zjugpt.com/llm',
+          // target: 'https://zjugpt.com/llm',
+          target: 'https://ai.zrzyt.zj.gov.cn/aiServer',
           // target: 'https://zdzy.zrzyt.zj.gov.cn/aisKnowledge',
           changeOrigin: true,
           rewrite: function (path) { return path.replace(/^\/aiServer/, ''); }