Przeglądaj źródła

添加文件上传功能,创建专用的axios实例以处理上传请求,优化上传过程中的错误提示和进度反馈,同时改进搜索框的交互逻辑和样式。

刘博博 2 dni temu
rodzic
commit
6fdd1e86cc

+ 68 - 2
src/apis/api.ts

@@ -10,7 +10,13 @@ const axiosInstance = axios.create({
     timeout: 600000 * 2,// 请求超时20分钟
 });
 
-// 请求拦截器
+// 创建专门用于文件上传的axios实例,超时时间设置为20分钟
+export const uploadAxiosInstance = axios.create({
+    baseURL: config.baseURL,
+    timeout: 20 * 60 * 1000, // 20分钟 
+});
+
+// 默认实例的请求拦截器
 axiosInstance.interceptors.request.use(
     (config: any) => {
         config.headers = getHeaders();
@@ -21,7 +27,24 @@ axiosInstance.interceptors.request.use(
     }
 );
 
-// 响应拦截器
+// 文件上传实例的请求拦截器
+uploadAxiosInstance.interceptors.request.use(
+    (config: any) => {
+        config.headers = {
+            ...config.headers,
+            ...getHeaders(),
+        };
+        if (!navigator.onLine) {
+            message.error('网络故障');
+        }
+        return config;
+    },
+    (error) => {
+        return Promise.reject(error);
+    }
+);
+
+// 默认实例的响应拦截器
 axiosInstance.interceptors.response.use(
     (response: AxiosResponse) => {// 成功信息
         const { config, data } = response;
@@ -64,4 +87,47 @@ axiosInstance.interceptors.response.use(
     }
 );
 
+// 文件上传实例的响应拦截器
+uploadAxiosInstance.interceptors.response.use(
+    (response: AxiosResponse) => {// 成功信息
+        const { config, data } = response;
+        if (config.responseType === 'blob') {
+            return Promise.resolve(data);
+        } else {
+            if (data.code === 200) {// 成功
+                return Promise.resolve(data);
+            } else {// 失败
+                if (data.code === 401) {
+                    LocalStorage.clear();
+                    router.navigate({ pathname: '/login' }, { replace: true });
+                    message.error('登录过期');
+                    return Promise.reject();
+                } else {
+                    return Promise.reject(data);
+                }
+            }
+        }
+    },
+    (error) => {// 错误信息
+        // HTTP状态码
+        const statusCode = error.response?.status;
+        if (String(error).includes('timeout')) {
+            message.error('请求超时');
+        } else if (statusCode === 504) {
+            // 504网关超时,通常是上传文件超时
+            message.error('请求超时,请稍后重试');
+        } else if (statusCode === 403) {
+            // 403禁止访问,通常是防火墙拦截或内容违规
+            message.error('检测到您的此次提交请求未能通过安全验证,请检查提示词中是否包含特殊符号(如 ` \ $ ), 请调整后重新提交。');
+        } else {
+            if (statusCode < 500) {
+                message.error('请求失败');
+            } else {
+                message.error('服务异常');
+            }
+        }
+        return Promise.reject();
+    }
+);
+
 export default axiosInstance;

+ 35 - 4
src/pages/deepseek/knowledgeLib/detail/drawerIndex.tsx

@@ -21,12 +21,13 @@ import InfoModalSetting from './components/InfoModalSetting';
 import router from '@/router';
 import { Record } from './types';
 import dayjs from 'dayjs';
-import axios from 'axios';
 import LocalStorage from '@/LocalStorage';
 import store from './store';
 import './style.less';
+import { uploadAxiosInstance } from '@/apis/api';
 
 const { Dragger } = Upload;
+
 interface Props{
     drawerItem: any;
 }
@@ -80,8 +81,38 @@ const KnowledgeLibInfo : React.FC<Props> = ({drawerItem}:Props) => {
   const props : UploadProps = {
     name: 'files',
     multiple: true,
-    action: '/api/deepseek/api/uploadDocument/' + params.knowledgeId,
-    headers: getHeaders(),
+    customRequest: async (options) => {
+      const { file, onSuccess, onError, onProgress } = options;
+      const formData = new FormData();
+      formData.append('files', file as File);
+
+      try {
+        const response = await uploadAxiosInstance.post(
+          `/deepseek/api/uploadDocument/${params.knowledgeId}`,
+          formData,
+          {
+            headers: {
+              'Content-Type': 'multipart/form-data',
+            },
+            onUploadProgress: (progressEvent) => {
+              if (progressEvent.total) {
+                const percent = Math.round((progressEvent.loaded * 100) / progressEvent.total);
+                onProgress?.({ percent });
+              }
+            },
+          }
+        );
+
+        // 处理响应
+        if (response.data && response.data.code === 200) {
+          onSuccess?.(response.data, file as any);
+        } else {
+          onError?.(new Error(response.data?.msg || '上传失败'));
+        }
+      } catch (error: any) {
+        onError?.(error);
+      }
+    },
     beforeUpload( file, fileList ) {
       setUploadLoading( true );
       // const allowedExtensions = ['md', 'txt', 'pdf', 'jpg', 'png', 'jpeg', 'docx', 'xlsx', 'pptx', 'eml', 'csv', 'tar', 'gz', 'bz2', 'zip', 'rar', 'jar'];
@@ -201,7 +232,7 @@ const KnowledgeLibInfo : React.FC<Props> = ({drawerItem}:Props) => {
     } );
 
     try {
-      const res = await axios.post( '/api/deepseek/api/uploadDocument/' + params.knowledgeId, formData, {
+      const res = await uploadAxiosInstance.post( '/deepseek/api/uploadDocument/' + params.knowledgeId, formData, {
         headers: { 'Content-Type': 'multipart/form-data' }
       } );
 

+ 34 - 4
src/pages/deepseek/knowledgeLib/detail/index.tsx

@@ -24,10 +24,10 @@ import InfoModalSetting from './components/InfoModalSetting';
 import router from '@/router';
 import { Record } from './types';
 import dayjs from 'dayjs';
-import axios from 'axios';
 import LocalStorage from '@/LocalStorage';
 import store from './store';
 import './style.less';
+import { uploadAxiosInstance } from '@/apis/api';
 
 const { Dragger } = Upload;
 
@@ -80,8 +80,38 @@ const KnowledgeLibInfo : React.FC = () => {
   const props : UploadProps = {
     name: 'files',
     multiple: true,
-    action: '/api/deepseek/api/uploadDocument/' + params.knowledgeId,
-    headers: getHeaders(),
+    customRequest: async (options) => {
+      const { file, onSuccess, onError, onProgress } = options;
+      const formData = new FormData();
+      formData.append('files', file as File);
+
+      try {
+        const response = await uploadAxiosInstance.post(
+          `/deepseek/api/uploadDocument/${params.knowledgeId}`,
+          formData,
+          {
+            headers: {
+              'Content-Type': 'multipart/form-data',
+            },
+            onUploadProgress: (progressEvent) => {
+              if (progressEvent.total) {
+                const percent = Math.round((progressEvent.loaded * 100) / progressEvent.total);
+                onProgress?.({ percent });
+              }
+            },
+          }
+        );
+
+        // 处理响应
+        if (response.data && response.data.code === 200) {
+          onSuccess?.(response.data, file as any);
+        } else {
+          onError?.(new Error(response.data?.msg || '上传失败'));
+        }
+      } catch (error: any) {
+        onError?.(error);
+      }
+    },
     beforeUpload( file, fileList ) {
       setUploadLoading( true );
       // const allowedExtensions = ['md', 'txt', 'pdf', 'jpg', 'png', 'jpeg', 'docx', 'xlsx', 'pptx', 'eml', 'csv', 'tar', 'gz', 'bz2', 'zip', 'rar', 'jar'];
@@ -200,7 +230,7 @@ const KnowledgeLibInfo : React.FC = () => {
     } );
 
     try {
-      const res = await axios.post( '/api/deepseek/api/uploadDocument/' + params.knowledgeId, formData, {
+      const res = await uploadAxiosInstance.post( '/deepseek/api/uploadDocument/' + params.knowledgeId, formData, {
         headers: { 'Content-Type': 'multipart/form-data' }
       } );
 

+ 40 - 21
src/pages/deepseek/questionAnswer/list/index.tsx

@@ -142,6 +142,7 @@ const QuestionAnswerList: React.FC = () => {
   // 搜索输入框展开状态
   const [isSearchExpanded, setIsSearchExpanded] = React.useState(false);
   const searchWrapperRef = React.useRef<HTMLDivElement>(null);
+  const searchInputRef = React.useRef<any>(null);
   // 标记是否正在重置,避免触发 useEffect 循环
   const isResettingRef = React.useRef(false);
 
@@ -513,6 +514,35 @@ const QuestionAnswerList: React.FC = () => {
     };
   }, []);
 
+  /** 点击外部关闭搜索框 */
+  React.useEffect(() => {
+    const handleClickOutside = (event: MouseEvent) => {
+      if (searchWrapperRef.current && !searchWrapperRef.current.contains(event.target as Node)) {
+        setIsSearchExpanded(false);
+      }
+    };
+
+    if (isSearchExpanded) {
+      document.addEventListener('mousedown', handleClickOutside, true);
+      return () => {
+        document.removeEventListener('mousedown', handleClickOutside, true);
+      };
+    }
+  }, [isSearchExpanded]);
+
+  // 展开搜索框并聚焦输入框
+  const handleExpandSearch = () => {
+    setIsSearchExpanded(true);
+    // 延迟聚焦,确保输入框已渲染
+    setTimeout(() => {
+      const input = searchInputRef.current?.input || searchInputRef.current;
+      if (input) {
+        input.focus();
+        input.select(); // 选中输入框中的文本
+      }
+    }, 150);
+  };
+
 
   const handleAppTypeChange = (value: string) => {
     if (value === '41') {
@@ -680,43 +710,32 @@ const QuestionAnswerList: React.FC = () => {
           <FormItem>
             <Space size={12}>
               {/* <div 
-                className="search-expand-wrapper"
+                className={`search-expand-wrapper ${isSearchExpanded ? 'expanded' : ''}`}
                 ref={searchWrapperRef}
-                onMouseEnter={() => setIsSearchExpanded(true)}
-                onMouseLeave={(e) => {
-                  // 如果输入框有焦点,不收起
-                  const input = searchWrapperRef.current?.querySelector('input');
-                  if (input !== document.activeElement) {
-                    setIsSearchExpanded(false);
-                  }
-                }}
               >
                 <div className={`search-input-container ${isSearchExpanded ? 'expanded' : ''}`}>
                   <FormItem name='keyword' style={{ marginBottom: 0 }}>
                     <Input
+                      ref={searchInputRef}
                       className="search-input"
                       placeholder="请输入关键字"
                       allowClear
-                      prefix={<SearchOutlined />}
                       onPressEnter={handleClickSearch}
                       onFocus={() => setIsSearchExpanded(true)}
-                      onBlur={(e) => {
-                        // 延迟检查,避免点击按钮时立即收起
-                        setTimeout(() => {
-                          if (!searchWrapperRef.current?.matches(':hover')) {
-                            setIsSearchExpanded(false);
-                          }
-                        }, 200);
-                      }}
                     />
                   </FormItem>
                 </div>
                 <Button
                   type='primary'
-                  shape="circle"
-                  onClick={handleClickSearch}
+                  className="search-button-blue"
+                  onClick={() => {
+                    if (isSearchExpanded) {
+                      handleClickSearch();
+                    } else {
+                      handleExpandSearch();
+                    }
+                  }}
                   icon={<SearchOutlined />}
-                  className="search-button"
                 />
               </div> */}
               <Tooltip title="重置">

+ 62 - 50
src/pages/deepseek/questionAnswer/list/style.less

@@ -218,7 +218,20 @@
   display: flex;
   align-items: center;
   height: 32px;
+  border-radius: 16px;
+  overflow: hidden;
+  border: 1px solid #d9d9d9;
+  background: #fff;
   transition: all 0.3s ease;
+  
+  &:hover {
+    border-color: #40a9ff;
+  }
+  
+  // 当输入框展开时,边框颜色改变
+  &.expanded {
+    border-color: #40a9ff;
+  }
 }
 
 .search-input-container {
@@ -227,42 +240,49 @@
   transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
   opacity: 0;
   width: 0;
-  margin-right: 0;
-  transform: translateX(-10px);
+  flex: 1;
   white-space: nowrap;
   
   &.expanded {
     opacity: 1;
     width: 200px;
-    margin-right: 8px;
     transform: translateX(0);
   }
 
   .search-input {
     transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
-    border-radius: 16px;
     height: 32px;
     width: 100%;
     display: flex;
     align-items: center;
+    border: none;
     
     .ant-input {
-      border-radius: 16px;
+      border: none;
+      box-shadow: none;
       transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
-      height: 32px;
-      line-height: 32px;
-      padding: 4px 11px;
-      display: flex;
-      align-items: center;
+      height: 30px;
+      line-height: 30px;
+      padding: 4px 12px;
+      border-radius: 16px 0 0 16px;
+      
+      &:focus,
+      &:hover {
+        border: none;
+        box-shadow: none;
+      }
     }
     
     .ant-input-affix-wrapper {
-      border-radius: 16px;
+      border: none;
+      box-shadow: none;
       transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
-      height: 32px;
+      height: 30px;
       display: flex;
       align-items: center;
-      padding: 4px 11px;
+      padding: 4px 12px;
+      border-radius: 16px 0 0 16px;
+      background: transparent;
       
       .ant-input {
         height: auto;
@@ -272,58 +292,50 @@
         box-shadow: none;
       }
       
-      &:hover {
-        border-color: #40a9ff;
-      }
-      
+      &:hover,
       &:focus,
       &.ant-input-affix-wrapper-focused {
-        border-color: #1890ff;
-        box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
+        border: none;
+        box-shadow: none;
       }
     }
-    
-    .ant-input-prefix {
-      transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
-      color: #999;
-      display: flex;
-      align-items: center;
-      margin-right: 8px;
-    }
   }
 }
 
-.search-button {
+.search-button-blue {
   flex-shrink: 0;
+  height: 32px;
+  min-width: 32px;
+  padding: 0 12px;
+  border: none;
+  border-radius: 16px;
+  background: #1890ff !important;
+  display: flex;
+  align-items: center;
+  justify-content: center;
   transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
   z-index: 1;
+  cursor: pointer;
   
-  &:hover {
-    transform: scale(1.05);
-    box-shadow: 0 4px 12px rgba(24, 144, 255, 0.3);
+  .anticon {
+    color: #fff;
+    font-size: 14px;
   }
-}
-
-// 展开动画优化 - 使用hover触发
-.search-expand-wrapper:hover {
-  .search-input-container {
-    opacity: 1;
-    width: 200px;
-    margin-right: 8px;
-    transform: translateX(0);
+  
+  &:hover {
+    background: #40a9ff !important;
+    transform: none;
   }
   
-  .search-button {
-    transform: scale(1.05);
-    box-shadow: 0 4px 12px rgba(24, 144, 255, 0.3);
+  &:active {
+    background: #096dd9 !important;
   }
 }
 
-// 当输入框获得焦点时保持展开状态
-.search-input-container.expanded,
-.search-expand-wrapper:focus-within .search-input-container {
-  opacity: 1 !important;
-  width: 200px !important;
-  margin-right: 8px !important;
-  transform: translateX(0) !important;
+// 当输入框展开时,按钮显示为右侧圆角
+.search-expand-wrapper .search-input-container.expanded ~ .search-button-blue {
+  border-radius: 0 16px 16px 0;
+  width: auto;
+  min-width: 32px;
+  padding: 0 12px;
 }

+ 8 - 0
src/style/global.less

@@ -162,3 +162,11 @@ ul li {
         transform: none;
     }
 }
+
+// Message 提示居中显示
+.ant-message {
+    top: 50% !important;
+    left: 50% !important;
+    transform: translate(-50%, -50%) !important;
+    padding-top: 0 !important;
+}