Przeglądaj źródła

知识库基本页面搭建

songxy 4 miesięcy temu
rodzic
commit
553cb3e0a6

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

@@ -17,6 +17,7 @@ declare module 'vue' {
     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']
+    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']

+ 2 - 1
ais_search/web/public/config.js

@@ -15,7 +15,8 @@
       '2': 15,
     },
 
-    server: 'https://natureai.zjugis.com/server',
+    // server: 'https://natureai.zjugis.com/server',
+    server: '/server',
     managerServer: 'https://natureai.zjugis.com/manager/server',
     chatServer: '/chat',
     knowledgeServer: 'https://natureai.zjugis.com/llm',

BIN
ais_search/web/public/images/document/cover.png


BIN
ais_search/web/public/images/document/icon-ai-g.gif


BIN
ais_search/web/public/images/document/icon-ai-title.png


BIN
ais_search/web/public/images/document/icon-qes.png


BIN
ais_search/web/public/images/document/icon-scale.png


BIN
ais_search/web/public/images/document/icon-white-send.png


BIN
ais_search/web/public/images/document/user.png


+ 50 - 0
ais_search/web/src/components/myIcon/index.vue

@@ -0,0 +1,50 @@
+<template>
+  <svg
+    :style="{
+      ...props.style,
+      width: `${props.size ?? ''}${props.unit}`,
+      height: `${props.size ?? ''}${props.unit}`,
+      fill: props.color ?? '#000000',
+    }"
+    :class="`icon ww-svg-icon ${props.className}`"
+    aria-hidden="true"
+  >
+    <use :xlink:href="`#${props.icon}`"></use>
+  </svg>
+</template>
+
+<script setup lang="ts">
+import "@/assets/iconfont/iconfont";
+
+interface Props {
+  icon: string; // 图标id
+  size?: string | number; // 图标大小
+  color?: string; // 图标颜色
+  unit?: "rem" | "px";
+  style?: object;
+  className?: string;
+}
+const props = withDefaults(defineProps<Props>(), {
+  icon: "",
+  size: 24,
+  color: "",
+  unit: "px",
+  style: () => {
+    return {
+      width: 10,
+      height: 10,
+      color: "",
+    };
+  },
+  className: "",
+});
+</script>
+
+<style lang="scss" scoped>
+@import "@/assets/iconfont/iconfont";
+
+.ww-svg-icon {
+  width: 24px;
+  height: 24px;
+}
+</style>

+ 6 - 0
ais_search/web/src/router/routes.js

@@ -16,6 +16,12 @@ export default [
         meta: { title: 'ai搜索' },
         component: () => import('@/views/ai-search/ai-search.vue'),
       },
+      {
+        path: 'document',
+        name: 'Document',
+        meta: { title: 'ai文档' },
+        component: () => import('@/views/document/index.vue'),
+      },
       // {
       //   path: '/register',
       //   name: 'register',

+ 26 - 1
ais_search/web/src/utils/common.js

@@ -1,5 +1,30 @@
-import {nodeEventSource} from "@llm-eaf/node-event-source";
+import { nodeEventSource } from "@llm-eaf/node-event-source";
 
+export const getRealFilePath = (
+  path,
+  type
+) => {
+  let jumpUrl = "";
+  if (!path) return jumpUrl;
+  const windowConfig = (window).AppGlobalConfig;
+  if (type === "user") {
+    jumpUrl = path.replace(
+      windowConfig.userFilePdfUrl,
+      windowConfig.userUploadFile
+    );
+  } else if (type === "knowledge") {
+    jumpUrl = path.replace(
+      windowConfig.knowledgeDocUrl,
+      windowConfig.knowledgeDocUrlProxy
+    );
+  } else {
+    jumpUrl = path.replace(windowConfig.filePdfUrl, windowConfig.fileLibrary);
+  }
+  // console.log("jumpUrl", jumpUrl);
+  return jumpUrl;
+  // 转码一下,防止有些特殊字符导致跳转失败(会出错)
+  return encodeURIComponent(jumpUrl);
+};
 export const isEmptyStr = str => {
   return !str || (str.trim && str.trim().length === 0);
 }

+ 153 - 0
ais_search/web/src/utils/http.ts

@@ -0,0 +1,153 @@
+import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from "axios";
+
+const windowConfig = (window as any).AppGlobalConfig;
+
+// 创建 axios 实例
+const http: AxiosInstance = axios.create({
+  baseURL: windowConfig.server,
+  timeout: 30000, // 请求超时时间
+});
+
+const Authorization = windowConfig?.authToken ?? "Authorization";
+// 请求拦截器
+http.interceptors.request.use(
+  (config: any) => {
+    const token = localStorage.getItem("token");
+    if (token) {
+      config.headers = {
+        ...config.headers,
+        [Authorization]: token,
+      };
+    }
+    return config;
+  },
+  (error) => Promise.reject(error)
+);
+
+// 响应拦截器
+http.interceptors.response.use(
+  (response: AxiosResponse) => {
+    // 检查响应类型,如果是文件类型,直接返回
+    const contentType = response.headers["content-type"];
+    if (
+      (contentType && contentType.includes("application/octet-stream")) ||
+      response.config.responseType === "blob"
+    ) {
+      return response; // 直接返回文件数据
+    }
+
+    // 大模型类接口请求体,直接返回全部内容
+    if (response.status && response.status == 200) {
+      return response.data; // 直接返回有效数据
+    }
+
+    // 处理非文件类型的响应
+    if (response.data.code !== 200) {
+      console.error(`Error: ${response.data.message}`);
+      return Promise.reject(new Error(response.data.message));
+    }
+
+    return response.data;
+  },
+  (error) => {
+    console.error(error.response?.data || error.message);
+    return Promise.reject(error);
+  }
+);
+
+// 封装实例方法
+class HttpRequest {
+  private instance: AxiosInstance;
+
+  constructor(instance: AxiosInstance) {
+    this.instance = instance;
+  }
+
+  get<T = any>(
+    url: string,
+    params?: Record<string, any>,
+    config?: AxiosRequestConfig
+  ): Promise<T> {
+    return this.instance
+      .get<T>(url, { ...config, params })
+      .then((response) => response as T);
+  }
+
+  post<T = any>(
+    url: string,
+    data?: Record<string, any>,
+    config?: AxiosRequestConfig
+  ): Promise<T> {
+    return this.instance
+      .post<T>(url, data, config)
+      .then((response) => response as T);
+  }
+
+  delete<T = any>(url: string, config?: AxiosRequestConfig): Promise<T> {
+    return this.instance
+      .delete<T>(url, config)
+      .then((response) => response as T);
+  }
+
+  // 下载文件方法,支持 GET 和 POST
+  download(
+    method: "GET" | "POST",
+    url: string,
+    options: {
+      params?: Record<string, any>; // get 请求参数
+      data?: Record<string, any>; // post 请求参数
+      config?: AxiosRequestConfig; // Axios 配置
+    } = {}
+  ): Promise<void> {
+    const { params, data, config } = options;
+
+    // 设置响应类型为 blob
+    const axiosConfig: AxiosRequestConfig = {
+      ...config,
+      responseType: "blob",
+    };
+
+    // 选择请求方法
+    const request =
+      method === "GET"
+        ? this.instance.get(url, { ...axiosConfig, params })
+        : this.instance.post(url, data, axiosConfig);
+
+    return request
+      .then((response) => {
+        console.log("response", response);
+        const blob = new Blob([response.data]);
+        const contentDisposition = response.headers["content-disposition"];
+        const defaultFileName = "downloaded-file.zip"; // 默认文件名
+        let fileName = defaultFileName;
+
+        // 从 Content-Disposition 获取文件名
+        if (contentDisposition) {
+          const match = contentDisposition.match(/filename="?(.+)"?/);
+          if (match && match[1]) {
+            fileName = decodeURIComponent(match[1]);
+          }
+        }
+
+        // 创建下载链接
+        const downloadUrl = window.URL.createObjectURL(blob);
+        const a = document.createElement("a");
+        a.href = downloadUrl;
+        a.download = fileName;
+        a.style.display = "none";
+        document.body.appendChild(a);
+        a.click();
+        window.URL.revokeObjectURL(downloadUrl); // 释放 URL
+        document.body.removeChild(a);
+      })
+      .catch((error) => {
+        console.error("Download failed:", error);
+        return Promise.reject(error);
+      });
+  }
+}
+
+// 实例化封装
+const api = new HttpRequest(http);
+
+export default api;

+ 124 - 0
ais_search/web/src/views/components/PDFReader.vue

@@ -0,0 +1,124 @@
+<template>
+  <div style="width: 100%; height: 100%; position: relative; overflow: hidden">
+    <iframe
+      :src="src"
+      width="100%"
+      height="100%"
+      style="position: relative; border: none"
+      ref="iframeRef"
+      @load="onLoad"
+    ></iframe>
+  </div>
+</template>
+
+<script setup>
+import { message } from "ant-design-vue";
+import { ref, computed } from "vue";
+
+const iframeRef = ref(null);
+const props = defineProps({
+  src: String,
+});
+
+// 计算 iframe 的源 URL
+const src = computed(() => {
+  let sanitizedSrc = props.src.replaceAll("&", "%26");
+  return `/write/lib/pdfjs/web/viewer.html?file=${sanitizedSrc}&t=${new Date().getTime()}`;
+});
+
+// iframe 加载完成后的处理
+const onLoad = () => {
+  setTimeout(() => {
+    const doc =
+      iframeRef.value.contentDocument || iframeRef.value.contentWindow.document;
+
+    // 确保 PDF.js 的对象已加载
+    if (!doc || !doc.defaultView.PDFViewerApplication) {
+      console.error("PDF.js 未正确加载");
+      return;
+    }
+  }, 2000);
+};
+
+// 调用 PDF.js 的查找方法
+const searchText = (txt) => {
+  const doc =
+    iframeRef.value.contentDocument || iframeRef.value.contentWindow.document;
+
+  if (!doc || !doc.defaultView.PDFViewerApplication) {
+    console.error("PDF 未正确加载");
+    return;
+  }
+
+  const PDFViewerApplication = doc.defaultView.PDFViewerApplication;
+
+  // 确保文本查找控件可用
+  if (!PDFViewerApplication.findBar || !PDFViewerApplication.findController) {
+    console.error("PDF 查找功能未正确初始化");
+    return;
+  }
+
+  // 设置搜索关键字并触发搜索
+  // PDFViewerApplication.findBar.open(); // 打开查找工具栏(可选)
+
+  // 确保 findBar 存在
+  const findBar = PDFViewerApplication.findBar;
+  if (!findBar) {
+    // console.error("findBar 未找到");
+    return;
+  }
+
+  PDFViewerApplication.findBar.open(); // 打开查找工具栏
+  // 隐藏工具条但不关闭查找功能
+  findBar.bar.classList.add("hidden");
+
+  // 填充搜索框并触发搜索
+  const findInput = findBar.findField; // 获取搜索输入框
+  findInput.value = txt;
+
+  // 手动触发输入事件,模拟用户操作
+  const event = new Event("input", { bubbles: true, cancelable: true });
+  findInput.dispatchEvent(event);
+
+  // 监听查找结果
+  const findController = PDFViewerApplication.findController;
+  const checkMatch = setTimeout(() => {
+    // 检查当前匹配数量
+    const matchCount = findController?._matchesCountTotal;
+
+    if (matchCount === 0) {
+      clearTimeout(checkMatch);
+      // 弹框提示
+      message.info("未匹配到相关内容!");
+      return;
+    }
+
+    if (matchCount > 0) {
+      clearTimeout(checkMatch);
+    }
+  }, 500);
+};
+
+// 移除高亮(可选功能)
+const removeSourceHighlight = () => {
+  const doc =
+    iframeRef.value.contentDocument || iframeRef.value.contentWindow.document;
+
+  if (!doc || !doc.defaultView.PDFViewerApplication) {
+    // console.error("PDF.js 未正确加载");
+    return;
+  }
+
+  const PDFViewerApplication = doc.defaultView.PDFViewerApplication;
+
+  // 清空查找结果
+  PDFViewerApplication.findController.executeCommand("find", {
+    query: "",
+  });
+};
+
+defineExpose({
+  searchText,
+  removeSourceHighlight,
+});
+</script>

+ 68 - 0
ais_search/web/src/views/components/title.vue

@@ -0,0 +1,68 @@
+<template>
+  <div class="title-container">
+    <div class="left-box">
+      <div
+        class="t-item"
+        :class="{ checked: t.checked }"
+        v-for="(t, i) in props.items"
+        :key="i"
+        @click="t.onClick && t.onClick()"
+      >
+        <span v-if="i > 0"> <RightOutlined class="icon" /></span>
+        {{ t.name }}
+      </div>
+    </div>
+  </div>
+</template>
+<script setup>
+import { RightOutlined } from "@ant-design/icons-vue";
+
+const props = defineProps({
+  items:
+    Array[
+      {
+        name: String,
+        checked: Boolean,
+        onClick: Function,
+      }
+    ],
+});
+</script>
+<style lang="scss" scoped>
+.title-container {
+  height: 60px;
+  width: 100%;
+  padding: 0px 30px;
+  display: flex;
+  align-items: center;
+  border-bottom: 1px solid #f1f2f4;
+  justify-content: space-between;
+  .left-box {
+    display: flex;
+    align-items: center;
+    .t-item {
+      font-family: PingFang SC, PingFang SC;
+      font-weight: 400;
+      font-size: 15px;
+      color: #82889b;
+      cursor: pointer;
+      .icon {
+        width: 14px;
+        height: 24px;
+        color: #82889b;
+        margin: 0px 11px 0px 14px;
+      }
+    }
+    .checked {
+      color: #0f0f0f;
+      .icon {
+        color: #0f0f0f;
+      }
+    }
+  }
+  .avatar-box {
+    width: 500px;
+    height: 60px;
+  }
+}
+</style>

+ 123 - 0
ais_search/web/src/views/document/FileDetail.vue

@@ -0,0 +1,123 @@
+<template>
+  <div class="file-detail">
+    <div class="center-box">
+      <div class="center-top">
+        <div
+          class="tool-btn"
+          style="color: #ff9900"
+          v-if="fileDetail.isCollected"
+          @click="cancelStarThisFile"
+        >
+          <StarFilled />
+          <span class="star-file-text">取消收藏</span>
+        </div>
+        <div class="tool-btn" @click="starThisFile" v-else>
+          <StarOutlined />
+          <span>收藏文档</span>
+        </div>
+        <div
+          class="tool-btn"
+          @click="
+            downloadFile({
+              ...fileDetail,
+              fileStorepath: pdfSrc,
+            })
+          "
+        >
+          <DownloadOutlined />
+          <span>下载</span>
+        </div>
+      </div>
+      <div class="pdf-box" id="scrollArea1">
+        <PDFReader
+          v-if="fileDetail"
+          ref="pdfViewer"
+          :src="getRealFilePath(fileDetail.fileStorepath)"
+        />
+      </div>
+    </div>
+  </div>
+</template>
+<script lang="ts" setup>
+/**
+ * @description 文件详情
+ */
+import { ref, onMounted, watch, toRefs } from "vue";
+import {
+  StarOutlined,
+  DownloadOutlined,
+  StarFilled,
+} from "@ant-design/icons-vue";
+import PDFReader from "../components/PDFReader.vue";
+import { getRealFilePath } from '@/utils/common.js';
+
+const pdfViewer = ref<any>(null);
+
+const props = defineProps({
+  // 文件详情
+  fileDetail: {
+    type: Object,
+    default: {},
+  }
+});
+const { fileDetail } = toRefs(props);
+const pdfSrc = ref("");
+onMounted(() => {
+  getDocDetail();
+});
+watch(
+  () => fileDetail.value,
+  () => {
+    getDocDetail();
+  }
+);
+const getDocDetail = () => {
+  const src = fileDetail.value.fileStorepath;
+  pdfSrc.value = getRealFilePath(src);
+
+};
+
+const downloadFile = () => {
+
+}
+</script>
+<style scoped lang="scss">
+.file-detail {
+  width: 100%;
+  height: calc(100% - 60px);
+  display: flex;
+  .center-box {
+    flex: 1;
+    .center-top {
+      width: 100%;
+      height: 50px;
+      display: flex;
+      justify-content: flex-end;
+      align-items: center;
+      padding-right: 20px;
+      .tool-btn {
+        font-weight: 500;
+        font-size: 14px;
+        color: #172538;
+        margin-left: 40px;
+        cursor: pointer;
+        span {
+          margin-left: 6px;
+        }
+        // .star-file-text::after {
+        //   content: "已收藏";
+        // }
+        // .star-file-text:hover::after {
+        //   content: "取消收藏";
+        // }
+      }
+    }
+    .pdf-box {
+      width: 100%;
+      height: calc(100% - 55px);
+      background: white;
+      overflow-y: auto;
+    }
+  }
+}
+</style>

+ 126 - 0
ais_search/web/src/views/document/FileUpload.vue

@@ -0,0 +1,126 @@
+<template>
+  <a-modal
+    v-model:open="open"
+    title="上传文档"
+    :footer="null"
+    width="600px"
+    :maskClosable="false"
+  >
+    <a-upload-dragger
+      style="margin: 20px 0 10px; width: 100%"
+      v-model:fileList="fileList"
+      name="file"
+      :multiple="true"
+      :maxCount="1"
+      accept=".pdf"
+      @change="handleChange"
+      @drop="handleDrop"
+      :beforeUpload="handleUpload"
+    >
+      <p class="ant-upload-drag-icon">
+        <inbox-outlined></inbox-outlined>
+      </p>
+      <p class="ant-upload-text">点击上传或拖入文档</p>
+      <p class="ant-upload-hint">文档单个最大50MB;当前仅支持PDF格式</p>
+    </a-upload-dragger>
+  </a-modal>
+</template>
+<script lang="ts" setup>
+/**
+ * @description 文件上传组件
+ */
+import { ref, defineExpose } from "vue";
+import { InboxOutlined } from "@ant-design/icons-vue";
+import { message } from "ant-design-vue";
+import type { UploadChangeParam } from "ant-design-vue";
+import { useUserStore } from "@/stores";
+import axios from "axios";
+import { useRouter, useRoute } from "vue-router";
+const router = useRouter();
+const route = useRoute();
+
+// 弹窗部分
+const open = ref<boolean>(false);
+const showModal = () => {
+  open.value = true;
+};
+defineExpose({ showModal });
+
+// 文件上传部分
+const fileList = ref<any[]>([]);
+const handleChange = (info: UploadChangeParam) => {
+  const status = info.file.status;
+  if (status === "done") {
+    message.success(`“${info.file.name}” 文件上传成功.`);
+  } else if (status === "error") {
+    message.error(`“${info.file.name}” 文件上传失败.`);
+  }
+};
+const handleDrop = (e: DragEvent) => {
+  console.log(e);
+};
+
+const store: any = useUserStore();
+const userId = store?.user?.user?.id ?? "";
+const handleUpload = async (file: any) => {
+  const formData = new FormData();
+  formData.append("userId", userId); // 用户id
+  formData.append("file", file); // 文件对象
+
+  const cleanAxios = axios.create();
+  // 在线上请求才不会出错
+  const uploadUrl =
+    (window as any).AppGlobalConfig.onlineAddress +
+    "/server/file/library/user/upload/file";
+  try {
+    // 调用接口上传文件
+    message.success("文件上传中,请稍等 ...");
+    const response = await cleanAxios.post(uploadUrl, formData, {
+      headers: {
+        "Content-Type": "multipart/form-data",
+      },
+    });
+
+    console.log("response", response);
+    // 上传成功处理
+    if (response.status === 200 && response.data.success) {
+      fileList.value = [
+        {
+          uid: file.uid, // 保留唯一标识
+          name: file.name, // 文件名
+          status: "done", // 上传状态
+          url: response.data.data, // 服务器返回的文件地址
+        },
+      ];
+      message.success("文件上传成功!即将为您跳转到我的上传页面 ...");
+      const timer = setTimeout(() => {
+        // 清空文件列表
+        fileList.value = [];
+        open.value = false;
+        if (route.name === "myUploadFiles") {
+          router
+            .replace({ path: "/home", query: { t: Date.now() } })
+            .then(() => {
+              router.replace({ name: "myUploadFiles" });
+            });
+        } else {
+          // 跳转到 "myUploadFiles" 页面
+          router.push({ name: "myUploadFiles" });
+        }
+        clearTimeout(timer);
+      }, 1500);
+    } else {
+      message.error("文件上传失败!");
+      fileList.value = [];
+    }
+  } catch (error) {
+    // 错误处理
+    message.error("文件上传失败,请稍后重试!");
+    fileList.value = [];
+    console.error(error);
+  }
+  // 返回 false,阻止默认上传行为
+  return false;
+};
+</script>
+<style scoped lang="scss"></style>

+ 104 - 0
ais_search/web/src/views/document/MoveFiles.vue

@@ -0,0 +1,104 @@
+<template>
+  <a-modal
+    v-model:open="open"
+    title="文件移动"
+    @ok="handleOk"
+    @cancel="closeModel"
+  >
+    <a-directory-tree
+      class="directory-tree"
+      v-model:expandedKeys="expandedKeys"
+      v-model:selectedKeys="selectedKeys"
+      :tree-data="treeData"
+      style="
+        border: 1px solid #e4e7ea;
+        border-radius: 4px;
+        padding: 10px;
+        max-height: 400px;
+        min-height: 200px;
+        overflow-y: scroll;
+      "
+    >
+      <template #icon="{ key, selected }">
+        <MyIcon
+          :icon="iconTypeName.dir"
+          size="18"
+          style="margin-right: 5px; margin-top: 2px"
+        />
+      </template>
+    </a-directory-tree>
+  </a-modal>
+</template>
+<script lang="ts" setup>
+/**
+ * @description 文件移动弹窗
+ */
+import { ref, onMounted, watch, toRefs, PropType } from "vue";
+import { message } from "ant-design-vue";
+import MyIcon from "@/components/myIcon/index.vue";
+import { getAllFolder } from "./http";
+import { iconTypeName } from "./config";
+import { moveFiles } from "./http";
+const props = defineProps({
+  ids: {
+    type: Array as PropType<string[]>,
+    default: [],
+  },
+  open: {
+    type: Boolean,
+    default: false,
+  },
+  closeModel: {
+    type: Function,
+    default: () => {},
+  },
+});
+const { ids, open, closeModel } = toRefs(props);
+
+const expandedKeys = ref<string[]>(["root"]);
+const selectedKeys = ref<string[]>([]);
+const treeData = ref([
+  {
+    title: "我的文档",
+    key: "root",
+    children: [],
+  },
+]);
+
+onMounted(() => {
+  initModel();
+});
+
+watch(
+  () => open.value,
+  () => {
+    initModel();
+  }
+);
+const initModel = () => {
+  selectedKeys.value = [];
+  getAllFolder().then((res) => {
+    console.log(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("文件移动失败,请稍后重试!");
+  }
+};
+</script>
+<style scoped lang="scss"></style>

+ 64 - 0
ais_search/web/src/views/document/TitleHeader.vue

@@ -0,0 +1,64 @@
+<template>
+  <div class="title-container">
+    <div class="left-box">
+      <div
+        class="t-item"
+        :class="{ checked: t.checked }"
+        v-for="(t, i) in props.items"
+        :key="i"
+        @click="t.onClick && t.onClick()"
+      >
+        <span v-if="i > 0"> <RightOutlined class="icon" /></span>
+        {{ t.name }}
+      </div>
+    </div>
+  </div>
+</template>
+<script setup>
+import { RightOutlined } from "@ant-design/icons-vue";
+
+const props = defineProps({
+  items:
+    Array[
+      {
+        name: String,
+        checked: Boolean,
+        onClick: Function,
+      }
+    ],
+});
+</script>
+<style lang="scss" scoped>
+.title-container {
+  height: 60px;
+  width: 100%;
+  padding: 0px 30px;
+  display: flex;
+  align-items: center;
+  border-bottom: 1px solid #f1f2f4;
+  justify-content: space-between;
+  .left-box {
+    display: flex;
+    align-items: center;
+    .t-item {
+      font-family: PingFang SC, PingFang SC;
+      font-weight: 400;
+      font-size: 15px;
+      color: #82889b;
+      cursor: pointer;
+      .icon {
+        width: 14px;
+        height: 24px;
+        color: #82889b;
+        margin: 0px 11px 0px 14px;
+      }
+    }
+    .checked {
+      color: #0f0f0f;
+      .icon {
+        color: #0f0f0f;
+      }
+    }
+  }
+}
+</style>

+ 40 - 0
ais_search/web/src/views/document/config.ts

@@ -0,0 +1,40 @@
+// 图标对应图标库id
+export const iconTypeName: any = {
+  dir: "icon-wjj",
+  pdf: "icon-wenjianleixing-suolvetu-PDFwendang",
+  word: "icon-word",
+  text: "icon-txt",
+  ppt: "icon-ppt",
+};
+
+// 表格列
+export const columns = [
+  {
+    title: "文档名称",
+    dataIndex: "fileName",
+    key: "fileName",
+  },
+  {
+    title: "知识领域",
+    dataIndex: "fileBizline",
+    key: "fileBizline",
+  },
+  {
+    title: "文章体裁",
+    dataIndex: "fileFormat",
+    key: "fileFormat",
+  },
+  {
+    title: "收藏时间",
+    dataIndex: "createTime",
+    key: "createTime",
+    width: 200,
+    align: "center",
+  },
+  {
+    title: "操作",
+    key: "action",
+    width: 150,
+    align: "center",
+  },
+];

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

@@ -0,0 +1,64 @@
+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 请求体
+  });

+ 48 - 0
ais_search/web/src/views/document/index.scss

@@ -0,0 +1,48 @@
+
+.page-doc {
+  border: 1px solid #f00;
+  height: 100%;
+  >.header {
+    width: 100%;
+    height: 60px;
+  }
+  >.my-doc-container {
+    width: 100%;
+    height: calc(100% - 60px);
+    background-color: #fff;
+    font-family: PingFang SC, PingFang SC;
+    .page-content {
+      width: 100%;
+      padding: 30px;
+      .content-top {
+        display: flex;
+        justify-content: space-between;
+        .search-icon {
+          font-size: 18px;
+          color: #a8aeb7;
+          &:hover {
+            color: #4096ff;
+          }
+        }
+      }
+      .content-table {
+        width: 100%;
+        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;
+          }
+        }
+      }
+    }
+  }
+}

+ 366 - 0
ais_search/web/src/views/document/index.vue

@@ -0,0 +1,366 @@
+<template>
+  <div class="page-doc">
+    <div class="header">
+      <home-header @login="emits('login')" />
+    </div>
+    <div class="my-doc-container">
+      <TitleHeader :items="menuList" />
+      <FileDetail
+        v-if="clickFileDetail && clickFileDetail.sign == 'file'"
+        :fileDetail="clickFileDetail"
+        :changeFile="showFileDetail"
+      />
+      <div class="page-content" v-else>
+        <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"
+              v-if="!clickFileDetail"
+              >新建文件夹</a-button
+            >
+            <a-button
+              :icon="h(DownloadOutlined)"
+              @click="downloadFiles(state.selectedRowKeys)"
+              style="margin-left: 15px; height: 36px"
+              >下载</a-button
+            >
+            <a-button
+              v-if="clickFileDetail"
+              :icon="h(DeliveredProcedureOutlined)"
+              @click="moveFiles(state.selectedRowKeys)"
+              style="margin-left: 15px; height: 36px"
+              >移动</a-button
+            >
+          </div>
+          <div class="right">
+            <a-input
+              v-model:value="searchValue"
+              placeholder="搜索文档"
+              style="width: 280px; height: 40px"
+              @pressEnter="getDataSource({ searchKey: searchValue })"
+            >
+              <template #suffix>
+                <SearchOutlined
+                  @click="getDataSource({ searchKey: searchValue })"
+                  style=""
+                  class="search-icon"
+                />
+              </template>
+            </a-input>
+          </div>
+        </div>
+        <div class="content-table">
+          <a-table
+            :rowSelection="{
+              selectedRowKeys: state.selectedRowKeys,
+              onChange: onSelectChange,
+            }"
+            :columns="columns"
+            :dataSource="dataSource"
+            :pagination="false"
+          >
+            <template #bodyCell="{ column, record }">
+              <template v-if="column.key === 'fileName'">
+                <div
+                  v-if="record.sign == 'dir'"
+                  class="name-box"
+                  @click="jumpToFile(record)"
+                >
+                  <MyIcon :icon="iconTypeName.dir" size="18" />
+                  <span>{{ record.fileName }}</span>
+                </div>
+                <div v-else class="name-box" @click="showFileDetail(record)">
+                  <MyIcon :icon="iconTypeName[record.fileType]" size="18" />
+                  <span>{{ record.fileName }}</span>
+                </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"
+                  @click="
+                    record.sign !== 'dir'
+                      ? 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 v-if="record.sign !== 'dir'">
+                        <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="topFile(record.id)"
+                          >置顶</a-button
+                        >
+                      </a-menu-item>
+                    </a-menu>
+                  </template>
+                </a-dropdown>
+              </template>
+            </template>
+          </a-table>
+        </div>
+      </div>
+      <a-modal v-model:open="openNewFolderModel" title="新建分组" @ok="newFolder">
+        <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" />
+    </div>
+  </div>
+</template>
+<script lang="ts" setup>
+/**
+ * @description 我的文档
+ */
+import { h, ref, reactive, onMounted } from "vue";
+import {
+  CloudUploadOutlined,
+  SearchOutlined,
+  DownloadOutlined,
+  FolderAddOutlined,
+  VerticalAlignTopOutlined,
+  DeliveredProcedureOutlined,
+} from "@ant-design/icons-vue";
+import HomeHeader from '@/views/home/components/HomeHeader.vue';
+import { message } from "ant-design-vue";
+import MyIcon from "@/components/myIcon/index.vue";
+import {
+  topDocs,
+  getDocList,
+  deleteDocs,
+  downloadDocs,
+  madeNewFolder,
+  isRepeatFolder
+} from "./http";
+import { columns, iconTypeName } from "./config";
+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 = ref<{
+  name: string,
+  id: string,
+  checked: boolean
+}[]>([
+  {
+    name: "我的收藏",
+    id: '',
+    checked: true
+  }
+])
+// 切换目录
+const changeMenu = (fileName: any = null) => {
+  // 切换目录时重置搜索内容和选中的文件
+  searchValue.value = "";
+  state.selectedRowKeys = [];
+
+  if (!fileName) {
+    // 清空选择打开的文件夹
+    clickFileDetail.value = null;
+    // 刷新列表
+    getDataSource();
+    return;
+  }
+  menuList.value.forEach((item) => {
+    item['checked'] = false;
+  })
+  menuList.value.push({
+    name: fileName,
+    id: '',
+    checked: true,
+  })
+};
+
+// 获取数据
+const searchValue = ref("");
+const dataSource = ref<any[]>([]);
+const getDataSource = (params = {}) => {
+  // 重置选中的文件
+  state.selectedRowKeys = [];
+  // 获取文档列表
+  getDocList(params).then((res) => {
+    if (res.success) {
+      dataSource.value = res.data.map((item: any) => ({
+        key: item.id,
+        ...item,
+      }));
+    }
+  });
+};
+
+const state = reactive<{
+  selectedRowKeys: any[];
+  loading: boolean;
+}>({
+  selectedRowKeys: [], // Check here to configure the default column
+  loading: false,
+});
+
+const onSelectChange = (selectedRowKeys: any[]) => {
+  state.selectedRowKeys = selectedRowKeys;
+};
+
+// 点击进入文件夹
+const clickFileDetail = ref<null | any>(null);
+const jumpToFile = (data: any) => {
+  clickFileDetail.value = data;
+  // 触发目录切换的回调
+  changeMenu(data.fileName);
+  getDataSource({
+    id: data.id,
+  });
+};
+
+// 新建文件夹
+const openNewFolderModel = ref(false);
+const folderName = ref("");
+const newFolder = async () => {
+  const res = await isRepeatFolder(folderName.value);
+  if (!res.success) {
+    message.info("文件夹名称重复,请重新输入!");
+    return false;
+  }
+  const res2 = await madeNewFolder(folderName.value);
+  if (res2.success) {
+    message.success("文件夹新建成功!");
+    folderName.value = "";
+    // 刷新列表
+    getDataSource();
+    openNewFolderModel.value = false;
+  } else {
+    message.error("新建文件夹失败,请稍后重试!");
+  }
+};
+
+// 下载(单文件下载)
+const downloadFile = async (data: any) => {
+  if (!data.fileStorepath) {
+    message.error("下载失败,未找到文件下载地址!");
+    return;
+  }
+  return;
+  const blob = await response.blob();
+  const url = window.URL.createObjectURL(blob);
+  const a = document.createElement("a");
+  a.href = url;
+  a.download = data.fileName ?? "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 downloadFiles = async (ids: any[] = []) => {
+  message.success("正在为你下载文件,请稍等 ...");
+  try {
+    await downloadDocs(ids);
+  } catch {
+    message.error("下载失败,请稍后重试!");
+  }
+};
+// 移动:最外层文件禁止多选移动,文件夹类型禁止移动
+const moveFileModel = ref(false);
+const moveFileIds = ref<any[]>([]);
+const moveFiles = (ids: any[] = []) => {
+  moveFileIds.value = ids;
+  moveFileModel.value = true;
+};
+const closeMoveFileModel = () => {
+  moveFileModel.value = false;
+  moveFileIds.value = [];
+  refreshList();
+};
+// 列表刷新
+const refreshList = () => {
+  if (clickFileDetail.value) {
+    // 关闭弹窗后刷新列表
+    getDataSource({
+      id: clickFileDetail.value.id,
+    });
+    // 刷新目录
+    changeMenu(clickFileDetail.value.fileName);
+  } else {
+    // 关闭弹窗后刷新列表
+    getDataSource();
+    // 刷新目录
+    changeMenu();
+  }
+};
+
+// 置顶
+const topFile = async (id: string) => {
+  const res = await topDocs(id);
+  if (res.success) {
+    message.success("置顶成功!");
+    // 刷新列表
+    refreshList();
+  } else {
+    message.error("置顶失败,请稍后重试!");
+  }
+};
+
+// 跳到文件详情页面
+const showFileDetail = (data: any) => {
+  console.log("跳到文件详情页面", data);
+  clickFileDetail.value = {
+    ...data,
+    id: data.fileId,
+  };
+  // 触发目录切换的回调
+  changeMenu(data.fileName);
+};
+</script>
+<style scoped lang="scss">
+@import "./index.scss";
+</style>

+ 9 - 3
ais_search/web/vite.config.js

@@ -41,10 +41,16 @@ export default defineConfig({
     cors: true,
     proxy: {
       '/server': {
-        target: 'http://localhost:7501',
-        changeOrigin: true,
-        rewrite: (path) => path.replace(/^\/server/, '')
+          target: 'http://121.40.148.47:8531/server',
+          // target: 'http://localhost:7503/server',
+          changeOrigin: true,
+          rewrite: function (path) { return path.replace(/^\/server/, ''); }
       },
+      // '/server': {
+      //   target: 'http://localhost:7501',
+      //   changeOrigin: true,
+      //   rewrite: (path) => path.replace(/^\/server/, '')
+      // },
       '/chat': {
         target: 'http://lq.lianqiai.cn:20333/chat',
         changeOrigin: true,