Explorar el Código

抽离AI知识库

songxy hace 2 días
padre
commit
b7f074e1b1

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

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

+ 7 - 1
ais_search_zj/web/src/router/routes.js

@@ -61,7 +61,7 @@ export default [
           },
           {
             path: 'document',
-            name: 'Document',
+            name: 'Document1',
             meta: { title: 'ai文档' },
             component: () => import('@/views/document/index.vue'),
           },
@@ -79,6 +79,12 @@ export default [
           }
         ]
       },
+      {
+        path: 'document',
+        name: 'Document',
+        meta: { title: 'ai知识库' },
+        component: () => import('@/views/document/index2.vue'),
+      },
       {
         path: 'ai-search',
         name: 'AiSearch',

+ 134 - 0
ais_search_zj/web/src/views/document/index2.scss

@@ -0,0 +1,134 @@
+
+.document_box {
+  height: 100%;
+  .header {
+    width: 100%;
+    height: 60px;
+    background: #F4F6F9;
+  }
+  .top_btn {
+    height: 36px;
+  }
+  .m-l-15 {
+    margin-left: 15px;
+  }
+  >.my-doc-container {
+    width: 100%;
+    height: calc(100% - 60px - 50px);
+    box-sizing: border-box;
+    background-color: #f2f5fa;
+    font-family: PingFang SC, PingFang SC;
+    display: flex;
+    >div {
+      &.left_box {
+        background-color: #fff;
+        width: 260px;
+        padding-bottom: 0px;
+        display: flex;
+        flex-direction: column;
+        border-right: 1px solid #DBDFE5;
+        >.type_item {
+          padding: 10px;
+          cursor: pointer;
+          user-select: none;
+          >.btn {
+            font-size: 14px;
+            padding: 12px 15px;
+            border-radius: 6px 6px 6px 6px;
+          }
+        }
+        >.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;
+          padding: 10px 15px;
+          ::v-deep {
+            .ant-menu-light {
+              background-color: transparent;
+            }
+          }
+        }
+      }
+      &.content_box {
+        padding-left: 0px;
+        flex: 1;
+        >.page-content {
+          background-color: #fff;
+          border-radius: 15px;
+          overflow-y: auto;
+          width: 100%;
+          height: 100%;
+          padding: 30px;
+          display: flex;
+          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;
+                width: 450px;
+                overflow: hidden;
+                text-overflow: ellipsis;
+              }
+              &:hover {
+                color: #1890ff;
+              }
+            }
+          }
+          .pagination-box {
+            display: flex;
+            align-items: center;
+            justify-content: space-between;
+          }
+        }
+      }
+    }
+  }
+  ::v-deep {
+    th.ant-table-cell {
+      &::before {
+        width: 0px !important;
+      }
+    }
+    .ant-table-column-sorters {
+      justify-content: flex-start;
+    }
+    .ant-table-column-title {
+      flex: none;
+    }
+  }
+}

+ 586 - 0
ais_search_zj/web/src/views/document/index2.vue

@@ -0,0 +1,586 @@
+<template>
+  <div class="document_box">
+    <a-affix>
+      <div class="header">
+        <home-header @login="emits('login')"  sub-title="自然资源大模型" :link="false" />
+      </div>
+    </a-affix>
+    <FileDetail
+      v-if="fileViewerVisabled"
+      :fileDetail="currentFile"
+      :fileType="fileType"
+      :changeFile="showFileDetail"
+      @close="closeViewerHandle"
+    />
+    <div class="my-doc-container">
+      <div class="left_box">
+        <div class="type_item" v-if="isFolderEditor" @click="()=>{onMenuClickHandle({type: 'add', payload: {'id': 0}})}">
+          <div class="btn">新建一级类型</div>
+        </div>
+        <div class="menu_box">
+          <menus
+            mode="inline"
+            :items="folders"
+            :editor="isFolderEditor"
+            :current-id="currentId"
+            @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"
+                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=""> 全部</a-menu-item>
+                    <a-menu-item key="1"> 是</a-menu-item>
+                    <a-menu-item key="-1"> 否</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="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 class="name-box" @click="showFileDetail(record)" :title="record.name">
+                        <MyIcon icon="icon-wenjianleixing-suolvetu-PDFwendang" size="18" />
+                        <span>{{ record.name }}</span>
+                      </div>
+                    </div>
+                  </div>
+                </template>
+                <template v-if="column.key === 'typeName'">
+                  <div v-if="record.typeName === '未分类'" style="color: #EF740B">
+                    {{ record.typeName }}
+                  </div>
+                  <div v-else>
+                    {{ record.typeName }}
+                  </div>
+                </template>
+                <template v-if="column.key === 'size'">
+                  <div>
+                    {{record.size ? (record.size / 1024 / 1024).toFixed(2) : 0}}M
+                  </div>
+                </template>
+                <template v-if="column.key === 'createTime'">
+                  <div>
+                    {{ record.createTime && 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)"
+                            v-if="false"
+                            >编辑</a-button
+                          >
+                        </a-menu-item>
+                      </a-menu>
+                    </template>
+                  </a-dropdown>
+                </template>
+              </template>
+            </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>
+    </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>
+/**
+ * @description 我的文档
+ */
+import { cloneDeep } from 'lodash-es';
+import { h, ref, reactive, onMounted } from "vue";
+import http from "@/utils/http";
+import { findPath } from "@/utils/common";
+import { listToTree } from '@/utils/struct'
+import {
+  DownOutlined,
+  FormOutlined,
+  DeleteOutlined,
+  CloudUploadOutlined,
+  SearchOutlined,
+  DownloadOutlined,
+  VerticalAlignTopOutlined,
+  DeliveredProcedureOutlined,
+  ConsoleSqlOutlined,
+} from "@ant-design/icons-vue";
+import { message } from "ant-design-vue";
+import HomeHeader from '@/views/home/components/HomeHeader.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 dayjs from "dayjs";
+
+const currentId = ref(-1);
+const isType = ref(0);
+const onChangeBusClass = (data) => {
+  const key = data['key']
+  if (!key) {
+    isType.value = 0
+  } else {
+    isType.value = parseInt(key)
+  }
+  initTableList();
+}
+// 表格列
+const columns = [
+  {
+    title: "文档名称",
+    dataIndex: "name",
+    key: "name",
+    width: 480,
+    ellipsis: true
+  },
+  {
+    title: "文档类型",
+    dataIndex: "type",
+    key: "type",
+    width: 200
+  },
+  {
+    title: "业务类型",
+    dataIndex: "typeName",
+    key: "typeName",
+    width: 200
+  },
+  {
+    title: "文档大小",
+    dataIndex: "size",
+    key: "size",
+    width: 200,
+    sorter: {
+      compare: (a, b) => a.size - b.size,
+      multiple: 2,
+    },
+  },
+  {
+    title: "创建时间",
+    dataIndex: "createTime",
+    key: "createTime",
+    width: 160,
+    align: "center",
+  },
+  {
+    title: "操作",
+    key: "action",
+    width: 250,
+    align: "center",
+  },
+];
+const searchValue = ref("");
+const dataSource = ref<any[]>([]);
+const pageNo = ref(1)
+const total = ref(0)
+const loading = ref(false)
+const getTableList = () => {
+  const urlStr = `/ai/knowledge/file/page`
+  loading.value = true;
+  const sendData = {
+    pageNo: pageNo.value,
+    pageSize: 10,
+    name: searchValue.value,
+    isType: isType.value,
+    parentId: currentId.value === -1 ? '' : currentId.value
+  }
+  http.get(urlStr, sendData).then((result) => {
+    loading.value = false;
+    const resultData = result.data;
+    if (resultData['list']) {
+      dataSource.value = resultData['list'].map((item) => {
+        return {
+          ...item,
+          typeName: filterTypeFunc(item['parentId'])
+        }
+      });
+      total.value = resultData['total']
+    }
+  }).catch((err) => {
+    loading.value = false;
+  });
+};
+const filterTypeFunc = (parentId) => {
+  if (parentId == -1) return '未分类'
+  return findPath(folders.value, parentId)
+}
+const initTableList = () => {
+  pageNo.value = 1;
+  getTableList();
+}
+getTableList();
+
+const onPageChange = (page) => {
+  pageNo.value = page;
+  getTableList();
+}
+const state = reactive({
+  selectedRowKeys: [], // Check here to configure the default column
+  loading: false,
+});
+
+const onSelectChange = (selectedRowKeys1) => {
+  state.selectedRowKeys = selectedRowKeys1;
+};
+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)
+    }
+  })
+}
+getTypeList();
+// 新建文件夹
+const folderName = ref("");
+const openNewFolderModel = ref(false);
+const createFolder = async () => {
+  const urlStr = '/ai/knowledge/type/create'
+  const formData = new FormData();
+  formData.append("name", folderName.value)
+  formData.append("parentId", currentId.value);
+  http.post(urlStr, formData).then((result) => {
+    if (result.data) {
+      message.success("文件夹新建成功!");
+      folderName.value = "";
+      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']
+  switch (type) {
+    case 'query':
+      searchValue.value = ''
+      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) => {
+  console.log(data)
+  const downloadUrl = `${window.AppGlobalConfig.knowledgeDocUrlProxy}${data.name}`
+  const response = await fetch(downloadUrl);
+  const blob = await response.blob();
+  const url = window.URL.createObjectURL(blob);
+  const a = document.createElement("a");
+  a.href = url;
+  a.download = data.name ?? "download.pdf"; // 强制下载并指定文件名
+  document.body.appendChild(a);
+  a.click();
+  document.body.removeChild(a);
+  // 释放 Blob URL
+  window.URL.revokeObjectURL(url);
+};
+//文件导入
+// 上传文档
+const userUploadFileRef = ref<any>(null);
+const uploadFiles = () => userUploadFileRef.value.showModal();
+const closeHandle = () => {
+  getTableList();
+}
+// 多文件打包下载
+const downloadFiles = async (ids: any[] = []) => {
+  if (!ids || ids.length === 0) {
+    message.error("请至少选择一个文件!");
+    return;
+  }
+  message.success("正在为你下载文件,请稍等 ...");
+  const selectedArrs = dataSource.value.filter((item) => {
+    return ids.includes(item['id']);
+  })
+  selectedArrs.forEach((item) => {
+    downloadFile(item)
+  })
+};
+// 置顶
+const topFileHandle = async (item) => {
+  const urlStr = `/ai/knowledge/file/update`
+  const sendData = {
+    ...item
+  }
+  sendData['sort'] = 0
+  http.post(urlStr, sendData).then((result) => {
+    if (result.data) {
+      message.success("置顶成功!");
+      getTableList();
+    } else {
+      message.error("置顶失败,请稍后重试!");
+    }
+  });
+};
+// 移动:最外层文件禁止多选移动,文件夹类型禁止移动
+const moveFileModel = ref(false);
+const moveFileIds = ref<any[]>([]);
+const moveFiles = (ids: any[] = []) => {
+  moveFileIds.value = ids;
+  moveFileModel.value = true;
+};
+const closeMoveFileModel = (bool) => {
+  moveFileModel.value = false;
+  if (!bool) return;
+  moveFileIds.value = [];
+  getTableList();
+};
+// 跳到文件详情页面
+const fileViewerVisabled = ref(false)
+const fileType = ref('')
+const currentFile = ref()
+const showFileDetail = (data: any) => {
+  console.log(data)
+  fileViewerVisabled.value = true;
+  data['viewerUrl'] = `${window.AppGlobalConfig.knowledgeDocUrlProxy}${data.name}`
+  currentFile.value = data;
+  const name = data.name;
+  fileType.value = name.slice(name.indexOf(".") + 1)
+};
+const closeViewerHandle = () => {
+  fileViewerVisabled.value = false;
+}
+</script>
+<style scoped lang="scss">
+@import "./index.scss";
+</style>

+ 10 - 1
ais_search_zj/web/src/views/home/components/HomeHeader.vue

@@ -1,7 +1,7 @@
 <template>
   <div class="home-header">
     <div class="left">
-      <div class="sys-icon" @click="linkToHome">
+      <div class="sys-icon" @click="toLinkHandle">
         <div class="iconfont icon-a-zu11198"></div>
         <div class="title">{{ subTitle }}</div>
       </div>
@@ -24,8 +24,17 @@ const props = defineProps({
   subTitle: {
     type: String,
     default: ''
+  },
+  link: {
+    type: Boolean,
+    default: true
   }
 })
+const toLinkHandle = () => {
+  if (props.link) {
+    linkToHome();
+  }
+}
 </script>
 <style scoped lang="scss">
 .home-header {

+ 6 - 0
ais_search_zj/web/src/views/home/index.vue

@@ -60,6 +60,12 @@ const menuRoutes = [
   {
     title: '知识库',
     icon: 'icon-66zhishikuguanli',
+    name: 'Document1',
+    iframe: false
+  },
+  {
+    title: '个人知识库',
+    icon: 'icon-66zhishikuguanli',
     name: 'Document',
     iframe: false
   },