Przeglądaj źródła

feat: 智慧办公增加上传文件

sunxiao 7 miesięcy temu
rodzic
commit
db49c61266

+ 4 - 0
src/assets/svgs/chat/icon-file-active.svg

@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="28" height="29" viewBox="0 0 28 29" fill="none">
+    <path d="M11.5693 7.08568L12.2605 6.36294L12.2605 6.36294L11.5693 7.08568ZM10.7686 6.403L11.2742 5.54023H11.2742L10.7686 6.403ZM10.1258 6.14856L9.902 7.1232L10.1258 6.14856ZM16.7838 24.0898C17.3361 24.0898 17.7838 23.6421 17.7838 23.0898C17.7838 22.5376 17.3361 22.0898 16.7838 22.0898V24.0898ZM21.75 12.1252C21.75 12.6775 22.1977 13.1252 22.75 13.1252C23.3023 13.1252 23.75 12.6775 23.75 12.1252H21.75ZM12.1703 7.66031L11.4791 8.38305L12.1703 7.66031ZM13.5525 8.21484V9.21484V8.21484ZM3.75 21.0898V8.08984H1.75V21.0898H3.75ZM12.8614 6.93758L12.2605 6.36294L10.8782 7.80841L11.4791 8.38305L12.8614 6.93758ZM9.05494 5.08984H4.75V7.08984H9.05494V5.08984ZM12.2605 6.36294C11.9095 6.02732 11.6227 5.74446 11.2742 5.54023L10.263 7.26578C10.363 7.32438 10.4605 7.40899 10.8782 7.80841L12.2605 6.36294ZM9.05494 7.08984C9.64413 7.08984 9.78242 7.09575 9.902 7.1232L10.3495 5.17391C9.95761 5.08394 9.55282 5.08984 9.05494 5.08984V7.08984ZM11.2742 5.54023C10.9864 5.37159 10.6739 5.24839 10.3495 5.17391L9.902 7.1232C10.0311 7.15283 10.1531 7.20139 10.263 7.26578L11.2742 5.54023ZM13.5525 9.21484L20.75 9.21484V7.21484L13.5525 7.21484V9.21484ZM16.7838 22.0898H4.75V24.0898H16.7838V22.0898ZM21.75 10.2148V12.1252H23.75V10.2148H21.75ZM11.4791 8.38305C12.0374 8.9169 12.7801 9.21484 13.5525 9.21484V7.21484C13.295 7.21484 13.0475 7.11553 12.8614 6.93758L11.4791 8.38305ZM20.75 9.21484C21.3023 9.21484 21.75 9.66256 21.75 10.2148H23.75C23.75 8.55799 22.4069 7.21484 20.75 7.21484V9.21484ZM3.75 8.08984C3.75 7.53756 4.19771 7.08984 4.75 7.08984V5.08984C3.09314 5.08984 1.75 6.43299 1.75 8.08984H3.75ZM1.75 21.0898C1.75 22.7467 3.09315 24.0898 4.75 24.0898V22.0898C4.19772 22.0898 3.75 21.6421 3.75 21.0898H1.75Z" fill="#2454FF"/>
+    <path d="M22.75 15.8481V23.1106M22.75 15.8481L25.25 18.7354M22.75 15.8481L20.25 18.7354" stroke="#2454FF" stroke-width="2" stroke-linecap="round"/>
+<script xmlns=""/></svg>

+ 4 - 0
src/assets/svgs/chat/icon-file-default.svg

@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="28" height="29" viewBox="0 0 28 29" fill="none">
+    <path d="M16.7838 23.0898H4.75C3.64543 23.0898 2.75 22.1944 2.75 21.0898L2.75 8.08984C2.75 6.98527 3.64543 6.08984 4.75 6.08984H9.05494C9.59847 6.08984 9.87001 6.08984 10.1258 6.14856C10.3525 6.20061 10.5697 6.28649 10.7686 6.403C10.9928 6.53442 11.185 6.71816 11.5693 7.08568L12.1703 7.66031C12.5424 8.01621 13.0375 8.21484 13.5525 8.21484L20.75 8.21484C21.8546 8.21484 22.75 9.11027 22.75 10.2148V12.1252" stroke="#4F5866" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
+    <path d="M22.75 15.8481V23.1106M22.75 15.8481L25.25 18.7354M22.75 15.8481L20.25 18.7354" stroke="#4F5866" stroke-width="2" stroke-linecap="round" />
+<script xmlns=""/></svg>

+ 7 - 0
src/assets/svgs/chat/icon-file.svg

@@ -0,0 +1,7 @@
+<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M23.1961 2.25703H9.50625C6.76055 2.25703 4.53867 4.48242 4.53867 7.22461V28.7719C4.53867 31.5176 6.76406 33.7395 9.50625 33.7395H26.5746C29.3203 33.7395 31.5422 31.5141 31.5422 28.7719V10.6066C31.5422 9.48867 31.0992 8.41641 30.3082 7.62187L26.1773 3.49102C25.3863 2.7 24.3141 2.25703 23.1961 2.25703Z" fill="#2454FF"/>
+<path d="M30.2941 7.62188L26.1633 3.49102C25.5656 2.89336 24.8098 2.49609 23.9941 2.33438V6.97148C23.9941 8.44453 25.1859 9.63633 26.659 9.63633H31.4191C31.2363 8.87695 30.8531 8.17734 30.2941 7.62188Z" fill="#193FC4"/>
+<rect x="8" y="12" width="11" height="3" rx="1.5" fill="white"/>
+<rect x="8" y="18" width="20" height="3" rx="1.5" fill="white"/>
+<rect x="8" y="24" width="20" height="3" rx="1.5" fill="white"/>
+</svg>

+ 240 - 5
src/components/Chat/ChatAgentInput.vue

@@ -1,7 +1,8 @@
 <script setup>
 import { ref, unref, onMounted, onUnmounted, computed, watch } from 'vue';
-import { useMessage, NInput, NSwitch, NPopover, NScrollbar } from 'naive-ui';
-import dayjs from 'dayjs';
+import { useMessage, NInput, NSwitch, NPopover, NScrollbar, NUpload, NTooltip, NProgress } from 'naive-ui';
+import { useUserStore } from '@/stores/modules/userStore';
+import { baseURL } from '@/utils/request';
 import { getFormatYesterDay } from '@/utils/format';
 import { helperApi } from '@/api/helper';
 import SvgIcon from '@/components/SvgIcon';
@@ -19,6 +20,8 @@ const emit = defineEmits(['onClick', 'onEnter']);
 
 const MAX_NUM = 5;
 
+const useStore = useUserStore();
+
 const modelLoading = defineModel('loading');
 const switchStatus = defineModel('switch');
 
@@ -37,7 +40,11 @@ const popoverTriggerRef = ref(null);
 const popoverInnerRef = ref(null);
 const scrollRef = ref(null);
 
+const uploadFileList = ref([]);
+const uploadLoading = ref(false);
+
 const agentOptions = computed(() => helperList.value.filter(({ tools }) => tools));
+const lastFileListIndex = computed(() => uploadFileList.value.length == 0 ? 0 : uploadFileList.value.length - 1);
 
 const focusInput = _ => isFocusState.value = true;
 
@@ -80,9 +87,14 @@ const commonEmitEvent = (eventName) => {
     return message.warning('当前对话进行中');
   }
 
-  emit(eventName, { question: val, selectedOption: selectedOption.value || {} });
+  if ( uploadLoading.value ) {
+    return message.warning('文件上传中,请稍后');
+  }
+
+  emit(eventName, { question: val, selectedOption: selectedOption.value || {}, uploadFileList: uploadFileList.value });
 
   inpVal.value = '';
+  uploadFileList.value = [];
 }
 
 // 回车事件
@@ -160,6 +172,17 @@ const handleKeyDown = (event) => {
   }
 }
 
+const formatFileItem = (file) => {
+  const { name } = file;
+  return {
+    name: name.substring(0, name.lastIndexOf('.')),
+    url: "",
+    size: (file.file.size / 1024).toFixed(2) + "KB",
+    suffix: name.substring( name.lastIndexOf('.') + 1 ).toUpperCase(),
+    originSuffix:name.substring( name.lastIndexOf('.') )
+  }
+}
+
 // 处理点击空白处关闭
 const closePopoverOutside =(event) => {
   if (!isOpen.value) return; 
@@ -176,11 +199,88 @@ const selectOption = (index) => {
   inpVal.value = selectedOption.value.content;
 }
 
+const beforeUpload = ({ file }) => {
+  if (( file.file?.size / ( 1024 * 2 ) ) > 5) {
+    message.warning("只能上传.doc和.txt格式的文件, 请重新上传");
+    return false;
+  }
+
+  const index = unref(lastFileListIndex);
+  
+  uploadFileList.value[index] = {
+    ...formatFileItem(file),
+    percentage: 0
+  }
+
+  uploadLoading.value = true
+
+  console.log( "开始上传", uploadFileList.value );
+
+  return true;
+}
+
+// 上传文件
+const handleUploadChange = ({ file }) => {
+
+  if (file.status === 'uploading') {
+    uploadFileList.value[lastFileListIndex.value].percentage = file.percentage;
+    console.log( "上传中", uploadFileList.value );
+  }
+
+}
+
+// 文件上传完成
+const handleFinish = ({ file, event }) => {
+
+  uploadLoading.value = false
+
+  try {
+
+    const res = JSON.parse( (event?.target).response );
+
+    if ( res.code == 200 ) {
+      
+      uploadFileList.value[lastFileListIndex.value] = {
+        ...uploadFileList.value[lastFileListIndex.value],
+        url: res.data,
+      }
+
+      console.log( "上传完成", uploadFileList.value );
+
+      // uploadFileList.value.push({
+      //   name: name.substring(0, name.lastIndexOf('.')),
+      //   url: res.data,
+      //   size: (file.file.size / 1024).toFixed(2) + "KB",
+      //   suffix: name.substring( name.lastIndexOf('.') + 1 ).toUpperCase()
+      // })
+    }
+
+  } catch (error) {
+    console.log("上传完成, 但是存在错误", error);
+  }
+
+}
+
+// 上传失败
+const handleUploadError = (error) => {
+  uploadLoading.value = false;
+}
+
+// 删除文件
+const onRemoveFile = (i) => {
+  uploadFileList.value.splice(i, 1);
+}
+
 onMounted(async () => {
+  const url = import.meta.env.VITE_BASE_URL;
+const prefix = import.meta.env.VITE_BASE_PREFIX;
+const baseURL = url + prefix;
+
+console.log( "baseURL", baseURL );
   const { data } = await helperApi.getHelperList();
   
   const result = getFormatYesterDay(data)
-  console.log("result", result);
+
   helperList.value = result;
   document.addEventListener('keydown', handleKeyDown);
   document.addEventListener('click', closePopoverOutside);
@@ -223,8 +323,54 @@ defineExpose({
               <SvgIcon name="chat-icon-close-btn"></SvgIcon>
             </li>
           </ul>
+
+          <ul class="file-list-wrapper" v-show="uploadFileList.length">
+            <li class="file-item space-x-[14px]" v-for="(item, index) in uploadFileList" :key="index">
+              <div class="file-icon"></div>
+              <div class="file-info">
+                <p class="title">{{ item.name }}</p>
+                <p class="info space-x-[8px]">
+                  <span class="suffix">{{ item.suffix }}</span>
+                  <span class="size">{{ item.size }}</span>
+                </p>
+              </div>
+              <span class="close" @click="onRemoveFile(i)" v-show="item.percentage == 100">x</span>
+              <div class="file-progress" v-if="item.percentage != 100">
+                <NProgress
+                  type="line"
+                  color="#3153f5"
+                  :percentage="item.percentage"
+                  :show-indicator="false"
+                  :height="3"
+                ></NProgress>
+              </div>
+            </li>
+          </ul>
+
           <div class="chat-inp-inner">
             <div class="inp-wrapper flex-1" @click="handleInpFocus">
+              <div class="upload-inner">
+                <NUpload
+                  accept=".doc,.txt"
+                  :disabled="uploadLoading"
+                  :show-file-list="false"
+                  :action="baseURL + '/qiniuyun/upLoadImage'"
+                  :headers="{
+                    'Authorization': 'Bearer' + useStore.token,
+                  }"
+                  @on-error="handleUploadError"
+                  @change="handleUploadChange"
+                  @finish="handleFinish"
+                  @before-upload="beforeUpload"
+                >
+                  <NTooltip trigger="hover">
+                    <template #trigger>
+                      <div class="upload-file-button"></div>
+                    </template>
+                    <span class="text-[12px]">支持上传文件(每次一个, 大小5MB以内)接受.doc、.txt等格式的文件</span>
+                  </NTooltip>
+                </NUpload>
+              </div>
               <NInput 
                 class="flex-1"
                 ref="inpRef" 
@@ -312,7 +458,25 @@ defineExpose({
     background: #fff;
 
     .inp-wrapper {
-      padding: 17px 0px 17px 34px;
+      @include flex(x, start, center);
+      padding: 17px 0px 17px 17px;
+
+      .upload-inner {
+        width: 30px;
+        height: 30px;
+        padding-top: 2px;
+
+        .upload-file-button {
+          width: 30px;
+          height: 30px;
+          background: url("@/assets/svgs/chat/icon-file-default.svg") center center no-repeat;
+          cursor: pointer;
+
+          &:hover {
+            background: url("@/assets/svgs/chat/icon-file-active.svg") center center no-repeat;
+          }
+        }
+      }
     }
 
     .submit-btn {
@@ -328,6 +492,77 @@ defineExpose({
       }
     }
   }
+  
+  .file-list-wrapper {
+    padding: 10px;
+    padding-bottom: 0px;
+    background: #fff;
+
+    .file-item {
+      position: relative;
+      @include flex(x, center, start);
+      width: 30%;
+      // height: 52px;
+      padding: 10px;
+      border-radius: 4px;
+      background: #f5f5f5;
+
+      .file-icon {
+        flex-shrink: 0;
+        width: 36px;
+        height: 36px;
+        background: url("@/assets/svgs/chat/icon-file.svg") center center no-repeat;
+      }
+
+      .file-info {
+        flex: 1;
+        @include flex(y, start, start);
+        font-size: 12px;
+        .title {
+          width: 170px;
+          color: #1a2029;
+          text-overflow: ellipsis;
+          overflow: hidden;
+          word-break: break-all;
+          white-space: nowrap;
+        }
+        .info {
+          flex: 1;
+          @include flex(x, center, between);
+          color: #838a95;
+        }
+      }
+
+      .file-progress {
+        position: absolute;
+        left: 0;
+        bottom: 0;
+        width: 100%;
+        margin: 0;
+      }
+
+      .close {
+        position: absolute;
+        top: 0;
+        right: 0;
+        width: 16px;
+        height: 16px;
+        border: 1px solid #fff;
+        border-radius: 100%;
+        transform: translate(40%, -40%);
+        background: #c8c8c8;
+        font-size: 10px;
+        text-align: center;
+        line-height: 12px;
+        cursor: pointer;
+        color: #fff;
+
+        &:hover {
+          background: #b7b7b7;
+        }
+      }
+    }
+  }
 }
 
 .popover-inner {

+ 71 - 6
src/components/Chat/ChatAsk.vue

@@ -9,17 +9,82 @@ defineProps({
   sessionId: {
     type: String,
     default: ''
+  },
+  uploadFileList: {
+    type: Array,
+    default: []
   }
 })
 
 </script>
 
 <template>
-  <div class="ask-inner flex items-start justify-start pl-[20px] mb-[20px]">
-    <div class="chat-ask_icon">
-      <SvgIcon name="chat-avatar" size="20" />
+  <div class="ask-wrapper pl-[20px] mb-[20px]">
+    <div class="ask-inner flex items-start justify-start">
+      <div class="chat-ask_icon">
+        <SvgIcon name="chat-avatar" size="20" />
+      </div>
+      <p class="flex-1 pt-[6px] ml-[16px] text-[15px] font-bold leading-[24px]" v-html="content"></p>
+      {{ sessionId }}
     </div>
-    <p class="flex-1 pt-[6px] ml-[16px] text-[15px] font-bold leading-[24px]" v-html="content"></p>
-    {{ sessionId }}
+
+    <ul class="file-list-wrapper pl-[48px]" v-show="uploadFileList.length">
+      <li class="file-item space-x-[14px]" v-for="(item, index) in uploadFileList" :key="index">
+        <div class="file-icon"></div>
+        <div class="file-info">
+          <p class="title">{{ item.name }}</p>
+          <p class="info space-x-[8px]">
+            <span class="suffix">{{ item.suffix }}</span>
+            <span class="size">{{ item.size }}</span>
+          </p>
+        </div>
+      </li>
+    </ul>
+
   </div>
-</template>
+</template>
+
+<style lang="scss" scoped>
+.file-list-wrapper {
+  padding: 10px 10px 0 48px;
+  padding-bottom: 0px;
+
+  .file-item {
+    position: relative;
+    @include flex(x, center, start);
+    width: 30%;
+    // height: 52px;
+    padding: 10px;
+    border-radius: 4px;
+    background: #fff;
+
+    .file-icon {
+      flex-shrink: 0;
+      width: 36px;
+      height: 36px;
+      background: url("@/assets/svgs/chat/icon-file.svg") center center no-repeat;
+    }
+
+    .file-info {
+      flex: 1;
+      @include flex(y, start, start);
+      font-size: 12px;
+
+      .title {
+        width: 170px;
+        color: #1a2029;
+        text-overflow: ellipsis;
+        overflow: hidden;
+        word-break: break-all;
+        white-space: nowrap;
+      }
+
+      .info {
+        flex: 1;
+        @include flex(x, center, between);
+        color: #838a95;
+      }
+    }
+  }
+}
+</style>

+ 3 - 4
src/utils/request.ts

@@ -12,10 +12,9 @@ const { notification } = createDiscreteApi(["notification"]);
 
 const useStore = useUserStore();
 
-const url = import.meta.env.VITE_BASE_URL;
-const prefix = import.meta.env.VITE_BASE_PREFIX;
-const baseURL = url + prefix;
-
+export const url = import.meta.env.VITE_BASE_URL;
+export const prefix = import.meta.env.VITE_BASE_PREFIX;
+export const baseURL = url + prefix;
 
 enum errorCode {
   '请求错误'          = 400,

+ 16 - 8
src/views/work/WorkView.vue

@@ -72,16 +72,22 @@ const handleChatDetail = async ({ sessionId }) => {
   scrollToBottom();
 }
 
-const onRegenerate = async ({ question, tools }) => {
+const onRegenerate = async ({ question, tools, uploadFileList }) => {
   controller = new AbortController();
 
   const sessionId = unref(currenSessionId);
+  let fileQuestionStr = '';
+
+  if ( uploadFileList && uploadFileList.length ) {
+    const [ fileItem ] = uploadFileList;
+    fileQuestionStr = `file:${fileItem.name + fileItem.originSuffix}||${fileItem.url}||${question}`
+  }
 
   const params = {
     data: {
       sessionId,
-      showVal: question,
-      question: question,
+      showVal: question,                        // 展示问题
+      question: fileQuestionStr || question,    // 给大模型的问题
       module: 2,
       tools: activeItem.value.tools || tools,
       isStrong: Number(unref(switchActive))
@@ -96,6 +102,7 @@ const onRegenerate = async ({ question, tools }) => {
         sessionId,
         question,
         answer,
+        uploadFileList,
         loading: true,
         delayLoading: false
       })
@@ -114,6 +121,7 @@ const onRegenerate = async ({ question, tools }) => {
       sessionId,
       question,
       answer,
+      uploadFileList,
       loading: false,
       delayLoading: false
     })
@@ -129,7 +137,7 @@ const onRegenerate = async ({ question, tools }) => {
   }
 }
 // 提交问题
-const handleSubmit = async ({question, selectedOption}) => {
+const handleSubmit = async ({question, selectedOption, uploadFileList}) => {
 
   if (unref(isExistInHistory)) {
     const { data: sessionId } = await chatApi.getChatSessionTag();
@@ -143,12 +151,13 @@ const handleSubmit = async ({question, selectedOption}) => {
     question,
     answer: '',
     loading: true,
-    delayLoading: true
+    delayLoading: true,
+    uploadFileList
   })
 
   scrollToBottom();
 
-  setTimeout(() => onRegenerate({ question, tools: selectedOption?.tools || null }), 2 * 1000);
+  setTimeout(() => onRegenerate({ question, tools: selectedOption?.tools || null,  uploadFileList}), 2 * 1000);
 }
 
 // 处理推荐问题
@@ -218,7 +227,6 @@ onUnmounted(() => {
             <div class="grid-item" v-for="item in helperList" :key="item.id" @click="handleWelcomeRecommend(item)">
               <div class="grid-item-icon space-x-[8px]">
                 <img :src="item.banner" alt="" class="w-[24px]">
-                <!-- <SvgIcon name="tool-report" size="24"></SvgIcon> -->
                 <h3 class="grid-item-title">{{ item.title }}</h3>
               </div>
               <div class="text-[#5E5E5E] mt-[8px] text-justify">
@@ -238,7 +246,7 @@ onUnmounted(() => {
 
       <div class="conversation-item" v-if="chatDataSource.length">
         <template v-for="item in chatDataSource" :key="item.id">
-          <ChatAsk :content="item.question" :sessionId="item.sessionId"></ChatAsk>
+          <ChatAsk :content="item.question" :sessionId="item.sessionId" :uploadFileList="item.uploadFileList"></ChatAsk>
           <ChatAnswer :id="item.id" :content="item.answer" :loading="item.loading" :delay-loading="item.delayLoading"
             :isSatisfied="item.isSatisfied" @on-click-icon="params => updateById(params)"></ChatAnswer>
         </template>