Przeglądaj źródła

知识库前端改造和后台接口对接

songxy 4 miesięcy temu
rodzic
commit
a62f6ebac9

+ 0 - 1
ais_search/web/components.d.ts

@@ -19,7 +19,6 @@ declare module 'vue' {
     MarkdownToc: typeof import('./src/components/markdown-toc/MarkdownToc.vue')['default']
     MyIcon: typeof import('./src/components/myIcon/index.vue')['default']
     NewSliderCardUpDown: typeof import('./src/components/NewSliderCardUpDown.vue')['default']
-    NoDataSkeleton: typeof import('./src/components/NoDataSkeleton.vue')['default']
     PdfCanvas: typeof import('./src/components/pdf/PdfCanvas.vue')['default']
     PDFViewer: typeof import('./src/components/pdf/PDFViewer.vue')['default']
     RouterLink: typeof import('vue-router')['RouterLink']

+ 11 - 1
ais_search/web/src/assets/base.css

@@ -42,4 +42,14 @@ ul, li {
 	list-style: none;
 	padding: 0px;
 	margin: 0px;
-  }
+  }
+  
+.popover_item {
+  margin-bottom: 0px;
+  padding: 5px 8px;
+  color: #7A7F90;
+  cursor: pointer;
+}
+.popover_item>.icon {
+  margin-right: 6px;
+}

+ 0 - 38
ais_search/web/src/components/NoDataSkeleton.vue

@@ -1,38 +0,0 @@
-<template>
-  <div class="noDataBox">
-    <a-skeleton active v-if="loading" />
-    <div class="no_data" v-else>
-      <div class="img" />
-    </div>
-  </div>
-</template>
-<script setup>
-
-defineOptions({
-  name: 'NoDataSkeleton'
-})
-
-defineProps({
-  loading: {
-    type: Boolean,
-    default: ()=>false
-  }
-})
-</script>
-
-<style lang="scss" scoped>
-.noDataBox {
-  width: 100%;
-  >.no_data {
-    padding: 30px;
-    padding-top: 50px;
-    >.img {
-      width: 128px;
-      height: 128px;
-      background: url("@/assets/images/no_data_icon.png") no-repeat;
-      background-size: 100% 100%;
-      margin: auto;
-    }
-  }
-}
-</style>

+ 23 - 0
ais_search/web/src/utils/struct.js

@@ -35,3 +35,26 @@ export function getAllParent(id, list = [], pIdField = 'pId', nameField = 'name'
     }
     return result;
 }
+
+const DEFAULT_CONFIG = {
+  id: 'id',
+  children: 'children',
+  pid: 'parentId'
+}
+const getConfig = (config) => Object.assign({}, DEFAULT_CONFIG, config)
+// tree from list
+export const listToTree =  (list, config) => {
+  const conf = getConfig(config)
+  const nodeMap = new Map()
+  const result = []
+  const { id, children, pid } = conf
+  for (const node of list) {
+    node[children] = node[children] || []
+    nodeMap.set(node[id], node)
+  }
+  for (const node of list) {
+    const parent = nodeMap.get(node[pid])
+    ;(parent ? parent.children : result).push(node)
+  }
+  return result
+}

+ 0 - 2669
ais_search/web/src/views/ai-search/ai-search copy.vue

@@ -1,2669 +0,0 @@
-<template>
-  <div class="ai-search-detail" id="pageContainer">
-    <a-affix>
-      <div class="header">
-        <ai-search-header />
-      </div>
-      <div class="sub-header">
-        <ai-search-sub-header @search="ask" />
-      </div>
-    </a-affix>
-    <div class="search-panel">
-      <a-affix
-        ref="markdownTocAffixRef"
-        :style="`width: 20%;    margin-left: 13%;max-height: ${tocHeight}px;`"
-        :offset-top="210"
-        v-show="!showDoc"
-      >
-        <markdown-toc :toc="tocs" :class="`markdown-toc-${askType}`" />
-      </a-affix>
-      <div :class="`search-detail search-detail-${askType}`" ref="searchRef">
-        <div
-          class="search-result"
-          id="searchResult"
-          style="overflow-x: hidden; height: auto"
-          ref="messageContainer"
-        >
-          <div class="top">
-            <div class="title">
-              <div class="icon"></div>
-              <div class="question" style="cursor: pointer" @click="openAskModal">
-                {{ question }}
-              </div>
-              <div v-show="askType === 'tdsc'" class="graph-switch">
-                <a-switch v-model:checked="graph" @change="questLandMark" />
-                生成图表
-              </div>
-            </div>
-          </div>
-
-          <div :class="`result-panel result-panel-${askType}`">
-            <div class="search-steps" v-show="activeIndex !== -1 && activeIndex < 5">
-              <div
-                v-show="st.key <= activeIndex"
-                :class="`step ${st.key < activeIndex ? 'step-done' : ''}`"
-                v-for="(st, i) in steps"
-                :key="`step-${i}`"
-              >
-                <div class="info">
-                  <div class="icon"></div>
-                  <div class="title">{{ st.name }}</div>
-                  <div class="tags">
-                    <div
-                      v-if="st.key === 2 && askType === 'tdsc'"
-                      style="width: 20%; font-size: 14px; line-height: 22px"
-                      class="tag-last"
-                    >
-                      {{ st.agent }}
-                    </div>
-                    <div
-                      :class="`tag${st.key === 0 ? ' tag-first' : ''}`"
-                      v-if="st.key < 3 && askType !== 'tdsc'"
-                      :title="t"
-                      v-for="(t, i) in st.tags"
-                      :key="`tag-${i}`"
-                    >
-                      {{ t }}
-                    </div>
-                    <div
-                      class="d"
-                      style="
-                        width: 80%;
-                        overflow: hidden;
-                        animation: scroll-text-992e3d76 25s linear infinite;
-                      "
-                      v-if="st.key < 3 && askType === 'tdsc'"
-                    >
-                      <div
-                        :title="t"
-                        :class="`tag${askType === 'tdsc' ? ' tag-rol' : ''}`"
-                        v-for="(t, i) in st.tags"
-                        :key="`tag-${i}`"
-                      >
-                        <div>{{ t }}</div>
-                      </div>
-                    </div>
-                  </div>
-                </div>
-                <div class="progress">
-                  <a-progress size="small" :show-info="false" :percent="st.prog" />
-                </div>
-              </div>
-            </div>
-
-            <div class="result">
-              <template v-if="askType === 'zcfg'">
-                <div class="tabs" style="position: relative">
-                  <div
-                    :class="`tab${activeTab === t.key ? ' tab-active' : ''}`"
-                    v-for="t in tabs"
-                    :key="t.key"
-                    @click="changeTab(t.key)"
-                  >
-                    <div class="title">{{ t.name }}</div>
-                    <div class="bottom"></div>
-                  </div>
-                  <div
-                    class="scope-type"
-                    style="
-                      display: flex;
-                      align-items: center;
-                      position: absolute;
-                      right: 5px;
-                      top: 10px;
-                    "
-                  >
-                    <div class="label">搜索模式:</div>
-                    <a-radio-group
-                      v-model:value="answerType"
-                      @change="changeAnswerType"
-                      name="radioGroup"
-                    >
-                      <a-radio value="0">简洁</a-radio>
-                      <a-radio value="1">深入</a-radio>
-                      <a-radio value="2">研究</a-radio>
-                    </a-radio-group>
-                    <div class="ds-box">
-                      <i class="iconfont icon-deepsee"></i>
-                      <div class="switch">
-                        <a-switch v-model:checked="dsChecked" @change="onChange" />
-                      </div>
-                    </div>
-                  </div>
-                </div>
-                <div class="result-view">
-                  <div class="q-r">
-                    <div class="ds-content-box" v-if="dsChecked">
-                      <div class="icon">
-                        <img src="/images/icon-ds.png" />
-                      </div>
-                      <div class="ds-panel">
-                        <div class="ds-loading" v-if="dsHintTxt" @click="dsUp = !dsUp">
-                          {{ dsHintTxt }}
-                          <DownOutlined class="icon-arrow" :class="{ rotate: dsUp }" />
-                        </div>
-                        <div class="ds-con" v-if="!dsUp">
-                          <vue-markdown-it
-                            id="dsMarkdown"
-                            :source="
-                              currentResponse.streamMock
-                                ? currentResponse.streamMsg.indexOf('###') > -1
-                                  ? currentResponse.streamMsg.substring(
-                                      0,
-                                      currentResponse.streamMsg.indexOf('###')
-                                    )
-                                  : currentResponse.streamMsg
-                                : currentResponse.msg.indexOf('###') > -1
-                                ? currentResponse.msg.substring(
-                                    0,
-                                    currentResponse.msg.indexOf('###')
-                                  )
-                                : currentResponse.msg
-                            "
-                            :options="{
-                              html: true,
-                              linkify: true
-                            }"
-                          />
-                        </div>
-                        <a-spin :indicator="indicator" v-if="dsLoading" />
-                      </div>
-                    </div>
-                    <vue-markdown-it
-                      id="resMarkdown"
-                      :source="
-                        currentResponse.streamMock
-                          ? currentResponse.streamMsg.indexOf('###') > -1
-                            ? currentResponse.streamMsg.substring(
-                                currentResponse.streamMsg.indexOf('###')
-                              )
-                            : ''
-                          : dsChecked
-                          ? currentResponse.msg.indexOf('###') > -1
-                            ? currentResponse.msg.substring(currentResponse.msg.indexOf('###'))
-                            : ''
-                          : currentResponse.msg
-                      "
-                      :options="{
-                        html: true,
-                        linkify: true
-                      }"
-                    />
-                  </div>
-                  <a-skeleton
-                    active
-                    v-if="!dsLoading && (activeIndex < 2 || !currentResponse.msg)"
-                  />
-                  <div v-if="activeIndex >= 5" class="follow">
-                    <div class="follow-btn" style="cursor: pointer">
-                      <!--                     <a-button v-show="activeTab === 'knowledge'" style="margin-right: 10px;float: left" type="link" color="green" size="small" @click="reAnswer">-->
-                      <a-button
-                        style="
-                          margin-right: 35px;
-                          float: left;
-                          font-family: Microsoft YaHei;
-                          font-weight: 400;
-                          font-size: 16px;
-                          color: #465d7c;
-                        "
-                        type="link"
-                        color="green"
-                        size="small"
-                        @click="changeTab(activeTab)"
-                      >
-                        重新生成
-                      </a-button>
-                      <div class="eval-card" style="float: left">
-                        <div
-                          :class="`eval-icon eval-icon-like${evaluate === 'like' ? '-s' : ''}`"
-                          @click="evalResponse('like')"
-                        ></div>
-                        <a-divider type="vertical" />
-                        <div
-                          :class="`eval-icon eval-icon-dislike${
-                            evaluate === 'dislike' ? '-s' : ''
-                          }`"
-                          @click="evalResponse('dislike')"
-                        ></div>
-                        <a-divider type="vertical" />
-                        <a-dropdown :trigger="['click']">
-                          <a class="ant-dropdown-link" @click.prevent>
-                            <div :class="`eval-icon eval-icon-copy`"></div>
-                          </a>
-                          <template #overlay>
-                            <a-menu>
-                              <a-menu-item key="0" @click="copy()"> 复制到剪贴板</a-menu-item>
-                              <a-menu-divider />
-                              <a-menu-item key="1" @click="exportAnswer('doc')">
-                                导出 Word
-                              </a-menu-item>
-                              <a-menu-divider />
-                              <a-menu-item key="2" @click="exportAnswer('pdf')">
-                                导出 PDF
-                              </a-menu-item>
-                            </a-menu>
-                          </template>
-                        </a-dropdown>
-                      </div>
-
-                      <a-button
-                        style="
-                          margin-right: 8px;
-                          background: rgba(33, 133, 242, 0.5);
-                          border-radius: 5px;
-                        "
-                        type="primary"
-                        size="small"
-                        v-show="activeTab === 'knowledge'"
-                        @click="followVisible = !followVisible"
-                      >
-                        追问
-                      </a-button>
-                      <!--                    <a-dropdown :trigger="['click']">-->
-                      <!--                      <a class="ant-dropdown-link" @click.prevent>-->
-                      <!--                        <MoreOutlined />-->
-                      <!--                      </a>-->
-                      <!--                      <template #overlay>-->
-                      <!--                        <a-menu>-->
-                      <!--                          <a-menu-item key="0">-->
-                      <!--                            导出 Word-->
-                      <!--                          </a-menu-item>-->
-                      <!--                          <a-menu-divider />-->
-                      <!--                          <a-menu-item key="1">-->
-                      <!--                            导出 PDF-->
-                      <!--                          </a-menu-item>-->
-                      <!--                        </a-menu>-->
-                      <!--                      </template>-->
-                      <!--                    </a-dropdown>-->
-                    </div>
-                    <div class="follow-input" v-show="followVisible">
-                      <a-textarea
-                        v-model:value="followQuestion"
-                        class="input-text"
-                        v-model="followQuestion"
-                        placeholder="继续提问"
-                      />
-                      <div class="button" @click="followAsk">发送</div>
-                    </div>
-                  </div>
-                  <!--                  <div class="more-questions" v-if="activeTab === 'knowledge' && activeIndex >= 4">-->
-                  <div class="more-questions" v-if="activeIndex >= 4">
-                    <div class="title" style="margin-bottom: 14px">
-                      你可以继续问我:
-                      <div class="change-title" @click="changeRecommendedQuestions">
-                        <div class="change-icon"></div>
-                        换一批
-                      </div>
-                    </div>
-                    <div class="questions" style="cursor: pointer">
-                      <div
-                        class="question"
-                        @click="openRecommendedQuestion(q)"
-                        v-for="(q, i) in questions"
-                        :key="i"
-                        style="
-                          line-height: 14px;
-                          text-align: left;
-                          font-style: normal;
-                          text-transform: none;
-                          width: fit-content;
-                          margin-bottom: 12px;
-                          background: #ffffff;
-                          box-shadow: 0px 1px 8px 0px #e4e4e4;
-                          border-radius: 10px;
-                          border: 1px solid #e9e9e9;
-                          padding: 16px;
-                          font-family: PingFang SC;
-                          font-weight: 500;
-                          font-size: 14px;
-                          color: #2185f2;
-                        "
-                      >
-                        {{ q }}
-                      </div>
-                    </div>
-                  </div>
-                  <div class="source" v-show="activeIndex === 5">
-                    <div class="title">
-                      <div class="icon"></div>
-                      <div class="text">来源</div>
-                      <div class="num">({{ currentResponse.docs.length }})</div>
-                    </div>
-                    <div class="items">
-                      <div
-                        class="item"
-                        v-if="activeTab !== 'net'"
-                        v-for="(doc, i) in currentResponse.docs"
-                        :key="'doc-' + i"
-                      >
-                        <div class="doc">
-                          <p>
-                            <span
-                              class="ma"
-                              style="font-weight: bolder; color: #000000; margin-right: 10px"
-                              >出处 [{{ i + 1 }}] </span
-                            ><span class="doc-link" @click="openDoc(doc, i)">{{ doc.doc }}</span>
-                          </p>
-                          <div
-                            :class="`doc-icon${
-                              !doc.showContent ? ' doc-icon-show' : ' doc-icon-hide'
-                            }`"
-                            @click="doc.showContent = !doc.showContent"
-                          ></div>
-                        </div>
-
-                        <div :class="`content${doc.showContent ? '' : ' content-hide'}`">
-                          <p>{{ doc.content }}</p>
-                        </div>
-                      </div>
-                      <div
-                        class="item item-url"
-                        v-if="activeTab === 'net'"
-                        v-for="(doc, i) in currentResponse.docs"
-                        :key="'doc-' + i"
-                      >
-                        <div class="doc">
-                          <p>
-                            <span class="doc-link" @click="openUrl(doc.link)">{{ doc.title }}</span>
-                          </p>
-                        </div>
-                        <div class="bottom">
-                          <div class="title-icon">
-                            <div class="icon"></div>
-                            <div class="title">{{ doc.doc }}</div>
-                          </div>
-                          <div class="index">{{ i + 1 }}</div>
-                        </div>
-                      </div>
-                    </div>
-                  </div>
-                </div>
-              </template>
-              <template v-else>
-                <div v-if="activeIndex >= 1" class="map-answer">
-                  <div class="left-panel">
-                    <div class="content">
-                      <div class="summary-card">
-                        <div class="summary-title" id="tdscSummaryTitle">总结</div>
-                        <div class="summary-content">
-                          <a-skeleton active v-if="!currentResponse.msg" />
-                          <vue-markdown-it
-                            :source="
-                              currentResponse.streamMock
-                                ? currentResponse.streamMsg
-                                : currentResponse.msg
-                            "
-                            :options="{
-                              html: true,
-                              linkify: true
-                            }"
-                          />
-                          <!--                        {{currentResponse.msg}}-->
-                        </div>
-                      </div>
-                      <div class="chart-card" v-if="currentResponse.hasChart" id="tdscChartCard">
-                        <div class="chart-title">生成图表</div>
-                        <a-skeleton
-                          active
-                          style="height: 100%"
-                          v-if="!currentResponse.chartOption"
-                        />
-                        <div v-else class="chart" id="summaryChart"></div>
-                      </div>
-                    </div>
-                  </div>
-                  <!--                  <div class="map-panel" id="tdscMapPanel">-->
-                  <!--                    <div class="map-title">空间位置</div>-->
-                  <!--                    <div class="map">-->
-                  <!--                      <a-i-map />-->
-                  <!--                    </div>-->
-                  <!--                  </div>-->
-                </div>
-              </template>
-            </div>
-          </div>
-        </div>
-        <!--   历史回答     -->
-        <div
-          v-show="askType === 'zcfg'"
-          class="search-result result-history"
-          style="overflow-y: hidden; height: auto"
-          v-if="questHistories.length > 0"
-          id="messageContainer"
-        >
-          <template v-for="(qh, i) in [...questHistories].reverse()" :key="`qh-${i}`">
-            <div class="top">
-              <div class="title">
-                <div class="icon"></div>
-                <div class="question" style="cursor: pointer">{{ qh.question }}</div>
-              </div>
-            </div>
-
-            <div class="result-panel">
-              <div class="result">
-                <div class="tabs">
-                  <div :class="`tab tab-active`">
-                    <div class="title">回答</div>
-                    <div class="bottom"></div>
-                  </div>
-                </div>
-              </div>
-              <div class="result-view">
-                <div class="q-r">
-                  <vue-markdown-it
-                    :source="qh.msg"
-                    :toc="true"
-                    :options="{
-                      html: true,
-                      linkify: true
-                    }"
-                  />
-                </div>
-              </div>
-            </div>
-          </template>
-        </div>
-      </div>
-      <a-affix v-if="showDoc" style="width: 52%; font-size: 16px" :offset-top="210">
-        <div
-          class="doc"
-          style="width: 100%; background-color: white; height: calc(100vh - 190px); overflow: auto"
-        >
-          <p-d-f-viewer
-            v-if="fileType === 'pdf'"
-            :src="pdfSrc"
-            @close="closeDoc"
-            :content="pdfContent"
-            :num="pdfNum"
-          />
-          <word-viewer
-            v-if="fileType === 'docx'"
-            :src="pdfSrc"
-            @close="closeDoc"
-            :content="pdfContent"
-            :num="pdfNum"
-          >
-          </word-viewer>
-          <txt-viewer v-if="fileType === 'txt'" :src="pdfSrc" @close="closeDoc" :txt="pdfContent" />
-        </div>
-      </a-affix>
-    </div>
-
-    <a-modal force-render class="input-modal" v-model:open="open" :closable="false" width="700px">
-      <template #footer>
-        <div class="footer" style="height: 26px; margin: 0 auto">
-          <div class="left" style="float: left; cursor: pointer" @click="clearModalTextArea">
-            清空
-          </div>
-          <a-button style="float: right" type="primary" size="small" @click="sendModalTextArea">
-            发送
-          </a-button>
-        </div>
-      </template>
-      <div class="modal-que">
-        <ai-textarea :q="question" ref="aiTextAreaRef" @enter="ask" />
-      </div>
-    </a-modal>
-    <affix-card />
-    <div class="footer" style="position: sticky; z-index: 9999">
-      <basic-footer />
-    </div>
-  </div>
-</template>
-<script setup>
-import { fetchEventSource } from '@microsoft/fetch-event-source';
-import PDFViewer from '@/components/pdf/PdfCanvas.vue';
-import WordViewer from '@/components/pdf/WordViewer.vue';
-import AiTextarea from '@/components/TextArea/AiTextarea.vue';
-import { VueMarkdownIt } from '@f3ve/vue-markdown-it';
-import TxtViewer from '@/components/pdf/TxtViewer.vue';
-import { message } from 'ant-design-vue';
-import { ref, defineProps, watch } from 'vue';
-import CommonAPI from '@/api/common';
-import ManagerAPI from '@/api/manager';
-import AIMap from '@/components/Map/AIMap.vue';
-import * as echarts from 'echarts';
-import PubsubService from '@/utils/PubsubService';
-import MarkdownToc from '@/components/markdown-toc/MarkdownToc.vue';
-import AiSearchHeader from '@/views/ai-search/components/AiSearchHeader.vue';
-import AiSearchSubHeader from '@/views/ai-search/components/AiSearchSubHeader.vue';
-import AffixCard from '@/views/components/AffixCard.vue';
-import BasicFooter from '@/layout/footer/BasicFooter.vue';
-import { useUserStore } from '@/stores';
-const { user: u = {}, updateUser } = useUserStore();
-const { user = {}, token } = u || {};
-import UserAPI from '@/api/user';
-const aiLoading = ref(false);
-const router = useRouter();
-const markdownTocAffixRef = ref(null);
-const tocHeight = ref(530);
-import { LoadingOutlined, DownOutlined } from '@ant-design/icons-vue';
-import { h } from 'vue';
-const indicator = h(LoadingOutlined, {
-  style: {
-    fontSize: '24px'
-  },
-  spin: true
-});
-const props = defineProps({
-  searchType: '',
-  askType: {
-    type: String,
-    default: 'zcfg'
-  }
-});
-
-const answerType = ref('0');
-const dsChecked = ref(false);
-const scope = ref('net');
-watch(
-  () => router.currentRoute.value,
-  (value) => {
-    const { query } = value;
-    if (query.q) {
-      ask(decodeURIComponent(query.q));
-      console.log(decodeURIComponent(query.q));
-      answerType.value = query.type || '0';
-      scope.value = query.scope || 'net';
-      var ds = query.ds;
-      dsHintTxt.value = '';
-      if (ds && ds == '1') {
-        ds = true;
-      } else {
-        ds = false;
-      }
-      dsChecked.value = ds;
-    }
-  }
-);
-
-watch(props.searchType, (val) => {
-  activeTab.value = val;
-});
-
-const question = ref('国有土地的使用方式有哪些?');
-const followQuestion = ref('');
-const statusText = ref('检索中');
-const activeIndex = ref(0);
-const activeTab = ref(props.searchType);
-let ctr = null;
-const aiTextAreaRef = ref(null);
-const searchRef = ref(null);
-
-const evaluate = ref(null);
-
-const scrollToBottom = () => {
-  const messageContainer = document.getElementById('messageContainer');
-  if (messageContainer && searchRef.value) {
-  }
-};
-
-const clearModalTextArea = () => {
-  aiTextAreaRef.value.clear();
-};
-
-const sendModalTextArea = () => {
-  aiTextAreaRef.value.send();
-};
-
-const tocs = ref([]);
-const open = ref(false);
-const showDoc = ref(false);
-const pdfSrc = ref('');
-const pdfContent = ref('');
-const pdfNum = ref(1);
-const fileType = ref('pdf');
-const dsLoading = ref(true);
-const startTime = ref(0);
-const endTime = ref(0);
-const dsHintTxt = ref('');
-const dsUp = ref(false);
-const times = ref(0);
-const timers = ref([]);
-const currentResponse = ref({
-  streamMsg: '',
-  streamMock: false,
-  msg: '',
-  originAnswer: '',
-  docs: []
-});
-let streamMockInterval = null;
-const streamToAnswer = () => {
-  console.log('streamToAnswer');
-  currentResponse.value.index = 0;
-  streamMockInterval = setInterval(() => {
-    const { originAnswer = '', msg, streamMsg = '', id, index = 0 } = currentResponse.value;
-
-    if (currentResponse.value.mockStart || index <= originAnswer.length) {
-      currentResponse.value.streamMsg = originAnswer.substr(0, index + 2).replaceAll('\n', '  \n');
-      if (originAnswer) {
-        currentResponse.value.index += 2;
-      }
-      let num = getNum(currentResponse.value.streamMsg);
-
-      while (num) {
-        const docsNum = currentResponse.value.docs.length;
-        if (docsNum && num > docsNum + 1) {
-        }
-
-        currentResponse.value.streamMsg = currentResponse.value.streamMsg.replace(
-          `[[${num}]]`,
-          `<span onclick="window.openDocByIndex(${num}, ${id})" class="poi" style="    cursor: pointer; display: inline-block; width: 20px; height: 20px; font-size: 12px; line-height: 20px; text-align: center; margin: 0 5px; border-radius: 10px;background: #d0d5dd;    width: 20px;
-    height: 20px;
-    background: #FFFFFF;
-    border-radius: 4px 4px 4px 4px;
-    border: 1px solid #BACAE3;">${num}</span>`
-        );
-
-        num = getNum(currentResponse.value.streamMsg);
-      }
-    } else {
-      if (streamMockInterval) {
-        clearInterval(streamMockInterval);
-        streamMockInterval = null;
-      }
-
-      activeIndex.value = 5;
-
-      searchRef.value.scrollTop = 0;
-      generateToc();
-      console.log('mock 结束');
-    }
-  }, 50);
-};
-
-const tabs = [
-  { key: 'knowledge', name: '知识库' },
-  { key: 'paper', name: '学术' },
-  { key: 'net', name: '全网' }
-];
-
-const steps = ref([
-  {
-    key: 0,
-    name: '问题分析',
-    tags: [],
-    prog: 0
-  },
-  {
-    key: 1,
-    name: '知识搜索',
-    tags: [],
-    prog: 0
-  },
-  {
-    key: 2,
-    name: '整理答案',
-    tags: [],
-    prog: 0
-  },
-  {
-    key: 3,
-    name: '回答完成',
-    tags: [],
-    prog: 0
-  }
-]);
-
-const doStep = () => {
-  setInterval(() => {
-    steps.value.forEach((v) => {
-      if (v.key < activeIndex.value) {
-        v.prog = 100;
-      } else if (v.key === activeIndex.value) {
-        if (v.prog + 1 === 100) {
-        } else {
-          v.prog += 1;
-        }
-      } else {
-        v.prog = 0;
-      }
-    });
-  }, 100);
-};
-
-// 相关案例关键词
-const caseKeyWords = ref([]);
-
-const setActiveIndexTags = (index, tags) => {
-  steps.value[index].tags = [];
-  // 把问题分析部分的关键词带出去
-  if (index == 0 && tags.length > 0) {
-    caseKeyWords.value = tags;
-  }
-  for (let i = 0; i < tags.length; i++) {
-    setTimeout(function () {
-      steps.value[index].tags = tags.slice(0, i + 1);
-    }, 500 * (i + 1));
-  }
-};
-
-const evalResponse = (evalType) => {
-  // ManagerAPI.feedback(currentResponse.value.logId, evalType === 'like' ? 1 : 2).then((res) => {
-  //   evaluate.value = evalType;
-  //   message.info('感谢你的反馈!');
-  // });
-};
-
-const changeStatusText = () => {
-  let i = 0;
-  setInterval(() => {
-    statusText.value = '检索中' + '.'.repeat(i);
-
-    if (i === 3) {
-      i = 0;
-    }
-    i++;
-  }, 500);
-};
-changeStatusText();
-const askType = ref('zcfg');
-const search = (q, type = 'zcfg') => {
-  question.value = q;
-  askType.value = type;
-  if (ctr) {
-    ctr.abort();
-    ctr = null;
-    if (streamMockInterval) {
-      clearInterval(streamMockInterval);
-      streamMockInterval = null;
-    }
-  }
-  if (type === 'zcfg') {
-    quest();
-  } else {
-    questLandMark();
-  }
-};
-
-const graph = ref(false);
-
-const questLandMark = async () => {
-  activeIndex.value = 0;
-  tocs.value = [];
-  steps.value = [
-    {
-      key: 0,
-      name: '问题分析',
-      tags: [],
-      prog: 0
-    },
-    {
-      key: 1,
-      name: '任务拆解',
-      tags: [],
-      prog: 0
-    },
-    {
-      key: 2,
-      name: 'Agent调用',
-      tags: [],
-      prog: 0
-    },
-    {
-      key: 3,
-      name: '回答完成',
-      tags: [],
-      prog: 0
-    }
-  ];
-
-  currentResponse.value = {
-    question: question.value,
-    id: 1,
-    msg: '',
-    originAnswer: '',
-    docs: [],
-    msgFinished: false,
-    hasChart: false
-  };
-  await getQuestionKeyWords();
-  ctr = new AbortController();
-  await fetchEventSource(window.AppGlobalConfig.landMarketUrl, {
-    method: 'POST',
-    openWhenHidden: true,
-    // redirect: 'follow',
-    headers: {
-      'Content-Type': 'application/json'
-    },
-    body: JSON.stringify({
-      data: question.value + (graph.value ? ',需要生成图表' : ',不需要生成图表')
-    }),
-    // mode: 'no-cors',
-    signal: ctr.signal,
-    async onmessage(msg) {
-      const { data } = msg;
-
-      if (!['', '[FINISH]', '[DONE]'].includes(data)) {
-        try {
-          currentResponse.value.originAnswer = data;
-          const response = JSON.parse(data);
-
-          if (response) {
-            const { agent_responses = [] } = response;
-
-            for (let agent of agent_responses) {
-              // 问题分析
-              if (agent.agent_name === 'plan_dispatcher' && activeIndex.value === 0) {
-                if (
-                  agent.choices.length > 0 &&
-                  agent.choices.filter((v) => v.role === 'assistant').length > 0
-                ) {
-                  const chio = agent.choices.filter((v) => v.role === 'assistant')[0];
-                  if (chio.finished) {
-                    activeIndex.value = 1;
-                    setActiveIndexTags(0, [chio.content]);
-                  }
-                }
-              }
-
-              if (agent.agent_name === 'land_supply_planner' && activeIndex.value === 1) {
-                if (
-                  agent.choices.length > 0 &&
-                  agent.choices.filter((v) => v.role === 'assistant').length > 0
-                ) {
-                  const chio = agent.choices.filter((v) => v.role === 'assistant')[0];
-
-                  if (chio.finished) {
-                    activeIndex.value = 2;
-                    setActiveIndexTags(1, [chio.content]);
-                    if (chio.content.includes('generate_chart')) {
-                      currentResponse.value.hasChart = true;
-                    }
-                  }
-                }
-              }
-
-              if (
-                ['generate_chart', 'LandSupplySqlAgent', 'summary'].includes(agent.agent_name) &&
-                activeIndex.value === 2
-              ) {
-                if (
-                  agent.choices.length > 0 &&
-                  agent.choices.filter((v) => v.role === 'assistant').length > 0
-                ) {
-                  const chio = agent.choices.filter((v) => v.role === 'assistant')[0];
-
-                  if (chio.finished && steps.value[2].agent !== agent.agent_name) {
-                    steps.value[2].agent = agent.agent_name;
-                    setActiveIndexTags(2, [chio.content]);
-                  }
-                }
-              }
-
-              if (agent.agent_name === 'summary') {
-                steps.value[2].agent = 'summary';
-                if (activeIndex.value === 2) {
-                  activeIndex.value = 3;
-                }
-                if (agent.choices.length === 3) {
-                  if (!currentResponse.value.msgFinished) {
-                    currentResponse.value.msg = agent.choices[2].content;
-                    currentResponse.value.msgFinished = agent.choices[2].finished;
-                  }
-                }
-              }
-
-              if (agent.agent_name === 'generate_chart') {
-                currentResponse.value.hasChart = true;
-
-                if (agent.executed) {
-                  const option = JSON.parse(
-                    agent.exec_res.replace('```json', '').replace('```', '').trim()
-                  );
-
-                  currentResponse.value.chartOption = option;
-                  setTimeout(() => {
-                    initChart(option);
-                  }, 1000);
-                }
-              }
-            }
-          }
-        } catch (e) {
-          console.error(e, data);
-        }
-      }
-
-      if (data === '[FINISH]') {
-        activeIndex.value = 5;
-      }
-    },
-    onclose() {
-      activeIndex.value = 5;
-      generateToc();
-      collectQuestion();
-    },
-    onerror(err) {
-      throw err;
-    },
-    onopen() {}
-  });
-};
-
-const openAskModal = () => {
-  open.value = true;
-  nextTick(() => {
-    if (aiTextAreaRef.value) {
-      aiTextAreaRef.value.setText(question.value);
-    }
-  });
-};
-
-const ask = async (q, isFllow) => {
-  if (ctr) {
-    if (streamMockInterval) {
-      clearInterval(streamMockInterval);
-      streamMockInterval = null;
-    }
-    ctr.abort();
-  }
-  times.value = 0;
-  if (timers.value) {
-    timers.value.forEach((t) => {
-      clearTimeout(t);
-      t = null;
-    });
-  }
-  timers.value = [];
-  if (dsChecked.value) {
-    dsHintTxt.value = '';
-    dsLoading.value = true;
-  }
-  question.value = q;
-  open.value = false;
-  showDoc.value = false;
-  askType.value = 'zcfg';
-  // activeTab.value = 'knowledge';
-  quest(isFllow);
-};
-
-const followAsk = () => {
-  ask(followQuestion.value, true);
-  followVisible.value = false;
-  followQuestion.value = '';
-  showDoc.value = false;
-};
-
-let questionUrl = '/chat/kb_chat';
-
-const changeTab = (tab) => {
-  if (!dsChecked.value) {
-    dsLoading.value = false;
-    dsHintTxt.value = '';
-  } else {
-    dsLoading.value = true;
-    dsHintTxt.value = '';
-  }
-  times.value = 0;
-  if (timers.value) {
-    timers.value.forEach((t) => {
-      clearTimeout(t);
-      t = null;
-    });
-  }
-  timers.value = [];
-  if (tab === 'net') {
-    questionUrl = '/chat/bing_chat';
-  } else {
-    questionUrl = '/chat/kb_chat';
-  }
-
-  activeTab.value = tab;
-  if (ctr) {
-    if (streamMockInterval) {
-      clearInterval(streamMockInterval);
-      streamMockInterval = null;
-    }
-    ctr.abort();
-  }
-  quest();
-};
-const onChange = () => {
-  dsChange();
-  changeAnswerType();
-};
-//ds绑定用户改变
-const dsChange = () => {
-  if (useUserStore().isLogin) {
-    var ds = '0';
-    if (dsChecked.value) {
-      ds = '1';
-    }
-    UserAPI.changeDeepseek(ds).then((res) => {
-      if (res?.success) {
-        user.enableDeepseek = parseInt(ds);
-        updateUser({ user, token });
-      }
-    });
-  }
-  if (timers.value) {
-    timers.value.forEach((t) => {
-      clearTimeout(t);
-      t = null;
-    });
-    timers.value = [];
-  }
-  times.value = 0;
-  dsHintTxt.value = '';
-  //打字机效果 切换会打印
-  aiLoading.value = true;
-};
-const changeAnswerType = () => {
-  if (!dsChecked.value) {
-    dsLoading.value = false;
-    dsHintTxt.value = '';
-  } else {
-    dsLoading.value = true;
-    dsHintTxt.value = '';
-  }
-  if (ctr) {
-    if (streamMockInterval) {
-      clearInterval(streamMockInterval);
-      streamMockInterval = null;
-    }
-    ctr.abort();
-  }
-  quest();
-};
-
-const questHistories = ref([]);
-
-const followVisible = ref(false);
-let scb = null;
-const quest = async (isFllow) => {
-  startTime.value = Date.now();
-  window.scroll({ top: 0, behavior: 'smooth' });
-  tocs.value = [];
-  question.value = (isFllow ? '追问: ' : '') + question.value;
-  evaluate.value = null;
-  activeIndex.value = 0;
-  steps.value = [
-    {
-      key: 0,
-      name: '问题分析',
-      tags: [],
-      prog: 0
-    },
-    {
-      key: 1,
-      name: '知识搜索',
-      tags: [],
-      prog: 0
-    },
-    {
-      key: 2,
-      name: '整理答案',
-      tags: [],
-      prog: 0
-    },
-    {
-      key: 3,
-      name: '正在生成答案...',
-      tags: [],
-      prog: 0
-    }
-  ];
-
-  if (!isFllow) {
-    showDoc.value = false;
-  }
-  searchRef.value.scrollTop = 0;
-  if (isFllow) {
-    const { id, question, msg, docs, originAnswer, keywords = [] } = currentResponse.value;
-    questHistories.value.push({ id, question, msg, docs: docs, originAnswer, keywords });
-  } else {
-    questHistories.value = [];
-  }
-  aiLoading.value = true;
-  if (scb !== null) {
-    clearInterval(scb);
-    scb = null;
-  } else {
-    scb = setInterval(() => {
-      scrollToBottom();
-    }, 500);
-  }
-  ctr = new AbortController();
-
-  const id = questHistories.value.length;
-  currentResponse.value = {
-    question: question.value,
-    id,
-    msg: '',
-    originAnswer: '',
-    streamMock: false,
-    streamMsg: '',
-    docs: []
-  };
-  activeIndex.value = 0;
-  await getQuestionKeyWords();
-
-  if (activeTab.value === 'net') {
-    questionUrl = '/chat/bing_chat';
-  } else {
-    questionUrl = '/chat/kb_chat';
-  }
-  getRecommendedQuestion();
-
-  const topKs = window?.AppGlobalConfig?.topKs || {
-    0: 5,
-    1: 10,
-    2: 15
-  };
-
-  const body =
-    activeTab.value === 'net'
-      ? {
-          query: question.value,
-          stream: true,
-          model: dsChecked.value ? 'deepseek-r1' : '',
-          search_type: answerType.value
-        }
-      : {
-          query: question.value,
-          mode: 'local_kb',
-          kb_name: activeTab.value === 'paper' ? 'compose_paper_material_total' : 'policy',
-          top_k: topKs[answerType.value],
-          search_type: answerType.value,
-          score_threshold: 0.5,
-          model: dsChecked.value ? 'deepseek-r1' : '',
-          history: isFllow ? getFlowHistory() : [],
-          stream: true,
-          prompt_name: 'rag_context_qa.md',
-          return_direct: false
-        };
-
-  if (activeTab.value === 'knowledge' && isFllow) {
-    if (questHistories.value.length > 0) {
-      body.history_keyword = questHistories.value[questHistories.value.length - 1].keywords;
-    }
-  }
-  if (activeTab.value !== 'net' && answerType.value !== '0') {
-    currentResponse.value.streamMock = true;
-    currentResponse.value.mockStart = true;
-    streamToAnswer();
-  }
-
-  await fetchEventSource(window.AppGlobalConfig.knowledgeServer + questionUrl, {
-    method: 'POST',
-    openWhenHidden: true,
-    headers: {
-      'Content-Type': 'application/json'
-    },
-
-    body: JSON.stringify(body),
-    signal: ctr.signal,
-    async onmessage(msg) {
-      if (activeIndex.value !== 3) {
-        activeIndex.value = 3;
-        setActiveIndexTags(2, ['知识整理', '要点归纳', '总结摘要']);
-      }
-
-      activeTab.value === 'net' ? handleNetResponse(msg, id) : handleKnowledgeResponse(msg, id);
-    },
-    onclose() {
-      if (scb !== null) {
-        clearInterval(scb);
-        scb = null;
-
-        collectQuestion();
-      }
-
-      if (currentResponse.value.streamMock) {
-        currentResponse.value.mockStart = false;
-      } else {
-        activeIndex.value = 5;
-
-        searchRef.value.scrollTop = 0;
-        generateToc();
-      }
-    },
-    onerror(err) {
-      throw err;
-    },
-    onopen() {
-      setActiveIndexTags(1, ['国家法律法规', '地方性法规', '政府规章', '政策解读', '经典案例']);
-    }
-  });
-};
-
-const getFlowHistory = () => {
-  const lastHistory = [...questHistories.value].splice(-1);
-  const parm = [];
-  lastHistory.forEach((item) => {
-    parm.push({ role: 'user', content: item.question });
-  });
-
-  return parm;
-};
-
-const handleKnowledgeResponse = (msg, id) => {
-  if (!msg || !msg.data) {
-    return;
-  }
-  const rData = JSON.parse(msg.data);
-  if (rData?.choices && rData.choices.length > 0) {
-    if (activeTab.value === 'net' && rData.status !== 2) {
-      return;
-    }
-    if (rData.status == 3) {
-      if (timers.value) {
-        timers.value.forEach((t) => {
-          clearTimeout(t);
-          t = null;
-        });
-        timers.value = [];
-      }
-      if (dsChecked.value) {
-        endTime.value = Date.now();
-        var time = ((endTime.value - startTime.value) / 1000).toFixed(0);
-        dsHintTxt.value = `已深度思考(用时 ${time} 秒)`;
-        dsLoading.value = false;
-        aiLoading.value=true;
-      }
-        currentResponse.value.originAnswer = rData.choices[0]?.delta?.content.replaceAll(
-          '\n',
-          `  \n`
-        );
-        currentResponse.value.msg = rData.choices[0]?.delta?.content.replaceAll('\n', `  \n`);
-        let num = getNum(currentResponse.value.msg);
-        while (num) {
-          const docsNum = currentResponse.value.docs.length;
-          if (docsNum && num > docsNum + 1) {
-          }
-
-          currentResponse.value.msg = currentResponse.value.msg.replace(
-            `[[${num}]]`,
-            `<span onclick="window.openDocByIndex(${num}, ${id})" class="poi" style="    cursor: pointer; display: inline-block; width: 20px; height: 20px; font-size: 12px; line-height: 20px; text-align: center; margin: 0 5px; border-radius: 10px;background: #d0d5dd;    width: 20px;
-    height: 20px;
-    background: #FFFFFF;
-    border-radius: 4px 4px 4px 4px;
-    border: 1px solid #BACAE3;">${num}</span>`
-          );
-
-          num = getNum(currentResponse.value.msg);
-        }
-    } else {
-      aiLoading.value = false;
-      if (dsChecked.value) {
-        dsHintTxt.value = '思考中...';
-        //ds模式打字机效果输出
-        const timer = setTimeout(() => {
-          if (!aiLoading.value) {
-            currentResponse.value.originAnswer += rData.choices[0]?.delta?.content.replaceAll(
-              '\n',
-              `  \n`
-            );
-            currentResponse.value.msg += rData.choices[0]?.delta?.content.replaceAll('\n', `  \n`);
-            let num = getNum(currentResponse.value.msg);
-            while (num) {
-              const docsNum = currentResponse.value.docs.length;
-              if (docsNum && num > docsNum + 1) {
-              }
-
-              currentResponse.value.msg = currentResponse.value.msg.replace(
-                `[[${num}]]`,
-                `<span onclick="window.openDocByIndex(${num}, ${id})" class="poi" style="    cursor: pointer; display: inline-block; width: 20px; height: 20px; font-size: 12px; line-height: 20px; text-align: center; margin: 0 5px; border-radius: 10px;background: #d0d5dd;    width: 20px;
-    height: 20px;
-    background: #FFFFFF;
-    border-radius: 4px 4px 4px 4px;
-    border: 1px solid #BACAE3;">${num}</span>`
-              );
-
-              num = getNum(currentResponse.value.msg);
-            }
-          }
-          clearTimeout(timer);
-          timers.value.push(timer);
-        }, times.value * 15);
-      } else {
-        currentResponse.value.originAnswer += rData.choices[0]?.delta?.content.replaceAll(
-          '\n',
-          `  \n`
-        );
-        currentResponse.value.msg += rData.choices[0]?.delta?.content.replaceAll('\n', `  \n`);
-        let num = getNum(currentResponse.value.msg);
-        while (num) {
-          const docsNum = currentResponse.value.docs.length;
-          if (docsNum && num > docsNum + 1) {
-          }
-
-          currentResponse.value.msg = currentResponse.value.msg.replace(
-            `[[${num}]]`,
-            `<span onclick="window.openDocByIndex(${num}, ${id})" class="poi" style="    cursor: pointer; display: inline-block; width: 20px; height: 20px; font-size: 12px; line-height: 20px; text-align: center; margin: 0 5px; border-radius: 10px;background: #d0d5dd;    width: 20px;
-    height: 20px;
-    background: #FFFFFF;
-    border-radius: 4px 4px 4px 4px;
-    border: 1px solid #BACAE3;">${num}</span>`
-          );
-
-          num = getNum(currentResponse.value.msg);
-        }
-      }
-
-      // currentResponse.value.msg += rData.choices[0]?.delta?.content.replaceAll('\n', `  \n`);
-    }
-  }
-
-  if (!currentResponse.value.docs.length) {
-    if (rData.docs && rData.docs.length) {
-      handleDocs(rData.docs);
-    }
-  }
-  times.value++;
-};
-
-const handleNetResponse = (msg, id) => {
-  const rData = msg.data;
-  if (!!rData && rData !== '[DONE]') {
-    try {
-      const res = JSON.parse(rData);
-      if (res.error) {
-        activeIndex.value = 4;
-        message.error(res.error);
-        currentResponse.value.msg = res.error;
-
-        return;
-      }
-      if (res.rag_finish) {
-        if (activeIndex.value < 4) {
-          activeIndex.value = 4;
-        }
-        if (dsChecked.value) {
-          endTime.value = Date.now();
-          var time = ((endTime.value - startTime.value) / 1000).toFixed(0);
-          dsHintTxt.value = `已深度思考(用时 ${time} 秒)`;
-          dsLoading.value = false;
-        }
-      } else {
-        if (dsChecked.value && dsHintTxt.value != '思考中...') {
-          dsHintTxt.value = '思考中...';
-        }
-      }
-      if (res.result) {
-        currentResponse.value.originAnswer = res.result;
-        currentResponse.value.msg = res.result.replaceAll('\n', `  \n`);
-
-        let num = getNum(currentResponse.value.msg);
-
-        while (num) {
-          const docsNum = currentResponse.value.docs.length;
-          if (docsNum && num > docsNum + 1) {
-            currentResponse.value.msg = currentResponse.value.msg.replace(`[[${num}]]`, ``);
-          }
-          currentResponse.value.msg = currentResponse.value.msg.replace(
-            `[[${num}]]`,
-            `<span onclick="window.openDocByIndex(${num}, ${id})" class="poi" style="    cursor: pointer; display: inline-block; width: 20px; height: 20px; font-size: 12px; line-height: 20px; text-align: center; margin: 0 5px; border-radius: 10px;background: #d0d5dd">${num}</span>`
-          );
-
-          num = getNum(currentResponse.value.msg);
-        }
-      }
-      if (res.source_list && res.source_list.length && !currentResponse.value.docs.length) {
-        handleDocs(res.source_list);
-      }
-    } catch (e) {}
-  }
-
-  if (rData === '[DONE]') {
-    console.log(currentResponse.value.msg);
-  }
-};
-
-const handleDocs = (docs) => {
-  if (
-    docs.length === 1 &&
-    "<span style='color:red'>未找到相关文档,该回答为大模型自身能力解答!</span>" === docs[0]
-  ) {
-    // message.warn("知识库中未收录相关内容,将为您自动切换全网模式,请稍候......");
-    // changeTab('net');
-    return;
-  }
-
-  currentResponse.value.docs = docs.map((v, i) => {
-    if (activeTab.value === 'net') {
-      return {
-        index: v.num,
-        doc: v.name,
-        link: v.url,
-        title: v.title,
-        summary: v.summary,
-        content: '',
-        showContent: false,
-        type: 'url'
-      };
-    }
-
-    if (v.indexOf('.pdf') > 0) {
-      return {
-        index: i++,
-        doc: v.substring(v.indexOf('] [') + 3, v.indexOf('.pdf]') + 4),
-        link: v.substring(v.indexOf('.pdf]') + 6, v.indexOf('.pdf)') + 4),
-        content: v.substring(v.indexOf('.pdf)') + 5),
-        showContent: false,
-        type: 'pdf'
-      };
-    } else if (v.indexOf('.txt') > 0) {
-      return {
-        index: i++,
-        doc: v.substring(v.indexOf('] [') + 3, v.indexOf('.txt]') + 4),
-        link: v.substring(v.indexOf('.txt]') + 6, v.indexOf('.txt)') + 4),
-        content: v.substring(v.indexOf('.txt)') + 5),
-        showContent: false,
-        type: 'txt'
-      };
-    } else if (v.indexOf('.docx') > 0) {
-      return {
-        index: i++,
-        doc: v.substring(v.indexOf('] [') + 3, v.indexOf('.docx]') + 5),
-        link: v.substring(v.indexOf('.docx]') + 7, v.indexOf('.docx)') + 5),
-        content: v.substring(v.indexOf('.docx)') + 6),
-        showContent: false,
-        type: 'docx'
-      };
-    }
-  });
-};
-
-const openDoc = (doc, i) => {
-  var link = doc.link;
-  var type = doc.type;
-  pdfSrc.value = link.replace(
-    window.AppGlobalConfig.knowledgeDocUrl.replace(
-      '=policy&',
-      activeTab.value === 'paper' ? '=compose_paper_material_total&' : '=policy&'
-    ),
-    window.AppGlobalConfig.knowledgeDocUrlProxy +
-      (activeTab.value === 'paper' ? 'compose_paper_material_total/' : 'policy/')
-  );
-  showDoc.value = true;
-  fileType.value = type;
-  pdfContent.value = doc.content;
-  pdfNum.value = i;
-};
-
-const closeDoc = () => {
-  showDoc.value = false;
-};
-
-watch(
-  () => showDoc.value,
-  (newVal) => {
-    // 发布打开关闭,在关闭文档的时候打开相关案例
-    PubsubService.publish('switch-relevant-cases-box', newVal);
-  }
-);
-
-const openUrl = (url) => {
-  window.open(url, '_blank');
-};
-
-const openDocByIndex = (ind, id) => {
-  showDoc.value = false;
-  let link = null;
-  if (id !== currentResponse.value.id) {
-    if (questHistories.value[id].docs[ind - 1].type === 'url') {
-      openUrl(questHistories.value[id].docs[ind - 1].link);
-      return;
-    }
-
-    link = questHistories.value[id].docs[ind - 1].link;
-    fileType.value = questHistories.value[id].docs[ind - 1].type;
-  } else {
-    if (currentResponse.value.docs[ind - 1].type === 'url') {
-      openUrl(currentResponse.value.docs[ind - 1].link);
-      return;
-    }
-    link = currentResponse.value.docs[ind - 1].link;
-    fileType.value = currentResponse.value.docs[ind - 1].type;
-    pdfContent.value = currentResponse.value.docs[ind - 1].content;
-    pdfNum.value = currentResponse.value.docs[ind - 1].num;
-  }
-  pdfNum.value = ind;
-  pdfSrc.value = link.replace(
-    window.AppGlobalConfig.knowledgeDocUrl.replace(
-      '=policy&',
-      activeTab.value === 'paper' ? '=compose_paper_material_total&' : '=policy&'
-    ),
-    window.AppGlobalConfig.knowledgeDocUrlProxy +
-      (activeTab.value === 'paper' ? 'compose_paper_material_total/' : 'policy/')
-  );
-
-  showDoc.value = true;
-};
-window.openDocByIndex = openDocByIndex;
-const getNum = (str) => {
-  const matches = str.match(/\[\[(\d+)\]\]/);
-
-  if (matches) {
-    return matches[1]; // 输出: 2
-  } else {
-    return null;
-  }
-};
-
-// 埋点采集数据
-const collectQuestion = () => {
-  const { question, originAnswer, keywords = [] } = currentResponse.value;
-  const userStore = useUserStore();
-  const param = {
-    question,
-    answer: originAnswer,
-    questionType: askType.value === 'zcfg' ? '政策法规' : '土地市场',
-    keywords: Array.isArray(keywords) ? keywords.join(',') : keywords,
-    user: '游客',
-    userId: '-1'
-  };
-  if (userStore.isLogin) {
-    const { id = '-1', displayName = '游客' } = userStore?.user?.user || {};
-    param.user = displayName;
-    param.userId = id;
-  }
-
-  // ManagerAPI.collect(param).then((res) => {
-  //   if (res.data) {
-  //     // 记录日志,用来反馈
-  //     currentResponse.value.logId = res.data;
-  //   }
-  // });
-};
-
-const questions = ref([]);
-let recommendedQuestions = [];
-let recommendedQuestionsIndex = 0;
-const getRecommendedQuestion = async (b = false) => {
-  const { question } = currentResponse.value;
-  const myHeaders = new Headers();
-  myHeaders.append('Content-Type', 'application/json');
-  questions.value = [];
-  if (!b) {
-    recommendedQuestions = [];
-  }
-  recommendedQuestionsIndex = 0;
-  const raw = JSON.stringify({
-    messages: [
-      {
-        content: `你会收到一个用户提问,请根据问题延伸出10个子问题。
-
-在你回答问题的时候,还需要注意推荐给用户的问题必须以列表的形式返回。
-for example:
-[
-    "1、南京市在推进产业用地高质量利用方面采取了哪些具体措施?这些措施的效果如何?",
-    "2、杭州市创新型产业用地管理的具体实施细则是什么?这些细则如何促进传统产业转型升级?",
-    "3、南京市和杭州市在土地供应方式上有哪些不同?这些不同如何影响各自的产业发展?",
-    "4、南京市如何通过政策支持和激励措施吸引重大投资项目?这些措施的实际效果如何?",
-    "5、杭州市“工业上楼”项目的实施情况如何?这一政策对提升土地利用效率有何影响?",
-]
-
-以下是用户提问:${question}`,
-        role: 'user'
-      }
-    ],
-    // model: 'qwen1.5-chat',
-    model: 'qwen2.5-instruct',
-    stream: false
-  });
-
-  const requestOptions = {
-    method: 'POST',
-    headers: myHeaders,
-    body: raw,
-    redirect: 'follow'
-  };
-
-  fetch(window.AppGlobalConfig.knowledgeServer + '/chat/chat/completions', requestOptions)
-    .then((response) => response.json())
-    .then((msg) => {
-      const str = msg.choices[0]?.message?.content;
-      if (str) {
-        recommendedQuestions = JSON.parse(str);
-        questions.value = recommendedQuestions.slice(0, 5);
-      }
-    })
-    .catch((error) => console.error(error));
-};
-
-const changeRecommendedQuestions = () => {
-  recommendedQuestionsIndex++;
-  if (recommendedQuestionsIndex % 2 === 0) {
-    questions.value = recommendedQuestions.slice(0, 5);
-  } else {
-    questions.value = recommendedQuestions.slice(5);
-    // getRecommendedQuestion(true)
-  }
-};
-
-const getQuestionKeyWords = async () => {
-  const { question } = currentResponse.value;
-  const myHeaders = new Headers();
-  myHeaders.append('Content-Type', 'application/json');
-  questions.value = [];
-  const raw = JSON.stringify({
-    messages: [
-      {
-        // 精确关键词提取叙述
-        content: `
-        请从以下文本中提取核心关键词,确保关键词简洁明了且准确反映文本的主要内容。
-        请按照以下格式输出:
-        关键词:关键词1,关键词2。
-        文本如下:“${question}”`,
-        role: 'user'
-      }
-    ],
-    // model: 'qwen1.5-chat',
-    model: 'qwen2.5-instruct',
-    stream: false
-  });
-
-  const requestOptions = {
-    method: 'POST',
-    headers: myHeaders,
-    body: raw,
-    redirect: 'follow'
-  };
-
-  return fetch(window.AppGlobalConfig.knowledgeServer + '/chat/chat/completions', requestOptions)
-    .then((response) => response.json())
-    .then((msg) => {
-      activeIndex.value = 1;
-      const str = msg.choices[0]?.message?.content;
-      if (str) {
-        // steps.value[0].tags = str.split('\n\n').replaceAll('核心词为:', '').replaceAll("。", "").map(q => {
-        //
-        //   return q.substring(q.indexOf('.') + 1, q.length)
-        // }).splice(0,3);
-        const keywords = splitWords(str).slice(0, 3);
-        setActiveIndexTags(0, keywords);
-        currentResponse.value.keywords = keywords;
-      }
-    })
-    .catch((error) => console.error(error));
-};
-
-const splitWords = (word) => {
-  if (word) {
-    word = word
-      .replaceAll('核心词为:', '')
-      .replaceAll('问题核心词:', '')
-      .replaceAll('。', '')
-      .trim();
-
-    if (word.indexOf('\n\n') !== -1) {
-      return word.split('\n\n');
-    }
-
-    if (word.indexOf(',') !== -1) {
-      return word.split(',');
-    }
-
-    if (word.indexOf('、') !== -1) {
-      return word.split('、');
-    }
-
-    if (word.indexOf(',') !== -1) {
-      return word.split(',');
-    }
-
-    return [word];
-  }
-
-  return [];
-};
-
-const exportAnswer = (type) => {
-  const { question, originAnswer } = currentResponse.value;
-  CommonAPI.answerExport(type, { question, answer: originAnswer });
-};
-
-const copy = async () => {
-  try {
-    const input = document.createElement('input');
-    input.value = currentResponse.value.msg;
-    document.body.appendChild(input);
-    input.select();
-    document.execCommand('copy');
-    document.body.removeChild(input);
-
-    message.info('内容已复制到剪贴板');
-  } catch (err) {
-    console.error(err);
-    // console.error('复制到剪贴板失败', err);
-  }
-};
-
-const changeActiveTab = (tab) => {
-  activeTab.value = tab;
-};
-
-const stopAI = () => {
-  if (ctr) {
-    ctr.abort();
-  }
-};
-
-const openRecommendedQuestion = (q) => {
-  if (q) {
-    if (q.charAt(1) === '、') {
-      q = q.substring(2);
-    }
-    window.open(
-      `/#/ai-search?q=${encodeURI(q.trim())}&scope=${activeTab.value}&type=${answerType.value}`,
-      '_blank'
-    );
-  }
-};
-
-function initChart(option = {}) {
-  let chart = echarts.init(document.getElementById('summaryChart'));
-  if (option.option) {
-    chart.setOption(option.option);
-  } else {
-    chart.setOption(option);
-  }
-}
-
-const generateToc = () => {
-  if (askType.value === 'tdsc') {
-    const summaryTitle = document.getElementById('tdscSummaryTitle');
-    const chartCard = document.getElementById('tdscChartCard');
-    const mapPanel = document.getElementById('tdscMapPanel');
-
-    tocs.value = [];
-    const offset = 550 + 210;
-    tocs.value.push({
-      id: 'tdscSummaryTitle',
-      children: [],
-      text: '总结',
-      top: summaryTitle.offsetTop + offset
-    });
-
-    if (chartCard.style.display !== 'none') {
-      tocs.value.push({
-        id: 'tdscChartCard',
-        children: [],
-        text: '生成图表',
-        top: chartCard.offsetTop + offset
-      });
-    }
-
-    // tocs.value.push({
-    //   id: 'tdscMapPanel',
-    //   children: [],
-    //   text: '空间位置',
-    //   top: mapPanel.offsetTop + offset
-    // });
-    return;
-  }
-  const markdowns = document.getElementById('resMarkdown').children;
-  let index = 1;
-  let children = [];
-  let toc = [];
-  for (const markdown of markdowns) {
-    if (markdown.tagName.length === 2 && markdown.tagName.toLocaleUpperCase().startsWith('H')) {
-      const id = 'title' + index++ + new Date().getTime();
-      markdown.setAttribute('id', id);
-      const tocItem = {
-        id,
-        text: markdown.innerText.trim(),
-        children: [],
-        top: markdown.offsetTop
-      };
-      children = tocItem.children;
-      toc.push(tocItem);
-    }
-    if (markdown.tagName.toLocaleUpperCase().trim() === 'P') {
-      const pChildren = markdown.children;
-      for (const pChild of pChildren) {
-        if (pChild.tagName.toLocaleUpperCase().trim() === 'STRONG') {
-          const id = 'strong' + index++ + new Date().getTime();
-          pChild.setAttribute('id', id);
-          const strongItem = {
-            id,
-            text: pChild?.innerText?.trim(),
-            children: [],
-            top: pChild.offsetTop
-          };
-          children.push(strongItem);
-          // children = strongItem.children
-        }
-      }
-    }
-  }
-
-  tocs.value = toc;
-
-  console.log('toc', toc);
-};
-
-onMounted(() => {
-  tocHeight.value = window.document.body.offsetHeight - 210 - 51;
-  doStep();
-  const { query } = router.currentRoute.value;
-  if (query.q) {
-    ask(decodeURIComponent(query.q));
-    answerType.value = query.type || '0';
-    scope.value = query.scope || 'net';
-    var ds = '0';
-    if (!useUserStore().isLogin) {
-      ds = query.ds;
-    } else {
-      ds = useUserStore().user.user.enableDeepseek
-        ? useUserStore().user.user.enableDeepseek + ''
-        : query.ds;
-    }
-    dsHintTxt.value = '';
-    if (ds && ds == '1') {
-      ds = true;
-      dsLoading.value = true;
-    } else {
-      ds = false;
-      dsLoading.value = false;
-    }
-    dsChecked.value = ds;
-    activeTab.value = scope.value;
-  }
-
-  window.addEventListener(
-    'scroll',
-    () => {
-      if (markdownTocAffixRef.value && markdownTocAffixRef.value.updatePosition) {
-        markdownTocAffixRef.value.updatePosition();
-      }
-    },
-    true
-  );
-});
-
-defineExpose({ search, changeActiveTab, stopAI });
-</script>
-<style scoped lang="scss">
-@import 'src/assets/scss/variables';
-
-.ai-search-detail {
-  width: 100%;
-  background: $background_color;
-
-  .header {
-    width: 100%;
-    height: 60px;
-  }
-
-  .sub-header {
-    width: 100%;
-    height: 130px;
-  }
-
-  .search-panel {
-    width: 100%;
-    display: flex;
-    padding: 51px 0;
-
-    .markdown-toc-tdsc {
-      :deep(.toc-list) {
-        margin: 30px 0;
-      }
-    }
-
-    .search-detail {
-      width: calc(52%);
-      background: #ffffff;
-      margin-left: 52px;
-
-      &-tdsc {
-        width: calc(52%);
-      }
-
-      .search-result {
-        height: calc(100% - 40px);
-
-        .top {
-          width: 100%;
-          //height: 105px;
-          background: #fff;
-          //margin-bottom: 5px;
-          border-radius: 4px 4px 4px 4px;
-
-          .title {
-            width: calc(100% - 7px - 2px);
-            display: flex;
-            //margin-bottom: 24PX;
-            align-items: center;
-            min-height: 33px;
-
-            .icon {
-              width: 33px;
-              height: 29px;
-              background: url('@/assets/images/ai-search/question-prefixicon.png') no-repeat;
-              background-size: 100% 100%;
-              margin-right: 16px;
-            }
-
-            .question {
-              width: calc(100% - 32px - 150px);
-              //line-height: 20px;
-              font-family: PingFang SC, PingFang SC;
-              font-weight: 600;
-              font-size: 24px;
-              color: #212121;
-              text-align: left;
-              font-style: normal;
-              text-transform: none;
-            }
-
-            .graph-switch {
-              display: flex;
-              align-items: center;
-              margin-left: 10px;
-              width: 110px;
-
-              button {
-                margin-right: 2px;
-              }
-            }
-          }
-
-          .status {
-            display: flex;
-            align-items: center;
-            margin-top: 15px;
-            justify-content: center;
-
-            @keyframes shake {
-              0% {
-                transform: translateX(0);
-              }
-              25% {
-                transform: translateX(0px);
-              }
-              50% {
-                transform: translateX(2px);
-              }
-              75% {
-                transform: translateX(0px);
-              }
-              100% {
-                transform: translateX(2px);
-              }
-            }
-
-            .text {
-              width: 55px;
-              font-family: Alibaba PuHuiTi 3, Alibaba PuHuiTi 30;
-              font-weight: normal;
-              font-size: 14px;
-              color: #9da6b5;
-              line-height: 20px;
-              text-stroke: 1px rgba(0, 0, 0, 0);
-              text-align: left;
-              font-style: normal;
-              text-transform: none;
-              -webkit-text-stroke: 1px rgba(0, 0, 0, 0);
-            }
-          }
-        }
-
-        .result-panel {
-          width: 100%;
-          background: #fff;
-          padding: 29px 0;
-
-          .search-steps {
-            width: 100%;
-            padding: 4px 0;
-            display: flex;
-            align-items: center;
-            margin-bottom: 30px;
-            flex-direction: column;
-
-            .step {
-              width: 100%;
-
-              .info {
-                width: 100%;
-                padding: 0;
-                display: flex;
-                align-items: center;
-
-                .icon {
-                  width: 18px;
-                  height: 18px;
-                  background: url('@/assets/images/ai-search/step-wait-icon.png') no-repeat;
-                  background-size: 100% 100%;
-                  margin-right: 7px;
-                }
-
-                .title {
-                  width: 120px;
-
-                  font-family: PingFang SC, PingFang SC;
-                  font-weight: 600;
-                  font-size: 16px;
-                  color: #212121;
-                  text-align: left;
-                  font-style: normal;
-                  text-transform: none;
-                }
-
-                .tags {
-                  width: calc(100% - 10px - 27px - 100px);
-                  display: flex;
-                  justify-content: end;
-                  overflow: hidden;
-                  height: 30px;
-                  align-items: center;
-
-                  .tag {
-                    padding: 4px 14px;
-
-                    margin-left: 8px;
-                    line-height: 14px;
-                    text-align: left;
-                    font-style: normal;
-                    text-transform: none;
-                    white-space: nowrap;
-                    overflow: hidden;
-                    text-overflow: ellipsis;
-                    max-width: 200px;
-                    cursor: pointer;
-                    font-weight: 400;
-                    font-size: 14px;
-                    color: #2185f2;
-                    height: 24px;
-                    background: #f5f9fc;
-                    border: 1px solid #e9eef5;
-                    font-family: PingFang SC, PingFang SC;
-                    font-weight: 400;
-                    font-size: 14px;
-                    color: #525b74;
-                    /* line-height: 0px; */
-                    text-align: left;
-                    font-style: normal;
-                    text-transform: none;
-
-                    &-first {
-                      background: none;
-
-                      font-family: Microsoft YaHei;
-                      font-weight: 400;
-                      font-size: 16px;
-                      color: #666666;
-                    }
-                  }
-
-                  .tag-rol {
-                    display: inline-block;
-                    white-space: nowrap;
-                    box-sizing: border-box;
-                    //animation: scroll-text 25s linear infinite;
-                    //width: 100%;
-                    background: none;
-                    height: 22px;
-                    max-width: none;
-                    overflow: unset;
-                  }
-
-                  @keyframes scroll-text {
-                    0% {
-                      transform: translateX(100%);
-                    }
-                    100% {
-                      transform: translateX(-100%);
-                    }
-                  }
-                }
-              }
-
-              &-done {
-                .info {
-                  .icon {
-                    width: 18px;
-                    height: 18px;
-                    background: url('@/assets/images/ai-search/step-done-icon.png') no-repeat;
-                    background-size: 100% 100%;
-                    margin-right: 7px;
-                  }
-
-                  .title {
-                    font-family: Microsoft YaHei;
-                    font-weight: 400;
-                    font-size: 16px;
-                    color: #333333;
-                    line-height: 30px;
-                  }
-                }
-              }
-            }
-          }
-
-          .result {
-            width: 100%;
-
-            .tabs {
-              display: flex;
-              border-bottom: 1px solid #ccc;
-              .ds-box {
-                display: flex;
-                height: 36px;
-                align-items: center;
-                .iconfont {
-                  color: #4d6bfe;
-                  font-size: 22px;
-                  margin-right: 13px;
-                }
-                .switch {
-                  :deep(.ant-switch-checked) {
-                    background-color: #117ff9;
-                  }
-                }
-              }
-              .tab {
-                font-family: PingFang SC, PingFang SC;
-                font-weight: 400;
-                font-size: 24px;
-                color: #212121;
-                text-align: left;
-                font-style: normal;
-                text-transform: none;
-                margin-right: 28px;
-                display: flex;
-                justify-content: center;
-                flex-direction: column;
-                align-items: center;
-                cursor: pointer;
-
-                .title {
-                  margin-bottom: 9px;
-                }
-
-                .bottom {
-                  height: 0;
-                  margin-top: 10px;
-                }
-
-                &-active,
-                &:hover {
-                  font-family: PingFang SC, PingFang SC;
-                  font-weight: 400;
-                  font-size: 24px;
-                  color: #1586fa;
-                  text-align: left;
-                  font-style: normal;
-                  text-transform: none;
-
-                  .bottom {
-                    width: 100%;
-                    height: 4px;
-                    background: #446ae7;
-                    border-radius: 2px;
-                  }
-                }
-              }
-            }
-
-            .result-view {
-              width: 100%;
-              height: 50%;
-              overflow-y: auto;
-
-              .q-r {
-                padding: 20px;
-                line-height: 30px;
-                background: #f5f8fa;
-                border-radius: 4px 4px 4px 4px;
-                margin: 20px 0;
-                .ds-content-box {
-                  display: flex;
-                  .icon {
-                    width: 26px;
-                    height: 26px;
-                    display: flex;
-                    align-items: center;
-                    justify-content: center;
-                    border-radius: 50%;
-                    background: white;
-                    border: 1px solid #117ff9;
-                    img {
-                      width: 18px;
-                      height: 18px;
-                    }
-                  }
-                  .ds-panel {
-                    margin-top: -5px;
-                    margin-left: 10px;
-                    width: calc(100% - 40px);
-                    .ds-loading {
-                      display: flex;
-                      align-items: center;
-                      .icon-arrow {
-                        padding-left: 5px;
-                        cursor: pointer;
-                      }
-                    }
-                    .rotate {
-                      transform: rotate(180deg);
-                    }
-                  }
-                  .ds-con {
-                    border-left: 1px solid #ccc;
-                    padding-left: 10px;
-                  }
-                }
-              }
-
-              // 追问
-              .follow {
-                padding: 0 20px 20px 0;
-                width: 100%;
-                text-align: right;
-                height: auto;
-                min-height: 40px;
-
-                .eval-card {
-                  width: auto;
-                  display: flex;
-                  /* float: left; */
-                  justify-content: center;
-                  align-items: center;
-
-                  .eval-icon {
-                    width: 22px;
-                    height: 22px;
-
-                    &-like {
-                      background: url('@/assets/images/ai-search/eval-like-icon.png') no-repeat;
-                      background-size: 100% 100%;
-
-                      &-s,
-                      &:hover {
-                        background: url('@/assets/images/ai-search/eval-like-icon.png') no-repeat;
-                        background-size: 100% 100%;
-                      }
-                    }
-
-                    &-dislike {
-                      background: url('@/assets/images/ai-search/eval-dislike-icon.png') no-repeat;
-                      background-size: 100% 100%;
-
-                      &-s,
-                      &:hover {
-                        background: url('@/assets/images/ai-search/eval-dislike-icon.png') no-repeat;
-                        background-size: 100% 100%;
-                      }
-                    }
-
-                    &-copy {
-                      background: url('@/assets/images/ai-search/eval-copy-icon.png') no-repeat;
-                      background-size: 100% 100%;
-
-                      &-s,
-                      &:hover {
-                        background: url('@/assets/images/ai-search/eval-copy-icon.png') no-repeat;
-                        background-size: 100% 100%;
-                      }
-                    }
-                  }
-                }
-
-                .follow-input {
-                  margin-top: 10px;
-                  position: relative;
-
-                  textarea {
-                    width: 100%;
-                    height: 120px;
-                    border-color: white;
-                    border-radius: 10px;
-                    resize: none;
-                    padding: 10px;
-                    background: transparent;
-                    font-size: 19px;
-                    border-color: #4096ff;
-                    box-shadow: 0 0 0 2px rgba(5, 145, 255, 0.1);
-
-                    textarea:focus-visible,
-                    textarea:focus {
-                      border: none !important;
-                      outline: none;
-                    }
-
-                    textarea:focus {
-                      border: none !important;
-                      outline: none;
-                    }
-                  }
-
-                  .button {
-                    width: 110px;
-                    height: 40px;
-                    background: linear-gradient(90deg, #5c62ea, #517de2);
-                    border-radius: 20px;
-                    position: absolute;
-                    right: 10px;
-                    bottom: 12px;
-                    display: flex;
-                    justify-content: center;
-                    align-items: center;
-                    font-family: Alibaba PuHuiTi 2;
-                    font-weight: normal;
-                    font-size: 16px;
-                    color: #ffffff;
-                    cursor: pointer;
-                  }
-                }
-              }
-
-              .more-questions {
-                .title {
-                  display: flex;
-                  margin-bottom: 14px;
-
-                  .change-title {
-                    margin-left: 20px;
-                    display: flex;
-                    cursor: pointer;
-                    font-size: 13px;
-                    color: #3c3c3c;
-
-                    .change-icon {
-                      width: 16px;
-                      height: 16px;
-                      margin-right: 4px;
-                      background: url('@/assets/images/ai-search/icon-chang-question.png') no-repeat;
-                      background-size: 100% 100%;
-                    }
-                  }
-                }
-              }
-
-              .source {
-                width: 100%;
-
-                .title {
-                  display: flex;
-                  align-items: center;
-                  font-family: PingFang SC;
-                  font-weight: bold;
-                  font-size: 20px;
-                  color: #333333;
-                  line-height: 30px;
-
-                  .icon {
-                    width: 20px;
-                    height: 20px;
-                    margin-right: 8px;
-                    background: url('@/assets/images/ai-search/source-icon.png') no-repeat;
-                    background-size: 100% 100%;
-                  }
-
-                  .text {
-                    font-family: Alibaba PuHuiTi 3, Alibaba PuHuiTi 30;
-                    font-weight: normal;
-                    //font-size: 16px;
-                    color: #404557;
-                    line-height: 30px;
-                    text-align: left;
-                    font-style: normal;
-                    text-transform: none;
-                    margin-right: 5px;
-                  }
-                }
-
-                .items {
-                  .item {
-                    width: calc(100% - 10px);
-                    min-height: 82px;
-                    background: rgba(255, 255, 255, 0.5);
-                    border-radius: 10px;
-                    display: flex;
-                    flex-direction: column;
-                    align-items: center;
-                    padding: 20px;
-                    color: #888;
-                    display: flex;
-                    line-height: 25px;
-                    margin-bottom: 20px;
-                    margin: 12px 0;
-                    background: #f1f9ff;
-                    border-radius: 8px 8px 8px 8px;
-
-                    .doc {
-                      display: flex;
-                      width: 100%;
-
-                      p {
-                        width: calc(100% - 25px);
-
-                        .doc-link {
-                          cursor: pointer;
-                          color: #446ae7;
-
-                          &:hover {
-                            font-weight: bold;
-                          }
-                        }
-                      }
-
-                      .doc-icon {
-                        width: 20px;
-                        height: 20px;
-                        margin-left: 5px;
-                        cursor: pointer;
-
-                        &-show {
-                          background: url('/images/zcbd/ai-search/icon-doc-show.png') no-repeat;
-                          background-size: 100% 100%;
-                        }
-
-                        &-hide {
-                          background: url('/images/zcbd/ai-search/icon-doc-hide.png') no-repeat;
-                          background-size: 100% 100%;
-                        }
-                      }
-                    }
-
-                    .content {
-                      //white-space: pre-line;
-                      width: 100%;
-                      font-family: Alibaba PuHuiTi 3, Alibaba PuHuiTi 30;
-                      font-weight: normal;
-                      font-size: 14px;
-                      color: #757575;
-                      line-height: 28px;
-                      text-align: left;
-                      font-style: normal;
-                      text-transform: none;
-
-                      &-hide {
-                        display: none;
-                      }
-                    }
-                  }
-
-                  .item-url {
-                    margin: 12px 0;
-                    background: #f1f9ff;
-                    border-radius: 8px 8px 8px 8px;
-
-                    .bottom {
-                      width: 100%;
-
-                      .title-icon {
-                        display: flex;
-                        justify-content: left;
-                        align-items: center;
-                        width: 90%;
-                        float: left;
-
-                        .icon {
-                          width: 23px;
-                          height: 23px;
-                          background: url('/images/zcbd/ai-search/icon-net-url.png') no-repeat;
-                          background-size: 100% 100%;
-                        }
-
-                        .title {
-                          color: #a9a3a3cc;
-                          font-size: 16px;
-                          margin-left: 10px;
-                        }
-                      }
-
-                      .index {
-                        float: right;
-                        width: 20px;
-                        height: 20px;
-                        font-size: 12px;
-                        line-height: 20px;
-                        text-align: center;
-                        margin: 0 5px;
-                        border-radius: 10px;
-                        background: #d0d5dd;
-                        color: #000;
-                      }
-                    }
-                  }
-                }
-              }
-            }
-
-            // 土地市场
-            .map-answer {
-              position: relative;
-              width: 100%;
-              min-height: 600px;
-
-              .left-panel {
-                width: 100%;
-                background: #ffffff;
-                z-index: 1000;
-                //position: relative;
-                left: 12px;
-                top: 15px;
-                border-radius: 10px;
-
-                .title {
-                  width: 100%;
-                  height: 30px;
-                  text-align: left;
-                  line-height: 30px;
-                  font-size: 18px;
-                  font-weight: bolder;
-                }
-
-                .content {
-                  width: 100%;
-                  height: auto;
-
-                  .chart-card {
-                    width: 100%;
-                    height: 330px;
-
-                    .chart-title {
-                      width: 100%;
-                      height: 30px;
-                      font-size: 18px;
-                      font-weight: bolder;
-                      padding: 0 5px;
-                      line-height: 30px;
-                      margin: 10px 0;
-                    }
-
-                    .chart {
-                      width: 100%;
-                      height: 300px;
-                    }
-
-                    //background: #0a84ff;
-                  }
-
-                  .summary-card {
-                    width: 100%;
-                    height: 50%;
-
-                    .summary-title {
-                      width: 100%;
-                      height: 30px;
-                      font-size: 18px;
-                      font-weight: bolder;
-                      padding: 0 5px;
-                      line-height: 30px;
-                      margin: 10px 0;
-                    }
-
-                    .summary-content {
-                      width: calc(100% - 20px);
-                      height: calc(100% - 30px);
-                      padding: 0;
-                      white-space: pre-wrap;
-                      overflow-y: auto;
-                      border-radius: 10px;
-                      margin: 10px;
-                      font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Noto Sans, Helvetica,
-                        Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji';
-                      font-size: 16px;
-                      line-height: 1.5;
-                    }
-                  }
-                }
-              }
-
-              .map-panel {
-                width: 100%;
-                height: 630px;
-
-                .map-title {
-                  width: 100%;
-                  height: 30px;
-                  font-size: 18px;
-                  font-weight: bolder;
-                  padding: 0 5px;
-                  line-height: 30px;
-                  margin: 10px 0;
-                }
-
-                .map {
-                  width: 100%;
-                  height: 600px;
-                }
-              }
-            }
-          }
-        }
-      }
-    }
-  }
-
-  ::-webkit-scrollbar {
-    width: 8px;
-  }
-
-  ::-webkit-scrollbar-thumb {
-    background-color: rgb(0 0 0 / 10%);
-    border-radius: 10px;
-  }
-}
-
-::v-deep .input-modal {
-  .modal-que {
-    background: #0e91fd;
-
-    textarea {
-      width: 100%;
-      height: 257px !important;
-      border: none;
-      border-radius: 10px;
-      resize: none;
-      padding: 10px;
-      background: transparent;
-      font-size: 19px;
-
-      &:focus-visible {
-        border: none !important;
-        outline: none;
-      }
-    }
-  }
-}
-</style>
-

+ 138 - 0
ais_search/web/src/views/document/Menus.vue

@@ -0,0 +1,138 @@
+<template>
+  <div class="menus_box">
+    <div class="menu_item" v-for="(item, index) in items" :key="index">
+      <div :class="{menu: true, active: selectedId === item['id']}">
+        <span class="left" @click="onClickHandle('query', item)">
+          <ContainerOutlined v-if="item['parentId'] === 0" />
+          {{item['name']}}
+        </span>
+        <span class="right" v-if="item['id'] !== -1">
+          <template v-if="editor">
+            <FolderAddOutlined v-if="item['parentId'] === 0" class="operation_icon" @click="()=>onClickHandle('add', item)" />
+            <DeleteOutlined v-else class="operation_icon" />
+            <a-popover placement="bottom" trigger="click">
+              <template #content>
+                <p class="popover_item" v-if="item['parentId'] === 0" @click="()=>onClickHandle('delete', item)"><DeleteOutlined class="icon" />刪除</p>
+                <p class="popover_item"  @click="()=>onClickHandle('edit', item)"><FormOutlined  class="icon"/>编辑</p>
+                <p class="popover_item"  @click="()=>onClickHandle('move_up', item)" ><ArrowUpOutlined  class="icon"  />上移</p>
+                <p class="popover_item"  @click="()=>onClickHandle('move_down', item)"><ArrowDownOutlined  class="icon"  @click="()=>onClickHandle('move_down', item)" />下移</p>
+              </template>
+              <DashOutlined style="color:#7A7F90" />
+            </a-popover>
+          </template>
+          <template v-else-if="item['children'] && item['children'].length > 0">
+            <UpOutlined v-if="activeId === item['id']" class="toggle_icon" />
+            <DownOutlined v-else class="toggle_icon" />
+          </template>
+        </span>
+      </div>
+      <div :class="{menu_children: true, active: activeId === item['id']}" v-if="item['children'] && item['children'].length> 0">
+        <Menus :items="item['children']" :editor="editor" @menu-click="onMenuClickHandle" />
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import {
+  DashOutlined,
+  FormOutlined,
+  FolderAddOutlined,
+  DeleteOutlined,
+  ArrowUpOutlined,
+  ArrowDownOutlined,
+  UpOutlined,
+  DownOutlined,
+  ContainerOutlined
+} from "@ant-design/icons-vue";
+defineOptions({
+  name: 'Menus'
+})
+interface MenuItemType {
+  id: string|number;
+  name: string,
+  icon?: Function,
+  children?: MenuItemType[]
+}
+const props = defineProps<{
+  items: MenuItemType[],
+  editor: boolean
+}>()
+const selectedId = ref(-1)
+const activeId = ref(-1);
+const emits = defineEmits<{
+  (e: 'menu-click', p: any)
+}>()
+const toggleMenusHandle = (item) => {
+  if(activeId.value === item['id']){
+    // activeId.value = -1;
+  }else if(item['id'] !== -1){
+    activeId.value = item['id']
+  }
+}
+const onClickHandle = (type, payload) => {
+  if(type === 'query'){
+    toggleMenusHandle(payload)
+  }
+  if(payload['parentId'] === 0 && payload['id'] !== -1) return;
+  emits('menu-click', {
+    type,
+    payload
+  })
+}
+const onMenuClickHandle = (data) => {
+  const type = data['type']
+  const payload = data['payload']
+  emits('menu-click', {
+    type,
+    payload
+  })
+}
+</script>
+<style scoped lang="scss">
+.menus_box {
+  padding-left: 20px;
+  >.menu_item {
+    >.menu {
+      padding: 10px 15px;
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+      cursor: pointer;
+      >.left {
+        font-size: 14px;
+        color: #172538;
+        flex: 1;
+        user-select: none;
+      }
+      &.active {
+        border-radius: 4px;
+        border: 1px solid #80BCFF;
+        background: #EBF5FF;
+        >.left {
+          color: #3082F4;
+        }
+      }
+    }
+    >.menu_children {
+      padding-left: 15px;
+      display: none;
+      &.active {
+        display: block;
+      }
+    }
+  }
+}
+.operation_icon {
+  display: inline-block;
+  margin-right: 8px;
+  color: #7A7F90;
+  cursor: pointer;
+}
+.toggle_icon {
+  font-size: 14px;
+  color: #7A7F90;
+  cursor: pointer;
+  transform: scale(1, 0.8);
+}
+</style>

+ 47 - 27
ais_search/web/src/views/document/MoveFiles.vue

@@ -35,9 +35,9 @@
  */
 import { ref, onMounted, watch, toRefs, PropType } from "vue";
 import { message } from "ant-design-vue";
+import http from "@/utils/http";
 import MyIcon from "@/components/myIcon/index.vue";
-import { getAllFolder } from "./http";
-import { moveFiles } from "./http";
+import { listToTree } from '@/utils/struct'
 const props = defineProps({
   ids: {
     type: Array as PropType<string[]>,
@@ -54,16 +54,37 @@ const props = defineProps({
 });
 const { ids, open, closeModel } = toRefs(props);
 
-const expandedKeys = ref<string[]>(["root"]);
+const expandedKeys = ref<string[]>([]);
 const selectedKeys = ref<string[]>([]);
-const treeData = ref([
-  {
-    title: "我的文档",
-    key: "root",
-    children: [],
-  },
-]);
+const treeData = ref();
 
+const getTypeList = () => {
+  const urlStr = `/ai/knowledge/type/list`
+  http.get(urlStr, {
+    name:''
+  }).then((result) => {
+    const resultList = result.data.map((item)=>{
+      return {
+        key: item['id'],
+        title: item['name'],
+        parentId: item['parentId'],
+        children: ''
+      }
+    })
+    if (result.data && result.data.length > 0) {
+      const lists = [{
+        "key": -1,
+        "title": "全部知识库",
+        "parentId": 0,
+        "children": null
+      }].concat(resultList)
+      treeData.value = listToTree(lists, {
+        id: 'key'
+      })
+    }
+  })
+}
+getTypeList();
 onMounted(() => {
   initModel();
 });
@@ -76,27 +97,26 @@ watch(
 );
 const initModel = () => {
   selectedKeys.value = [];
-  getAllFolder().then((res) => {
-    treeData.value[0].children = res.data.map((item: any) => {
-      return {
-        title: item.fileName,
-        key: item.fileId,
-      };
-    });
-  });
 };
 
 const handleOk = async () => {
-  const selectKey =
-    selectedKeys.value[0] == "root" ? "" : selectedKeys.value[0];
-  const res: any = await moveFiles(ids.value, selectKey);
-  console.log("res", res);
-  if (res.success) {
-    message.success("文件移动成功!");
-    closeModel.value();
-  } else {
-    message.error("文件移动失败,请稍后重试!");
+  const selectKey =selectedKeys.value[0];
+  console.log("res", selectKey);
+  console.log(props.ids)
+  if(selectKey)
+};
+const updateHandle = (id, parentId) => {
+  const urlStr = `/ai/knowledge/file/update`
+  const sendData = {
+    id,
+    parentId: parentId
   }
+  http.post(urlStr, sendData).then((result) => {
+    if (result.data) {
+      message.success("重命名成功!");
+      getTypeList();
+    }
+  });
 };
 </script>
 <style scoped lang="scss"></style>

+ 0 - 64
ais_search/web/src/views/document/http.ts

@@ -1,64 +0,0 @@
-import http from "@/utils/http";
-import { useUserStore } from "@/stores";
-const store: any = useUserStore();
-// console.log("store", store?.user?.user);
-// const userId = "76663c27696e5414b134e9ca6c61edf4";
-const userId = store?.user?.user?.id ?? "";
-// 先写死
-const proxyUrl = "";
-// 获取我的文档列表
-interface IDocProps {
-  id?: string; // 文件夹id
-  searchKey?: string;
-}
-export const getDocList = (params: IDocProps) =>
-  http.post(`${proxyUrl}/file/library/collect/list`, {
-    ...params,
-    userId,
-  });
-
-// 新建文件夹
-export const madeNewFolder = (name: string) =>
-  http.get(`${proxyUrl}/file/library/dir/make`, {
-    name,
-    userId,
-  });
-// 判断是否是重复文件夹
-export const isRepeatFolder = (name: string) =>
-  http.get(`${proxyUrl}/file/library/dir/check`, {
-    name,
-    userId,
-  });
-// 获取用户所有文件夹
-export const getAllFolder = () =>
-  http.get(`${proxyUrl}/file/library/dir/list`, {
-    userId,
-  });
-// 删除文件、文件夹
-export const deleteDocs = (ids: string[]) =>
-  http.post(`${proxyUrl}/file/library/collect/deleteorrecycler`, {
-    ids,
-    userId,
-    isDeleted: true,
-  });
-// 置顶文件或文件夹
-export const topDocs = (id: string) =>
-  http.get(`${proxyUrl}/file/library/collect/top`, {
-    id,
-    userId,
-  });
-// 移动文件
-export const moveFiles = (ids: string[], fileId: string) =>
-  http.post(`${proxyUrl}/file/library/collect/move`, {
-    ids,
-    fileId,
-    userId,
-  });
-// 打包多选文件并提供下载
-export const downloadDocs = (ids: string[]) =>
-  http.download("POST", `${proxyUrl}/file/library/collect/download`, {
-    data: {
-      ids,
-      userId,
-    }, // POST 请求体
-  });

+ 91 - 24
ais_search/web/src/views/document/index.scss

@@ -1,6 +1,12 @@
 
 .page-doc {
   height: 100%;
+  .top_btn {
+    height: 36px;
+  }
+  .m-l-15 {
+    margin-left: 15px;
+  }
   >.header {
     width: 100%;
     height: 60px;
@@ -8,37 +14,98 @@
   >.my-doc-container {
     width: 100%;
     height: calc(100% - 60px);
-    background-color: #fff;
+    background-color: #f2f5fa;
     font-family: PingFang SC, PingFang SC;
-    .page-content {
-      width: 100%;
-      padding: 30px;
-      .content-top {
+    display: flex;
+    >div {
+      &.left_box {
+        width: 260px;
+        padding: 20px 0px;
+        padding-bottom: 0px;
         display: flex;
-        justify-content: space-between;
-        .search-icon {
-          font-size: 18px;
-          color: #a8aeb7;
-          &:hover {
-            color: #4096ff;
+        flex-direction: column;
+        >.type_item {
+          padding: 10px;
+          cursor: pointer;
+          user-select: none;
+          >.btn {
+            font-size: 14px;
+            padding: 12px 15px;
+            border-radius: 6px 6px 6px 6px;
           }
         }
-      }
-      .content-table {
-        width: 100%;
-        margin-top: 20px;
-        :deep(.ant-table-cell) {
-          background-color: #fff !important;
+        >.all_item {
+          >.btn {
+            background: #EBF5FF;
+            border: 1px solid #80BCFF;
+            color: #3082F4;
+          }
+        }
+        >.type_item {
+          >.btn {
+            border: 1px solid #D6DBE0;
+            background-color: #fff;
+            color: #172538;
+            text-align: center;
+          }
+        }
+        >.menu_box {
+          flex: 1;
+          overflow-y: auto;
+          ::v-deep {
+            .ant-menu-light {
+              background-color: transparent;
+            }
+          }
         }
-        .name-box {
+      }
+      &.content_box {
+        padding: 20px;
+        padding-left: 10px;
+        >.page-content {
+          background-color: #fff;
+          border-radius: 15px;
+          overflow-y: auto;
+          width: 100%;
+          height: 100%;
+          padding: 30px;
           display: flex;
-          align-items: center;
-          cursor: pointer !important;
-          span {
-            margin-left: 10px;
+          flex-direction: column;
+          .content-top {
+            display: flex;
+            justify-content: space-between;
+            .search-icon {
+              font-size: 18px;
+              color: #a8aeb7;
+              &:hover {
+                color: #4096ff;
+              }
+            }
+          }
+          .content-table {
+            width: 100%;
+            flex: 1;
+            overflow-y: auto;
+            margin-top: 20px;
+            :deep(.ant-table-cell) {
+              background-color: #fff !important;
+            }
+            .name-box {
+              display: flex;
+              align-items: center;
+              cursor: pointer !important;
+              span {
+                margin-left: 10px;
+              }
+              &:hover {
+                color: #1890ff;
+              }
+            }
           }
-          &:hover {
-            color: #1890ff;
+          .pagination-box {
+            display: flex;
+            align-items: center;
+            justify-content: space-between;
           }
         }
       }

+ 362 - 288
ais_search/web/src/views/document/index.vue

@@ -11,173 +11,203 @@
       @close="closeViewerHandle"
     />
     <div class="my-doc-container">
-      <TitleHeader :items="menuList"  @tabClick="tabClickHandle" />
-      <div class="page-content">
-        <div class="content-top">
-          <div class="left">
-            <a-button
-              type="primary"
-              :icon="h(CloudUploadOutlined)"
-              @click="uploadFiles(state.selectedRowKeys)"
-              style="height: 36px"
-              >导入</a-button
-            >
-            <a-button
-              :icon="h(FolderAddOutlined)"
-              style="margin-left: 15px; height: 36px"
-              @click="openNewFolderModel = true"
-              >新建文件夹</a-button
-            >
-            <a-button
-              :icon="h(DownloadOutlined)"
-              @click="downloadFiles(state.selectedRowKeys)"
-              style="margin-left: 15px; height: 36px"
-              >下载</a-button
-            >
-            <a-button
-              v-if="false"
-              :icon="h(DeliveredProcedureOutlined)"
-              @click="moveFiles(state.selectedRowKeys)"
-              style="margin-left: 15px; height: 36px"
-              >移动</a-button
-            >
+      <div class="left_box">
+        <div class="type_item" v-if="isFolderEditor" @click="()=>{onMenuClickHandle({type: 'add', 'id': 0})}">
+          <div class="btn">新建一级类型</div>
+        </div>
+        <div class="menu_box">
+          <menus
+            mode="inline"
+            :items="folders"
+            :editor="isFolderEditor"
+            @menu-click="onMenuClickHandle" >
+        </menus>
+        </div>
+        <div class="type_item">
+          <div class="btn" @click="isFolderEditor=!isFolderEditor">{{ isFolderEditor ? '退出编辑': '业务类型管理' }}</div>
+        </div>
+      </div>
+      <div class="content_box">
+        <div class="page-content">
+          <div class="content-top">
+            <div class="left">
+              <a-button
+                type="primary"
+                :icon="h(CloudUploadOutlined)"
+                @click="uploadFiles(state.selectedRowKeys)"
+                class="top_btn"
+                >导入</a-button
+              >
+              <a-button
+                :icon="h(DownloadOutlined)"
+                @click="downloadFiles(state.selectedRowKeys)"
+                class="top_btn m-l-15"
+                >下载</a-button
+              >
+              <a-dropdown>
+                <template #overlay>
+                  <a-menu @click="onChangeBusClass">
+                    <a-menu-item key="1">
+                      是
+                    </a-menu-item>
+                  </a-menu>
+                </template>
+                <a-button class="top_btn m-l-15">
+                  业务是否分类
+                  <DownOutlined />
+                </a-button>
+              </a-dropdown>
+              <a-dropdown>
+                <template #overlay>
+                  <a-menu @click="onChangeDocType">
+                    <a-menu-item key="0">pdf</a-menu-item>
+                    <a-menu-item key="1">doc</a-menu-item>
+                  </a-menu>
+                </template>
+                <a-button class="top_btn m-l-15">
+                  文档格式
+                  <DownOutlined />
+                </a-button>
+              </a-dropdown>
+            </div>
+            <div class="right">
+              <a-input
+                v-model:value="searchValue"
+                placeholder="搜索文档"
+                style="width: 280px; height: 40px"
+                @pressEnter="initTableList"
+              >
+                <template #suffix>
+                  <SearchOutlined
+                    @click="initTableList"
+                    style=""
+                    class="search-icon"
+                  />
+                </template>
+              </a-input>
+            </div>
           </div>
-          <div class="right">
-            <a-input
-              v-model:value="searchValue"
-              placeholder="搜索文档"
-              style="width: 280px; height: 40px"
-              @pressEnter="searchHandle"
+          <div class="content-table">
+            <a-table
+              :row-selection="{
+                selectedRowKeys: state.selectedRowKeys,
+                onChange: onSelectChange,
+              }"
+              rowKey="id"
+              :loading="loading"
+              :columns="columns"
+              :data-source="dataSource"
+              :pagination="false"
             >
-              <template #suffix>
-                <SearchOutlined
-                  @click="searchHandle"
-                  style=""
-                  class="search-icon"
-                />
-              </template>
-            </a-input>
-          </div>
-        </div>
-        <div class="content-table">
-          <a-table
-            :row-selection="{
-              selectedRowKeys: state.selectedRowKeys,
-              onChange: onSelectChange,
-            }"
-            rowKey="id"
-            :loading="loading"
-            :columns="columns"
-            :data-source="dataSource"
-            :pagination="false"
-          >
-            <template #bodyCell="{ column, record }">
-              <template v-if="column.dataIndex === 'name'">
-                <div class="editable-cell">
-                  <div v-if="editableData[record.id] && !record.type">
-                    <a-input v-model:value="editableData[record.id].name" @pressEnter="updateHandle(record.id)" />
-                  </div>
-                  <div v-else class="editable-cell-text-wrapper">
-                    <div
-                      v-if="!record.type"
-                      class="name-box"
-                      @click="jumpToFile(record)"
-                    >
-                      <MyIcon icon="icon-wjj" size="18" />
-                      <span>{{ record.name }}</span>
+              <template #bodyCell="{ column, record }">
+                <template v-if="column.dataIndex === 'name'">
+                  <div class="editable-cell">
+                    <div v-if="editableData[record.id] && !record.type">
+                      <a-input v-model:value="editableData[record.id].name" @pressEnter="updateHandle(record.id)" />
                     </div>
-                    <div v-else class="name-box" @click="showFileDetail(record)">
-                      <MyIcon icon="icon-wenjianleixing-suolvetu-PDFwendang" size="18" />
-                      <span>{{ record.name }}</span>
+                    <div v-else class="editable-cell-text-wrapper">
+                      <div class="name-box" @click="showFileDetail(record)">
+                        <MyIcon icon="icon-wenjianleixing-suolvetu-PDFwendang" size="18" />
+                        <span>{{ record.name }}</span>
+                      </div>
                     </div>
                   </div>
-                </div>
-              </template>
-              <template v-if="column.key === 'createTime'">
-                <div>
-                  {{ dayjs(record.createTime).format("YYYY-MM-DD HH:mm:ss") }}
-                </div>
-              </template>
-              <template v-else-if="column.key === 'action'">
-                <a-button
-                  type="text"
-                  :icon="h(DownloadOutlined)"
-                  style="margin-right: 15px"
-                  v-if="record.type"
-                  @click="
-                    record.type
-                      ? downloadFile(record)
-                      : downloadFiles([record.id])
-                  "
-                  >下载</a-button
-                >
-                <a-dropdown>
-                  <a class="ant-dropdown-link" @click.prevent>
-                    <span style="font-size: 18px; font-wight: 600">···</span>
-                    <DownOutlined />
-                  </a>
-                  <template #overlay>
-                    <a-menu>
-                      <a-menu-item>
-                        <a-button
-                          type="link"
-                          :icon="h(DeleteOutlined)"
-                          style="color: #000"
-                          @click="deleteFiles(record.id)"
-                          >刪除</a-button
-                        >
-                      </a-menu-item>
-                      <a-menu-item>
-                        <a-button
-                          type="link"
-                          v-if="false"
-                          :icon="h(DeliveredProcedureOutlined)"
-                          style="color: #000"
-                          @click="moveFiles([record.id])"
-                          >移动</a-button
-                        >
-                      </a-menu-item>
-                      <a-menu-item>
-                        <a-button
-                          type="link"
-                          :icon="h(VerticalAlignTopOutlined)"
-                          style="color: #000"
-                          @click="topFileHandle(record)"
-                          >置顶</a-button
-                        >
-                      </a-menu-item>
-                      <a-menu-item>
-                        <a-button
-                          type="link"
-                          v-if="!record.type"
-                          :icon="h(FormOutlined)"
-                          style="color: #000"
-                          @click="editFolderName(record.id)"
-                          >编辑</a-button
-                        >
-                      </a-menu-item>
-                    </a-menu>
-                  </template>
-                </a-dropdown>
+                </template>
+                <template v-if="column.key === 'createTime'">
+                  <div>
+                    {{ dayjs(record.createTime).format("YYYY-MM-DD HH:mm:ss") }}
+                  </div>
+                </template>
+                <template v-else-if="column.key === 'action'">
+                  <a-button
+                    type="text"
+                    :icon="h(DownloadOutlined)"
+                    style="margin-right: 15px"
+                    @click="downloadFile(record)"
+                    >下载</a-button
+                  >
+                  <a-dropdown>
+                    <a class="ant-dropdown-link" @click.prevent>
+                      <span style="font-size: 18px; font-wight: 600">···</span>
+                    </a>
+                    <template #overlay>
+                      <a-menu>
+                        <a-menu-item>
+                          <a-button
+                            type="link"
+                            :icon="h(DeleteOutlined)"
+                            style="color: #000"
+                            @click="onDeleteFileHandle(record.id)"
+                            >刪除</a-button
+                          >
+                        </a-menu-item>
+                        <a-menu-item>
+                          <a-button
+                            type="link"
+                            :icon="h(DeliveredProcedureOutlined)"
+                            style="color: #000"
+                            @click="moveFiles([record.id])"
+                            >移动</a-button
+                          >
+                        </a-menu-item>
+                        <a-menu-item>
+                          <a-button
+                            type="link"
+                            :icon="h(VerticalAlignTopOutlined)"
+                            style="color: #000"
+                            @click="topFileHandle(record)"
+                            >置顶</a-button
+                          >
+                        </a-menu-item>
+                        <a-menu-item>
+                          <a-button
+                            type="link"
+                            :icon="h(FormOutlined)"
+                            style="color: #000"
+                            @click="editFolderName(record.id)"
+                            >编辑</a-button
+                          >
+                        </a-menu-item>
+                      </a-menu>
+                    </template>
+                  </a-dropdown>
+                </template>
               </template>
-            </template>
-          </a-table>
+            </a-table>
+          </div>
+          <div class="pagination-box">
+            <a-pagination v-model:current="current1" :showSizeChanger="false" show-quick-jumper :total="total" @change="onPageChange" />
+            <span>共{{ total }}个文件</span>
+          </div>
         </div>
       </div>
-      <a-modal v-model:open="openNewFolderModel" title="新建分组" @ok="createFolder">
-        <a-input
-          style="margin-top: 15px; margin-bottom: 10px"
-          v-model:value="folderName"
-          placeholder="请输入分组名称"
-        />
-      </a-modal>
-      <MoveFileModel
-        :open="moveFileModel"
-        :ids="moveFileIds"
-        :closeModel="closeMoveFileModel"
-      />
-      <FileUpload ref="userUploadFileRef" :pid="currentTabId" @close="closeHandle"/>
     </div>
+    <a-modal v-model:open="openNewFolderModel" title="新建类型" @ok="createFolder">
+      <a-input
+        style="margin-top: 15px; margin-bottom: 10px"
+        v-model:value="folderName"
+        placeholder="请输入类型名称"
+      />
+    </a-modal>
+    <a-modal v-model:open="editVisabled" title="编辑类型" @ok="updateHandle">
+      <a-input
+        style="margin-top: 15px; margin-bottom: 10px"
+        v-model:value="editableData.name"
+        placeholder="请输入类型名称"
+      />
+    </a-modal>
+    <a-modal v-model:open="deleteVisiabled" title="提示" @ok="deleteFolders">
+      <h2>确定删除该目录吗?</h2>
+    </a-modal>
+    <a-modal v-model:open="deleteFileVisiabled" title="提示" @ok="deleteFiles">
+      <h2>确定删除该文件吗?</h2>
+    </a-modal>
+    <MoveFileModel
+      :open="moveFileModel"
+      :ids="moveFileIds"
+      :closeModel="closeMoveFileModel"
+    />
+    <FileUpload ref="userUploadFileRef" :pid="currentId" @close="closeHandle"/>
   </div>
 </template>
 <script lang="ts" setup>
@@ -187,7 +217,9 @@
 import { cloneDeep } from 'lodash-es';
 import { h, ref, reactive, onMounted } from "vue";
 import http from "@/utils/http";
+import { listToTree } from '@/utils/struct'
 import {
+  DownOutlined,
   FormOutlined,
   DeleteOutlined,
   CloudUploadOutlined,
@@ -200,78 +232,38 @@ import {
 import HomeHeader from '@/views/home/components/HomeHeader.vue';
 import { message } from "ant-design-vue";
 import MyIcon from "@/components/myIcon/index.vue";
+import Menus from "./Menus.vue";
 import MoveFileModel from "./MoveFiles.vue";
 import FileUpload from "./FileUpload.vue";
 import FileDetail from "./FileDetail.vue";
-import TitleHeader from "./TitleHeader.vue";
 import dayjs from "dayjs";
 
-onMounted(() => {
-  getDataSource();
-});
-const menuList_old = JSON.parse(localStorage.getItem("_menuList")) || [
-  {
-    name: "全部",
-    id: 0,
-    checked: true
-  }
-]
-const menuList = ref<{
-  name: string,
-  id: number,
-  checked: boolean
-}[]>(cloneDeep(menuList_old))
-const currentTabId = ref(parseInt(localStorage.getItem("_currentTabId") as string) || 0)
-const tabClickHandle = (item) => {
-  if (currentTabId.value === item['id']) return;
-  currentTabId.value = item['id']
-  item['checked'] = true;
-  if (item['id'] === 0) {
-    menuList.value = [item];
-  } else {
-    let end = 0;
-    for (let i = 0; i < menuList.value.length; i++){
-      if (menuList.value[i].id === item.id) {
-        end = i;
-        break;
-      }
-    }
-    menuList.value = menuList.value.slice(0, end+1)
-  }
-  localStorage.setItem("_currentTabId", currentTabId.value.toString())
-  localStorage.setItem("_menuList", JSON.stringify(menuList.value))
-  getDataSource();
+const currentId = ref(-1);
+
+const onChangeBusClass = () => {
+  
 }
-// 切换目录
-const changeMenu = (data: any = null) => {
-  // 切换目录时重置搜索内容和选中的文件
-  searchValue.value = "";
-  state.selectedRowKeys = [];
-  menuList.value.forEach((item) => {
-    item['checked'] = false;
-  })
-  menuList.value.push({
-    name: data.name,
-    id: data.id,
-    checked: true,
-  })
-  localStorage.setItem("_currentTabId", currentTabId.value.toString())
-  localStorage.setItem("_menuList", JSON.stringify(menuList.value))
-};
+const onChangeDocType = () => {
 
+}
 // 表格列
 const columns = [
   {
     title: "文档名称",
     dataIndex: "name",
     key: "name",
-    width: 560
+    width: 360
   },
   {
     title: "文档类型",
     dataIndex: "type",
     key: "type",
   },
+  {
+    title: "业务类型",
+    dataIndex: "typeName",
+    key: "typeName",
+  },
   {
     title: "文档大小",
     dataIndex: "size",
@@ -296,28 +288,39 @@ const columns = [
 ];
 const searchValue = ref("");
 const dataSource = ref<any[]>([]);
+const pageNo = ref(1)
+const total = ref(0)
 const loading = ref(false)
-const getDataSource = () => {
-  // 重置选中的文件
-  state.selectedRowKeys = [];
-  const urlStr = `/ai/knowledge/file/list`
+const getTableList = () => {
+  const urlStr = `/ai/knowledge/file/page`
   loading.value = true;
-  // 获取文档列表
-  http.get(urlStr, {
+  const sendData = {
+    pageNo: pageNo.value,
+    pageSize: 10,
     name: searchValue.value,
-    parentId: currentTabId.value
-  }).then((result) => {
+    parentId: currentId.value === -1 ? '' : currentId.value
+  }
+  http.get(urlStr, sendData).then((result) => {
     loading.value = false;
-    if (result.data) {
-      dataSource.value = result.data;
+    const resultData = result.data;
+    if (resultData['list']) {
+      dataSource.value = resultData['list'];
+      total.value = resultData['total']
     }
   }).catch((err) => {
     loading.value = false;
   });
 };
-
+const initTableList = () => {
+  pageNo.value = 1;
+  getTableList();
+}
+getTableList();
+const onPageChange = (page) => {
+  pageNo.value = page;
+  getTableList();
+}
 const state = reactive<{
-  selectedRowKeys: any[];
   loading: boolean;
 }>({
   selectedRowKeys: [], // Check here to configure the default column
@@ -328,38 +331,156 @@ const onSelectChange = (selectedRowKeys1) => {
   state.selectedRowKeys = selectedRowKeys1;
 };
 const searchHandle = () => {
-  getDataSource()
+  initTableList()
 }
-// 点击进入文件夹
-const clickFileDetail = ref<null | any>(null);
-const jumpToFile = (data: any) => {
-  clickFileDetail.value = data;
-  currentTabId.value = data.id;
-  // 触发目录切换的回调
-  changeMenu(data);
-  getDataSource();
-};
-
+const currentFileId = ref(null)
+const deleteFileVisiabled = ref(false)
+const deleteFiles = () => {
+  const urlStr = `/ai/knowledge/file/delete?id=${currentFileId.value}`
+  http.get(urlStr).then((result) => {
+    if (result.data) {
+      message.success("删除成功!");
+      getTableList();
+    } else {
+      message.error("删除失败,请稍后重试!");
+    }
+    deleteFileVisiabled.value = false;
+  });
+}
+const onDeleteFileHandle = (id) => {
+  deleteFileVisiabled.value = true
+  currentFileId.value = id
+}
+/***
+ * 左侧文件夹目录功能
+ * 查询、新增、重命名、删除
+ */
+const isFolderEditor = ref(false)
+const folders = ref([])
+const getTypeList = () => {
+  const urlStr = `/ai/knowledge/type/list`
+  http.get(urlStr, {
+    name:''
+  }).then((result) => {
+    loading.value = false;
+    if (result.data && result.data.length > 0) {
+      const lists = [{
+        "id": -1,
+        "name": "全部知识库",
+        "parentId": 0,
+        "sort": null
+      }].concat(result.data)
+      folders.value = listToTree(lists)
+      console.log(folders.value)
+    }
+  })
+}
+getTypeList();
 // 新建文件夹
 const folderName = ref("");
 const openNewFolderModel = ref(false);
 const createFolder = async () => {
-  const urlStr = '/ai/knowledge/file/create'
+  const urlStr = '/ai/knowledge/type/create'
   const formData = new FormData();
   formData.append("name", folderName.value)
-  formData.append("parentId", currentTabId.value.toString());
+  formData.append("parentId", currentId.value);
   http.post(urlStr, formData).then((result) => {
     if (result.data) {
       message.success("文件夹新建成功!");
-      getDataSource(currentTabId.value);
       folderName.value = "";
-      openNewFolderModel.value = false;
+      getTypeList();
     } else {
       message.error("新建文件夹失败,请稍后重试!");
     }
+    openNewFolderModel.value = false;
   });
 };
-
+//文件夹重命名
+const editableData = ref({});
+const editVisabled = ref(false)
+const updateHandle = (id: string) => {
+  const urlStr = `/ai/knowledge/type/update`
+  const sendData = {
+    ...editableData.value
+  }
+  http.post(urlStr, sendData).then((result) => {
+    if (result.data) {
+      message.success("重命名成功!");
+      getTypeList();
+    }
+    editVisabled.value = false;
+  });
+};
+//文件夹刪除
+const deleteVisiabled = ref(false)
+const deleteFolders = () => {
+  const urlStr = `/ai/knowledge/type/delete?id=${currentId.value}`
+  http.get(urlStr).then((result) => {
+    if (result.data) {
+      message.success("删除成功!");
+      getTypeList();
+    } else {
+      message.error("删除失败,请稍后重试!");
+    }
+    deleteVisiabled.value = false;
+  });
+}
+//文件夹刪除
+const moveUpHandle = (id) => {
+  const urlStr = `/ai/knowledge/type/move-up?id=${id}`
+  http.get(urlStr).then((result) => {
+    if (result.data) {
+      message.success("上移成功!");
+      getTypeList();
+    } else {
+      message.error("上移失败,请稍后重试!");
+    }
+  });
+}
+//文件夹刪除
+const moveDownHandle = (id) => {
+  const urlStr = `/ai/knowledge/type/move-down?id=${id}`
+  http.get(urlStr).then((result) => {
+    if (result.data) {
+      message.success("下移成功!");
+      getTypeList();
+    } else {
+      message.error("下移失败,请稍后重试!");
+    }
+  });
+}
+const onMenuClickHandle = (data) => {
+  const type = data['type']
+  const payload = data['payload']
+  currentId.value = payload['id'] == -1 ? 0 : payload['id']
+  switch (type) {
+    case 'query':
+      searchValue.value = ''
+      console.log("payload value:", payload)
+      initTableList();
+      break;
+    case 'add':
+      openNewFolderModel.value = true
+      break;
+    case 'edit':
+      editVisabled.value = true
+      editableData.value = cloneDeep(payload);
+      break;
+    case 'delete':
+      deleteVisiabled.value = true
+      break;
+    case 'move_up':
+      moveUpHandle(payload['id'])
+      break;
+    case 'move_down':
+      moveDownHandle(payload['id'])
+      break;
+  }
+  
+};
+/**
+ * 文件
+ */
 // 下载(单文件下载)
 const downloadFile = async (data: any) => {
   const downloadUrl = `${window.AppGlobalConfig.knowledgeDocUrlProxy}${data.name}`
@@ -380,7 +501,7 @@ const downloadFile = async (data: any) => {
 const userUploadFileRef = ref<any>(null);
 const uploadFiles = () => userUploadFileRef.value.showModal();
 const closeHandle = () => {
-  getDataSource();
+  getTableList();
 }
 // 多文件打包下载
 const downloadFiles = async (ids: any[] = []) => {
@@ -392,26 +513,6 @@ const downloadFiles = async (ids: any[] = []) => {
     downloadFile(item['name'])
   })
 };
-//文件夹编辑
-const editableData = reactive({});
-const editFolderName = (id) => {
-  editableData[id] = cloneDeep(dataSource.value.filter(item => id === item.id)[0]);
-}
-const updateHandle = (id: string) => {
-  const item = dataSource.value.filter(item => id === item.id)[0];
-  Object.assign(item, editableData[id]);
-  const urlStr = `/ai/knowledge/file/update`
-  const sendData = {
-    ...item
-  }
-  http.post(urlStr, sendData).then((result) => {
-    if (result.data) {
-      message.success("重命名成功!");
-    }
-  });
-  delete editableData[id];
-};
-
 // 置顶
 const topFileHandle = async (item) => {
   const urlStr = `/ai/knowledge/file/update`
@@ -422,24 +523,12 @@ const topFileHandle = async (item) => {
   http.post(urlStr, sendData).then((result) => {
     if (result.data) {
       message.success("置顶成功!");
-      getDataSource();
+      getTableList();
     } else {
       message.error("置顶失败,请稍后重试!");
     }
   });
 };
-//刪除
-const deleteFiles = (id) => {
-  const urlStr = `/ai/knowledge/file/delete?id=${id}`
-  http.get(urlStr).then((result) => {
-    if (result.data) {
-      message.success("删除成功!");
-      getDataSource();
-    } else {
-      message.error("删除失败,请稍后重试!");
-    }
-  });
-}
 // 移动:最外层文件禁止多选移动,文件夹类型禁止移动
 const moveFileModel = ref(false);
 const moveFileIds = ref<any[]>([]);
@@ -450,21 +539,6 @@ const moveFiles = (ids: any[] = []) => {
 const closeMoveFileModel = () => {
   moveFileModel.value = false;
   moveFileIds.value = [];
-  refreshList();
-};
-// 列表刷新
-const refreshList = () => {
-  if (clickFileDetail.value) {
-    // 关闭弹窗后刷新列表
-    getDataSource();
-    // 刷新目录
-    changeMenu(clickFileDetail.value.fileName);
-  } else {
-    // 关闭弹窗后刷新列表
-    getDataSource();
-    // 刷新目录
-    changeMenu();
-  }
 };
 // 跳到文件详情页面
 const fileViewerVisabled = ref(false)

+ 0 - 4
ais_search/web/src/views/home/home.vue

@@ -77,10 +77,6 @@ import HomeHeader from '@/views/home/components/HomeHeader.vue';
 
 import { isEmptyStr } from '@/utils/common';
 import SliderCardUpDown from '@/components/NewSliderCardUpDown.vue';
-import { useUserStore } from '@/stores';
-const { user: u = {}, updateUser } = useUserStore();
-const { user = {}, token } = u || {};
-import UserAPI from '@/api/user';
 const emits = defineEmits(['login']);
 const question = ref('');
 const activeTypeIndex = ref(0);

+ 2 - 2
ais_search/web/vite.config.js

@@ -41,8 +41,8 @@ export default defineConfig({
     cors: true,
     proxy: {
       '/server': {
-          // target: 'http://121.40.148.47:8531/server',
-          target: 'https://zdzy.zrzyt.zj.gov.cn/aisKnowledge',
+          target: 'http://localhost:9999/',
+          // target: 'https://zdzy.zrzyt.zj.gov.cn/aisKnowledge',
           changeOrigin: true,
           rewrite: function (path) { return path.replace(/^\/server/, ''); }
       },