Quellcode durchsuchen

feat: 知识库详情

王晓东 vor 1 Monat
Ursprung
Commit
5bb6e267fd

+ 6 - 3
src/components/UploaderGrid/index.tsx

@@ -5,18 +5,21 @@ import IconDeleteGray16 from "@/components/icon/IconDeleteGray16";
 import IconPlusGray16 from "@/components/icon/IconPlusGray16";
 import type { TMediaType } from "@/types/index";
 
+
 export interface IndexProps {
   list: TMediaType[];
+  onNewUpload: ()=>  void;
   onChange?: (list: TMediaType[]) => void;
   onDelete?: (index:number, item: TMediaType) => void;
 }
 
-const Index = ({ list, onChange, onDelete }: IndexProps) => {
-
+const Index = ({onNewUpload, list, onChange, onDelete }: IndexProps) => {
+  
+  
   return (
     <View className="grid grid-cols-3 gap-x-8 gap-y-12 justify-center">
       <View className="flex">
-        <View className={style.addImageBtn}>
+        <View className={style.addImageBtn} onClick={onNewUpload}>
           <IconPlusGray16 />
         </View>
       </View>

+ 2 - 1
src/components/chat-message/MessageRobotRich.tsx

@@ -7,6 +7,7 @@ interface Props {
   data?: {
     avatar: string;
     name: string;
+    fileLen?: number;
   };
   children?: JSX.Element | JSX.Element[];
 }
@@ -26,7 +27,7 @@ export default ({ data, loading, analyzeStatus='idle', children }: Props) => {
               {loading && <ThinkAnimation></ThinkAnimation>}
               {analyzeStatus !== 'idle' && (
                 <View className="flex items-center gap-6">
-                  <View className="text-14 leading-28">{analyzeStatus === 'doing' ? '正在为您解析' : '已成功解析 2 份知识'} </View>
+                  <View className="text-14 leading-28">{analyzeStatus === 'doing' ? '正在为您解析' : `已成功解析 ${data?.fileLen} 份知识`} </View>
                   {(analyzeStatus === 'doing') && <ThinkAnimation></ThinkAnimation>}
                 </View>
               )}

+ 1 - 1
src/pages/index/components/WelcomeTips/index.module.less

@@ -1,7 +1,7 @@
 .container{
   width: 100%;
   padding: 12px;
-  position: absolute;
+  position: fixed;
   bottom: 16px;
   left: 0;
   right: 0;

+ 5 - 2
src/pages/index/index.tsx

@@ -1,20 +1,23 @@
 
 import PageCustom from "@/components/page-custom/index";
-import { View } from "@tarojs/components";
+import { View,Image } from "@tarojs/components";
 import { useEffect, useState } from "react";
 import DefaultAgent from '@/components/AgentPage'
 import InitView from './components/InitView'
 import { TAgent } from "@/types/agent";
+import NavBarNormal from "@/components/nav-bar-normal/index";
+import LogoImage from '@/images/logo.png'
 
 export default function Index() {
 
   const [defaultAgent, setDefaultAgent] = useState<TAgent|null>(null)
 
-  
+
   
   return (
     <PageCustom>
       <>
+      
       {!defaultAgent && <InitView setDefault={setDefaultAgent}></InitView>}
       {defaultAgent && <DefaultAgent agentId={defaultAgent.agentId}></DefaultAgent>}
       </>

+ 74 - 13
src/pages/knowledge-item-editor/index.tsx

@@ -14,26 +14,36 @@ import IconA from "@/components/icon/IconA";
 import IconPlusColor14 from "@/components/icon/IconPlusColor14";
 import IconALink from "@/components/icon/IconALink";
 import IconDeleteGray16 from "@/components/icon/IconDeleteGray16";
-import { useState } from "react";
+import { useEffect, useRef, useState } from "react";
 import IconAImage from "@/components/icon/IconAImage";
 import WemetaTextarea from "@/components/wemeta-textarea";
 import WemetaInput from "@/components/wemeta-input";
 import UploaderGrid from '@/components/UploaderGrid'
 import type { TMediaType } from "@/types/index";
 
+import { uploadFile } from "@/utils/http";
+import { EUploadFileScene } from "@/consts/enum";
+
+import Taro, { useRouter } from "@tarojs/taro";
+import { getKnowledgeQa, updateKnowledgeQa } from "@/service/knowledge";
+import { isSuccess } from "@/utils";
+
 type TFormdata = {q:string, a: string, links: string[], mediaList: TMediaType[]}
 
 interface IProps {
   initialData?: TFormdata
 }
-export default function Index({ initialData }: IProps) {
-  
+export default function Index() {
+
+  const router = useRouter()
+  const { knowledgeId, qaId } = router.params
+  const loadingRef = useRef(false)
   const [formData, setFormData] = useState<TFormdata>(
-    initialData || {
-      q: "手术费用大概多少?能刷医保吗?",
-      a: "全飞秒手术费用通常在1.5万-2万元左右,半飞秒约1.2万起,ICL晶体术因晶体品牌不同在2.5万-4万元不等。屈光手术属于择业性消费,一般不纳入医保范围,但部分城市可用商业保险或税优健康险报销。",
-      links: ["baidu.com"],
-      mediaList: [{fileType: 'image',url: 'https://cdn.wehome.cn/cmn/png/53/META-H8UKWHWU-2JNUAG2BARJF55VHU9QS3-YBQGHDAM-IW.png'}]
+    {
+      q: '',
+      a: '',
+      links: [],
+      mediaList: [] //{fileType: 'image',url: 'https://cdn.wehome.cn/cmn/png/53/META-H8UKWHWU-2JNUAG2BARJF55VHU9QS3-YBQGHDAM-IW.png'}
     }
   );
 
@@ -86,15 +96,66 @@ export default function Index({ initialData }: IProps) {
     }));
   };
 
-  const handleSubmit = ()=> {
-    // 过滤掉空的链接
+  const handleSubmit = async () => {
+    if(!knowledgeId || !qaId){
+      return
+    }
+
     const dataToSubmit = {
-      ...formData,
-      links: formData.links.filter(link => link.trim() !== '')
+      answer: formData.a,
+      questions: [formData.q],
+      qaId: parseInt(qaId),
+      links: formData.links.filter(link => link.trim() !== ''),
+      pics: formData.mediaList.map( item => item.url)
     }
     console.log(dataToSubmit)
+    await updateKnowledgeQa(knowledgeId, qaId, dataToSubmit)
+  }
+  const getQaDetail = async (knowledgeId: string, qaId: string)=> {
+    const {status, data} = await  getKnowledgeQa(knowledgeId, qaId)
+    if(isSuccess(status)){
+      setFormData({
+        q: data.questions[0],
+        a: data.answer,
+        links: data.links,
+        mediaList: data.pics.map( pic => {
+          return {fileType: 'image', url: pic}
+        }),
+      })
+    }
   }
 
+
+  const handleChooseMedia = ()=> {
+    Taro.chooseMedia({
+      count: 10,
+      mediaType: ["image"],
+      sourceType: ["album", "camera"],
+      async success(r) {
+        const tempFiles = r.tempFiles
+        // const tmpPath = tempFile.tempFilePath;
+        for(let i =0; i< tempFiles.length;i++){
+          const result = await uploadFile(tempFiles[0].tempFilePath, EUploadFileScene.OTHER)
+          if(result?.publicUrl){
+            setFormData({
+              ...formData,
+              mediaList: [{fileType:'image', url: result.publicUrl}, ...formData.mediaList]
+            });
+          }
+        }
+        
+        
+      },
+    });
+  }
+
+  useEffect(()=> {
+    if(knowledgeId && qaId){
+        getQaDetail(knowledgeId, qaId)
+    }
+    
+  }, [knowledgeId, qaId])
+
   return (
     <PageCustom>
       <NavBarNormal scrollFadeIn backText="飞秒小知识"></NavBarNormal>
@@ -172,7 +233,7 @@ export default function Index({ initialData }: IProps) {
                 </View>
                 <View className="flex-1 text-14 leading-28">图片</View>
               </View>
-              <UploaderGrid  list = {formData.mediaList} onChange={()=> {}} onDelete={handleDeleteMedia} />
+              <UploaderGrid onNewUpload={handleChooseMedia}  list = {formData.mediaList} onChange={()=> {}} onDelete={handleDeleteMedia} />
             </View>
             
           </View>

+ 7 - 0
src/pages/knowledge-item/index.module.less

@@ -0,0 +1,7 @@
+page{
+  overflow: hidden;
+}
+.scrollContainer{
+  height: calc(100vh - 400px);
+  overflow: auto;
+}

+ 113 - 20
src/pages/knowledge-item/index.tsx

@@ -2,35 +2,92 @@
  * 知识库
  */
 
-import { Text, View } from "@tarojs/components";
+import { Text, View,Image, ScrollView } from "@tarojs/components";
 
 import PageCustom from "@/components/page-custom/index";
 import NavBarNormal from "@/components/nav-bar-normal/index";
+import BottomBar from "@/components/BottomBar";
 import FigureListItem from "@/components/list/figure-list-item";
 import IconFIleTxt from "@/components/icon/IconFIleTxt";
 import IconEye from "@/components/icon/IconEye";
 import IconA from "@/components/icon/IconA";
 import IconQ from "@/components/icon/IconQ";
 import WemetaSwitch from "@/components/wemeta-switch";
-import { useState } from "react";
+import { useEffect, useState } from "react";
 import CardEditable from "@/components/card/card-editable/index";
-import Taro from "@tarojs/taro";
+import Taro, { useRouter } from "@tarojs/taro";
+import { getMyKnowledgeDetail, deleteKnowledgeQa, updateExactAnswer } from "@/service/knowledge";
+import { TKnowledgeDetail } from "@/types/knowledge";
+import { isSuccess } from "@/utils";
+import { useModalStore } from "@/store/modalStore";
+import style from './index.module.less'
 
 export default function Index() {
+  const router = useRouter()
+  const { knowledgeId } = router.params
   const [checked, setChecked] = useState(false);
-  const handleEdit = ()=> {
+  const [detail, setDetail] = useState<TKnowledgeDetail|null>(null)
+  
+  const { showModal } = useModalStore()
+  
+  const handleEdit = (qaId: number)=> {
     Taro.navigateTo({
-      url: '/pages/knowledge-item-editor/index'
+      url: `/pages/knowledge-item-editor/index?knowledgeId=${knowledgeId}&qaId=${qaId}`
     })
   }
+  // 删除问答项
+  const handleDeleteItem = async (qaId: number)=> {
+    if(!detail){
+      return
+    }
+    showModal({
+      content: '确定删除该问答项吗?',
+      onConfirm: async () => {
+        const { status }  = await deleteKnowledgeQa(detail.knowledgeId, qaId)
+        if(isSuccess(status)){
+          Taro.showToast({title: '删除成功', icon: 'success'})
+          getDetail(detail.knowledgeId)
+        }
+      }
+    })
+    
+  }
+
+  // 获取详情
+  const getDetail = async (knowledgeId: number)=> {
+    const response = await getMyKnowledgeDetail(knowledgeId)
+    if(isSuccess(response.status) && response.data){
+      setDetail(response.data)
+      setChecked(response.data.enableExactAnswer)
+    }
+  }
+
+  // 开启/关闭 精准问答模式 
+  const handleEnableExactAnswer = async () => {
+    if(!detail?.knowledgeId){
+      return
+    }
+    const {status} = await updateExactAnswer(detail.knowledgeId, !checked)
+    if(isSuccess(status)){
+      setChecked(!checked)
+    }
+  }
+  
+
+  useEffect(()=> {
+    knowledgeId && getDetail(parseInt(knowledgeId))
+  }, [knowledgeId])
+
   return (
     <PageCustom>
       <NavBarNormal backText="知识库"></NavBarNormal>
-      <View className="w-full">
+      <View className="w-full overflow-hidden">
         <View className="p-16">
           <View className="mb-16">
             <FigureListItem
-              figure={IconFIleTxt}
+              figure={()=> {
+                return <>{detail?.icon && <Image src={detail?.icon} mode="widthFix" style={{width: '36px', height: '36px'}}></Image>}</>
+              }}
               rightRenderer={() => (
                 <View>
                   <IconEye />
@@ -38,9 +95,9 @@ export default function Index() {
               )}
             >
               <View className="flex flex-col flex-1 gap-2 w-full">
-                <View className="text-14 leading-22">飞秒小知识</View>
+                <View className="text-14 leading-22 truncate">{detail?.title}</View>
                 <View className="text-12 leading-20 text-gray-45">
-                  03-24 12:20 | 822.KB
+                  {detail?.createTime} | {detail?.fileSizeStr}
                 </View>
               </View>
             </FigureListItem>
@@ -58,18 +115,55 @@ export default function Index() {
             <View className="flex-center">
               <WemetaSwitch
                 checked={checked}
-                onChange={(checked) => setChecked(checked)}
+                onChange={handleEnableExactAnswer}
               ></WemetaSwitch>
             </View>
           </View>
         </View>
-        <View>
+            
+        
+        <View className="pb-100">
           <View className="rounded-container-header">
             <View className="text-14 font-medium leading-22 px-16 pb-16">
-              问答列表共<Text className="text-primary">10</Text>条
+              问答列表共<Text className="text-primary">{detail?.qaList.length}</Text>条
+            </View>
+          </View>
+          <View className={style.scrollContainer}>
+            {detail?.qaList.map((item)=> {
+              return <View className="flex flex-col gap-16 px-16 mb-16">
+              <CardEditable buttons={[<View onClick={()=> handleDeleteItem(item.qaId)}>删除</View>, <View onClick={()=> handleEdit(item.qaId)}>编辑</View>]}>
+                <View className="flex items-start mb-8 gap-8">
+                  <View className="flex-center h-28"><IconQ/></View>
+                  <View className="flex-1 font-medium text-14 leading-28">{item.questions[0]}</View>
+                </View>
+
+                <View className="flex items-start gap-8">
+                  <View className="flex-center  h-28"><IconA/></View>
+                  <View className="flex-1 text-12 leading-20 text-gray-45 truncate">
+                    <View className="truncate">{item.answer}</View>
+
+                    {(!!item.links.length) && <View className="pb-12">
+                      {item.links.map((link)=> {
+                        return <View>查看链接 <Text>{link}</Text></View>
+                      })}
+                      </View>
+                    }
+                    {(!!item.pics.length) && <View className="pb-12">
+                      {item.pics.map((pic)=> {
+                        return <View>
+                          <Image src={pic} mode="widthFix" className="w-full"></Image>
+                        </View>
+                      })}
+                      </View>
+                    }
+                  </View>
+                </View>
+
+              </CardEditable>
             </View>
+            })}
           </View>
-          <View className="flex flex-col gap-16 px-16">
+          {/* <View className="flex flex-col gap-16 px-16">
             <CardEditable buttons={[<View>删除</View>, <View onClick={handleEdit}>编辑</View>]}>
               <View className="flex items-start mb-8 gap-8">
                 <View className="flex-center h-28"><IconQ/></View>
@@ -82,14 +176,13 @@ export default function Index() {
               </View>
 
             </CardEditable>
-          </View>
-        </View>
-        <View className="bottom-bar">
-          <View className="flex gap-8 px-20 py-12">
-            <View className="button-rounded button-plain button-warn w-88">删除</View>
-            <View className="button-rounded button-primary flex-1">共享到企业知识</View>
-          </View>
+          </View> */}
         </View>
+        <BottomBar>
+          <View className="button-rounded button-plain button-warn w-88">删除</View>
+          <View className="button-rounded button-primary flex-1">共享到企业知识</View>
+        </BottomBar>
+        
       </View>
     </PageCustom>
   );

+ 3 - 0
src/pages/knowledge/components/asistant-message/index.tsx

@@ -13,12 +13,15 @@ export const WelcomeCard = () => {
          · 智能解析处理,生成高质量的QA集。`}
       </Text>
       <Image
+        showMenuByLongpress
         src="https://nexthuman.cn/api-web/img/wechat.9d5eaf4d.jpg"
         style={{ width: "160px", height: "160px" }}
       />
     </View>
   );
 };
+
+
 export const AddSuccessfulTips = () => {
   return (
     <View>

+ 44 - 17
src/pages/knowledge/components/personal-tab/index.tsx

@@ -1,4 +1,4 @@
-import { ScrollView, View } from "@tarojs/components";
+import { Text, View, Image } from "@tarojs/components";
 import RoundedLabel from "../rounded-label";
 import IconFilterFeeds from "@/components/icon/IconFilterFeeds";
 import IconFilterBatch from "@/components/icon/IconFilterBatch";
@@ -19,31 +19,42 @@ import ViewStyleChat from "../view-style/ViewStyleChat";
 import ViewStyleList from "../view-style/ViewStyleList";
 import Taro from "@tarojs/taro";
 import { uploadKnowledgeFile } from "@/service/knowledge";
-import style from "../../index.module.less";
+
+import { useKnowledgeStore } from "@/store/knowledge";
 type TListStyle = "chat" | "list";
 
 const Index = () => {
   const [checked, setChecked] = useState(false);
   const [showPopup, setShowPopup] = useState(false);
+  const [showAiAsistant, setShowAiAsistant] = useState(false);
   const [listStyle, setListStyle] = useState<TListStyle>("chat");
+
+  const { loadMore } = useKnowledgeStore()
+
   const handleListStyleChange = (listStyle: TListStyle) => {
     setListStyle(listStyle);
   };
 
-  const handleChooseFile = () => {
-    Taro.chooseMessageFile({
-      count: 1,
-      type: "all",
-      async success(res) {
-        // tempFilePath可以作为img标签的src属性显示图片
-        const tempFilePaths = res.tempFiles;
-        const response = await uploadKnowledgeFile({
-          tmpPath: tempFilePaths[0].path,
-          fileName: "field",
-        });
-        console.log(response);
-      },
-    });
+  const handleShowAsistantTip = () => {
+    setShowAiAsistant(true)
+    // Taro.chooseMessageFile({
+    //   count: 1,
+    //   type: "all",
+    //   async success(res) {
+        
+    //     const tempFilePaths = res.tempFiles;
+    //     Taro.showLoading()
+    //     const result = await uploadKnowledgeFile({
+    //       tmpPath: tempFilePaths[0].path,
+    //       fileName: "field",
+    //     });
+    //     Taro.hideLoading()
+    //     if(result){
+    //       loadMore(true)
+    //     }
+
+    //   },
+    // });
   };
 
   return (
@@ -88,12 +99,28 @@ const Index = () => {
       </View>
 
       <View
-        onClick={handleChooseFile}
+        onClick={handleShowAsistantTip}
         className="fixed right-20 bottom-20 w-48 h-48 rounded-full bg-primary flex-center drop-shadow-[0_4px_16px_rgba(49,124,250,0.25)]"
       >
         <IconPlusBig></IconPlusBig>
       </View>
 
+      <Popup title="添加AI小助理的企业微信" setShow={setShowAiAsistant} show={showAiAsistant}>
+        <View className="p-20">
+          <View>将资料发送到我的企业微信,我会自动为你解析内容,并同步到这里。</View>
+          <Text>
+            {` · 无限制,图片/文档/链接等多种格式;;
+            · 无需小程序,直接在微信内操作;;
+            · 智能解析处理,生成高质量的QA集。`}
+          </Text>
+          <Image
+            showMenuByLongpress
+            src="https://nexthuman.cn/api-web/img/wechat.9d5eaf4d.jpg"
+            style={{ width: "160px", height: "160px" }}
+          />
+        </View>
+      </Popup>
+
       <Popup setShow={setShowPopup} show={showPopup} title="展示样式">
         <View
           className={`rounded-card ${

Datei-Diff unterdrückt, da er zu groß ist
+ 9 - 11
src/pages/knowledge/components/view-style/ViewStyleChat.tsx


+ 2 - 2
src/pages/knowledge/index.tsx

@@ -2,7 +2,7 @@
  * 知识库
  */
 
-import { View } from "@tarojs/components";
+import { View, Text } from "@tarojs/components";
 import { useState } from "react";
 import PageCustom from "@/components/page-custom/index";
 import NavBarNormal from "@/components/nav-bar-normal/index";
@@ -53,7 +53,7 @@ export default function Index() {
   ];
   return (
     <PageCustom>
-      <NavBarNormal backText="知识库"></NavBarNormal>
+      <NavBarNormal leftColumn={() => <Text className="text-16 font-medium">知识库</Text>}></NavBarNormal>
       <View className="w-full">
         <View className={style.container}>
           <WemetaTabs current="1" list={tabList} className="px-16"></WemetaTabs>

+ 15 - 14
src/service/knowledge.ts

@@ -3,7 +3,7 @@ import Taro from "@tarojs/taro";
 import { getHeaders } from "@/xiaolanbenlib/module/axios.js";
 import JsonChunkParser from "@/utils/jsonChunkParser";
 import request from "@/xiaolanbenlib/module/axios.js";
-import type { TKnowledgeItem, TQAListItem, TKnowledgeStreamResponseData } from "@/types/knowledge";
+import type { TKnowledgeItem, TQAListItem, TKnowledgeStreamResponseData, TKnowledgeDetail } from "@/types/knowledge";
 import { getSimpleHeader } from "@/xiaolanbenlib/module/axios.js";
 import { isSuccess } from "@/utils";
 
@@ -79,18 +79,15 @@ export const getMyKnowledgeStream = (data: {startId?: string, pageSize:number} )
 }
 
 
+
 // 获取指定知识库项的详情信息
-export type TKnowledgeDetailResponse = {
-  "content": "string",
-  "knowledgeList": TKnowledgeListItem[],
-  "qaList": TQAListItem[],
-  "role": "string",
-  "streamId": number,
-  "type": "string"
+export const getMyKnowledgeDetail = (knowledgeId: number) => {
+  return request.get<TKnowledgeDetail>(`${bluebookAiAgent}api/v1/my/knowledge/${knowledgeId}`)
 }
 
-export const getMyKnowledgeDetail = (knowledgeId: string) => {
-  return request.get<TKnowledgeDetailResponse>(`${bluebookAiAgent}api/v1/my/knowledge/${knowledgeId}`)
+// 是否启用文档精准问答
+export const updateExactAnswer = (knowledgeId: number, enable: boolean) => {
+  return request.put(`${bluebookAiAgent}api/v1/my/knowledge/${knowledgeId}/exactAnswer?enable=${enable}`)
 }
 
 // 删除知识库 信息流记录;流删除会同步删除相关的记录项
@@ -99,16 +96,20 @@ export const deleteKnowledgeStream = (streamId: string) => {
 }
 
 // 删除知识库 记录项
-export const deleteKnowledge = (knowledgeId: string) => {
+export const deleteKnowledge = (knowledgeId: number) => {
   return request.delete(`${bluebookAiAgent}api/v1/my/knowledge/${knowledgeId}`,)
 }
 // 删除指定知识库的 QA 项
-export const deleteKnowledgeQa = (knowledgeId: string, qaId: string) => {
+export const deleteKnowledgeQa = (knowledgeId: number, qaId: number) => {
   return request.delete(`${bluebookAiAgent}api/v1/my/knowledge/${knowledgeId}/${qaId}`,)
 }
 
+export const getKnowledgeQa = (knowledgeId: number|string, qaId: number|string) => {
+  return request.get<TQAListItem>(`${bluebookAiAgent}api/v1/my/knowledge/${knowledgeId}/${qaId}`,)
+}
+
 // 更新指定的知识库 QA 项
-export const updateKnowledgeQa = (knowledgeId: string, qaId: string, qa: TQAListItem) => {
+export const updateKnowledgeQa = (knowledgeId: number|string, qaId: number|string, qa: TQAListItem&{qaId: string|number}) => {
   return request.put(`${bluebookAiAgent}api/v1/my/knowledge/${knowledgeId}/${qaId}`, qa)
 }
 
@@ -127,7 +128,7 @@ export const getEntKnowledgeList = (data: {
 };
 // 获取指定知识库项的详情信息
 export const getEntKnowledgeDetail = (knowledgeId: string) => {
-  return request.get<TKnowledgeDetailResponse>(`${bluebookAiAgent}api/v1/ent/knowledge/${knowledgeId}`)
+  return request.get(`${bluebookAiAgent}api/v1/ent/knowledge/${knowledgeId}`)
 }
 
 // 企业知识库--信息流

+ 56 - 0
src/store/knowledge.ts

@@ -0,0 +1,56 @@
+import { create } from "zustand";
+
+import { isSuccess } from "@/utils";
+import type { TKnowledgeStreamResponseData } from "@/types/knowledge";
+
+import {
+  getMyKnowledgeStream as _getMyKnowledgeStream,
+} from "@/service/knowledge";
+import Taro from "@tarojs/taro";
+
+export interface KnowledgeStoreState {
+  scrollTop: number;
+  total: number;
+  list: TKnowledgeStreamResponseData[];
+  isLoading: boolean;
+  startId?: string;
+  loadMore: (force?: boolean) => Promise<void>;
+}
+
+export const useKnowledgeStore = create<KnowledgeStoreState>((set, get) => ({
+  scrollTop: 9999,
+  total: 10000,
+  list: [],
+  isLoading: false,
+  startId: undefined,
+  loadMore: async (force) => {
+    const { list, total, isLoading, startId } = get();
+    
+    if (!force && (list.length >= total || isLoading)) {
+      return;
+    }
+
+    set({ isLoading: true });
+    
+    try {
+      const response = await _getMyKnowledgeStream({
+        startId,
+        pageSize: 10,
+      });
+
+      const result = isSuccess(response.status);
+      if (result) {
+        // 请求到的数组倒序后排在数组前面
+        const newData = [...response.data.data.reverse(), ...list];
+        set({
+          list: newData,
+          startId: response.data.nextId,
+          total: response.data.totalCount ?? 0,
+        });
+      }
+    } finally {
+      set({ isLoading: false, scrollTop: get().scrollTop + 1 });
+    }
+  },
+
+}));

+ 12 - 4
src/types/knowledge.ts

@@ -1,10 +1,11 @@
 
 
 export type TQAListItem = {
-  "answer": string,
-  "links": string[],
-  "pics": string[],
-  "question": string
+  answer: string,
+  links: string[],
+  pics: string[],
+  questions: string[]
+  qaId: number|string
 }
 
 export type TKnowledgeItem = {
@@ -34,3 +35,10 @@ export type TKnowledgeStreamResponseData = {
   streamId: number;
   type: string;
 };
+
+
+export type TKnowledgeDetail = {
+  
+  qaList: TQAListItem[],
+  
+} & TKnowledgeItem

+ 128 - 1
src/utils/index.ts

@@ -412,4 +412,131 @@ export const getLoginId = ()=> {
 
 export const isSuccess = (status: number)=> {
   return status >= 200 && status < 300
-}
+}
+
+/**
+ * 文件大小单位类型
+ */
+export type FileSizeUnit = 'bit' | 'B' | 'KB' | 'MB' | 'GB' | 'TB';
+
+/**
+ * 文件大小转换配置
+ */
+export interface FileSizeOptions {
+  /** 保留小数位数,默认2位 */
+  precision?: number;
+  /** 是否使用二进制单位(1024进制),默认为true */
+  binary?: boolean;
+  /** 强制转换为指定单位,不自动选择 */
+  unit?: FileSizeUnit;
+  /** 是否添加单位后缀,默认为true */
+  withUnit?: boolean;
+}
+
+/**
+ * 文件大小转换工具
+ * @param size 文件大小(bit)
+ * @param options 转换配置
+ * @returns 转换后的文件大小字符串或数字
+ */
+export function convertFileSize(size: number, options: FileSizeOptions = {}): string | number {
+  const { 
+    precision = 2, 
+    binary = true, 
+    unit, 
+    withUnit = true 
+  } = options;
+  
+  // 检查输入是否为有效数字
+  if (isNaN(size) || !isFinite(size)) {
+    return withUnit ? 'Invalid size' : NaN;
+  }
+  
+  // 处理零值
+  if (size === 0) {
+    return withUnit ? `0${unit || 'B'}` : 0;
+  }
+  
+  // 计算转换基数
+  const base = binary ? 1024 : 1000;
+  
+  // 定义单位列表
+  const units: FileSizeUnit[] = ['bit', 'B', 'KB', 'MB', 'GB', 'TB'];
+  
+  // 如果指定了单位,直接转换
+  if (unit) {
+    const targetIndex = units.indexOf(unit);
+    
+    // 特殊处理bit到B的转换(1字节=8比特)
+    if (unit === 'B') {
+      size = size / 8;
+    } else if (unit === 'bit') {
+      // 如果目标是bit,直接返回原始值
+      return withUnit ? `${size} bit` : size;
+    }
+    
+    // 计算从B到目标单位的转换
+    if (targetIndex > 1) {
+      size = size / 8 / Math.pow(base, targetIndex - 1);
+    }
+    
+    const result = Number(size.toFixed(precision));
+    return withUnit ? `${result} ${unit}` : result;
+  }
+  
+  // 特殊处理bit和B
+  if (size < 8) {
+    return withUnit ? `${size} bit` : size;
+  }
+  
+  if (size < base * 8) {
+    const result = Number((size / 8).toFixed(precision));
+    return withUnit ? `${result} B` : result;
+  }
+  
+  // 自动选择合适的单位
+  let unitIndex = 2; // 从KB开始
+  let convertedSize = size / 8; // 先转换为字节
+  
+  while (convertedSize >= base && unitIndex < units.length - 1) {
+    convertedSize /= base;
+    unitIndex++;
+  }
+  
+  const result = Number(convertedSize.toFixed(precision));
+  return withUnit ? `${result} ${units[unitIndex]}` : result;
+}
+
+// 示例用法
+// const examples = [
+//   1024, // 1 KB (二进制)
+//   1000000, // ~0.95 MB (二进制)
+//   1000000, // 1 MB (十进制)
+//   1099511627776, // 1 TB (二进制)
+//   8, // 1 B
+//   1, // 1 bit
+//   0, // 0 B
+//   -1024, // -1 KB (二进制)
+//   NaN, // Invalid size
+//   Infinity // Invalid size
+// ];
+
+// console.log("二进制单位 (1024进制):");
+// examples.forEach(size => {
+//   console.log(`${size} bit = ${convertFileSize(size)}`);
+// });
+
+// console.log("\n十进制单位 (1000进制):");
+// examples.forEach(size => {
+//   console.log(`${size} bit = ${convertFileSize(size, { binary: false })}`);
+// });
+
+// console.log("\n强制转换为KB:");
+// examples.forEach(size => {
+//   console.log(`${size} bit = ${convertFileSize(size, { unit: 'KB' })}`);
+// });
+
+// console.log("\n不显示单位:");
+// examples.forEach(size => {
+//   console.log(`${size} bit = ${convertFileSize(size, { withUnit: false })}`);
+// });

Einige Dateien werden nicht angezeigt, da zu viele Dateien in diesem Diff geändert wurden.