Ver código fonte

feat: 增加二维码裁剪,地图跳转导航

王晓东 2 semanas atrás
pai
commit
3d40c9a344

+ 4 - 0
README.md

@@ -112,3 +112,7 @@ https://bluebook.feishu.cn/sheets/BW1dsW5NLhQKj7t3fnjc0xvsnpd?from=from_copylink
 https://bluebook.feishu.cn/docx/Ckh0dLOBzo0rjyxrUkrc0ffdn2c   
 
 密码:1#329L42
+
+
+## 后台管理
+https://leads.xiaolanben.com/#/peiyu/aiagent/agent

+ 9 - 2
project.private.config.json

@@ -8,12 +8,19 @@
   "condition": {
     "miniprogram": {
       "list": [
+        {
+          "name": "pages/profile/index",
+          "pathName": "pages/profile/index",
+          "query": "agentId=p_2e73c9d7efaYfDo2-agent_298&shareKey=2%241%24AAAAWrseqFxny9HPTkPIibQEBdl4yz7OICMFf156uBBMw1aTD0vFow7%2FrxLVsqOGTEHIaxZnBn5w9vDFW7T6ySb%2FfQFxSlsjHgPbzmyK%2Bv5LpT3D",
+          "scene": null,
+          "launchMode": "default"
+        },
         {
           "name": "pages/contact/index",
           "pathName": "pages/contact/index",
           "query": "",
-          "scene": null,
-          "launchMode": "default"
+          "launchMode": "default",
+          "scene": null
         }
       ]
     }

+ 3 - 7
src/components/AgentPage/components/AgentQRcode/index.tsx

@@ -6,8 +6,7 @@ import style from "./index.module.less";
 import QrcodeUploadTips from "@/components/qrcode-upload-tips";
 import useEditContactCard from "@/hooks/useEditContactCard";
 import { useAgentStore } from "@/store/agentStore";
-import { pickAndUploadImage } from "@/utils/upload";
-import { EUploadFileScene } from "@/consts/enum";
+import { pickAndUploadImageWithCrop } from "@/utils/upload";
 import Taro from "@tarojs/taro";
 import { TAgentDetail } from "@/types/agent";
 
@@ -26,10 +25,7 @@ export default ({ show, setShow, agent, isVisitor }: IProps) => {
   const handleClick = async () => {
     Taro.showLoading();
     try{
-      const result = await pickAndUploadImage(["album"], EUploadFileScene.OTHER, {
-        maxWidth: 1024,
-        maxHeight: 1024,
-      });
+      const result = await pickAndUploadImageWithCrop(["album"]);
       Taro.hideLoading()
       if (!result?.url) {
         return;
@@ -51,7 +47,7 @@ export default ({ show, setShow, agent, isVisitor }: IProps) => {
 
   const renderQrcode = () => {
     if (agent?.qrCodeUrl) {
-      return <Image src={agent?.qrCodeUrl} showMenuByLongpress mode="aspectFit"></Image>;
+      return <Image src={agent?.qrCodeUrl} showMenuByLongpress mode="widthFix"></Image>;
     }
     
     if(isMine){

+ 2 - 1
src/components/RenderContent/RenderMedia.tsx

@@ -6,6 +6,7 @@ interface Props {
 export default ({ urls }: Props) => {
   const handlePreview = (e: any, index: number) => {
     e.stopPropagation();
+    console.log(e, index, urls)
     Taro.previewMedia({
       current: index,
       //@ts-ignore
@@ -22,7 +23,7 @@ export default ({ urls }: Props) => {
   }
   if (urls.length === 1) {
     return (
-      <View className="w-full max-h-156 overflow-hidden rounded-8">
+      <View className="w-full max-h-156 overflow-hidden rounded-8" onClick={(e) => handlePreview(e, 0)}>
         <Image
           src={urls[0]}
           mode="widthFix"

+ 3 - 1
src/components/button-add/index.tsx

@@ -1,10 +1,12 @@
 import { View, Text, Image } from '@tarojs/components'
 import style from './index.module.less'
+import IconPlusBig from "@/components/icon/icon-plus-big";
 export default ()=> {
   
   return (
     <View className={style.iconAdd}>
-      <Image src='https://cdn.wehome.cn/cmn/png/100/META-H8UKVHWU-KIGP3BIL7M5AYC6XHNUA2-VDZCWI2M-O91.png' className={style.icon}></Image>
+      <IconPlusBig></IconPlusBig>
+      {/* <Image src='https://cdn.wehome.cn/cmn/png/100/META-H8UKVHWU-KIGP3BIL7M5AYC6XHNUA2-VDZCWI2M-O91.png' className={style.icon}></Image> */}
     </View>
   )
 }

+ 10 - 54
src/components/chat-message/MarkdownParser.tsx

@@ -259,12 +259,13 @@ const MarkdownParser = ({ content }: MarkdownParserProps) => {
         return;
       }
 
-      // 处理无序列表(支持嵌套
-      const ulMatch = line.match(/^\s*[-*+]\s+/);
-      if (ulMatch) {
+      // 处理无序列表和有序列表(统一显示为无序列表
+      const listMatch = line.match(/^\s*([-*+]|\d+\.)\s+/);
+      if (listMatch) {
         const depth = getIndentDepth(line); // 当前列表项的缩进深度
-        const text = line.replace(/^\s*[-*+]\s+/, ''); // 提取文本内容
-        const itemKey = getStableKey(`ul-item-${text}`, lineIndex);
+        // 提取文本内容(移除列表标记)
+        const text = line.replace(/^\s*([-*+]|\d+\.)\s+/, ''); 
+        const itemKey = getStableKey(`list-item-${text}`, lineIndex);
         
         // 创建新列表项
         const newItem: ListItem = {
@@ -286,7 +287,7 @@ const MarkdownParser = ({ content }: MarkdownParserProps) => {
               lastParentItem.children.push(poppedLevel);
             }
           } else {
-            // 如果栈为空,直接渲染弹出的层级(这种情况不应该发生)
+            // 如果栈为空,直接渲染弹出的层级
             const listKey = getStableKey(`unexpected-list-${poppedLevel.type}-${poppedLevel.depth}`);
             elements.push(
               <View key={listKey} className={`markdown-list markdown-${poppedLevel.type} depth-${poppedLevel.depth}`}>
@@ -296,10 +297,10 @@ const MarkdownParser = ({ content }: MarkdownParserProps) => {
           }
         }
 
-        // 2. 如果栈为空或当前深度大于栈顶深度,创建新层级
+        // 2. 如果栈为空或当前深度大于栈顶深度,创建新层级(统一使用ul类型)
         if (listStack.length === 0 || depth > listStack[listStack.length - 1].depth) {
           listStack.push({
-            type: 'ul',
+            type: 'ul',  // 所有列表都显示为无序列表
             items: [newItem],
             depth
           });
@@ -310,51 +311,6 @@ const MarkdownParser = ({ content }: MarkdownParserProps) => {
         return;
       }
 
-      // 处理有序列表(支持嵌套)
-      const olMatch = line.match(/^\s*\d+\.\s+/);
-      if (olMatch) {
-        const depth = getIndentDepth(line);
-        const text = line.replace(/^\s*\d+\.\s+/, '');
-        const itemKey = getStableKey(`ol-item-${text}`, lineIndex);
-        
-        const newItem: ListItem = {
-          content: <RichText nodes={parseInlineToNodes(text)} />,
-          key: itemKey
-        };
-
-        while (listStack.length > depth) {
-          const poppedLevel = listStack.pop()!;
-          if (listStack.length > 0) {
-            const parentLevel = listStack[listStack.length - 1];
-            if (parentLevel.items.length > 0) {
-              const lastParentItem = parentLevel.items[parentLevel.items.length - 1];
-              if (!lastParentItem.children) {
-                lastParentItem.children = [];
-              }
-              lastParentItem.children.push(poppedLevel);
-            }
-          } else {
-            const listKey = getStableKey(`unexpected-list-${poppedLevel.type}-${poppedLevel.depth}`);
-            elements.push(
-              <View key={listKey} className={`markdown-list markdown-${poppedLevel.type} depth-${poppedLevel.depth}`}>
-                {renderListItems(poppedLevel.items, 0)}
-              </View>
-            );
-          }
-        }
-
-        if (listStack.length === 0 || depth > listStack[listStack.length - 1].depth) {
-          listStack.push({
-            type: 'ol',
-            items: [newItem],
-            depth
-          });
-        } else {
-          listStack[listStack.length - 1].items.push(newItem);
-        }
-        return;
-      }
-
       // 处理引用
       const quoteMatch = line.match(/^\s*>\s+/);
       if (quoteMatch) {
@@ -403,4 +359,4 @@ const MarkdownParser = ({ content }: MarkdownParserProps) => {
   );
 };
 
-export default MarkdownParser;
+export default MarkdownParser;

+ 16 - 1
src/components/component-list/components/address.ts

@@ -38,7 +38,7 @@ export const useAddress = () => {
     await saveComponent(c);
   };
 
-  const handleChooseAddress = async (value, c: TComponentItem) => {
+  const handleChooseAddress = async (value: any, c: TComponentItem) => {
     setInsertIndex(-1);
     setCurrentComponent(c);
 
@@ -56,7 +56,22 @@ export const useAddress = () => {
     Taro.chooseLocation(option);
   };
 
+  // 唤起本地地图导航
+  const openNavigation = (value:any) => {
+    console.log(value,111)
+    Taro.openLocation({
+      latitude: value.latitude,
+      longitude: value.longitude,
+      name: value.name,
+      address: value.address,
+      scale: 18 // 缩放比例,范围5-18
+    }).catch(err => {
+      console.error('打开地图失败', err)
+    })
+  }
+
   return {
+    openNavigation,
     handleChooseAddress,
   };
 };

+ 9 - 3
src/components/component-list/index.tsx

@@ -47,9 +47,15 @@ export default ({ components, editMode = false }: Props) => {
     useState<TSocialMediaItem | null>(null);
   const [currentLink, setCurrentLink] = useState("");
 
-  const {handleChooseAddress} = useAddress()
+  const {handleChooseAddress, openNavigation} = useAddress()
 
-  
+  const handleMapClick = (editMode:boolean, c: TComponentItem)=> {
+    if(editMode){
+      handleChooseAddress(c.data, c)
+    }else{
+      openNavigation(c.data)
+    }
+  }
 
   const showMediaLink = (mediaItem: TSocialMediaItem, link: string, c: TEntityComponent) => {
     console.log(c,  SocialMediaType.shiping.value, link)
@@ -406,7 +412,7 @@ export default ({ components, editMode = false }: Props) => {
                   editMode={editMode}
                   onDelete={() => handleDelete(c)}
                   onClick={() =>
-                    editMode && handleChooseAddress(c.data, c)
+                    handleMapClick(editMode, c)
                   }
                   onMove={(direction) => handleSort(c, direction)}
                 >

+ 0 - 1
src/pages/chat/components/InputBar/useChatInput.ts

@@ -207,7 +207,6 @@ export const useChatInput = ({
       },
       onReceived: (m) => {
         updateRobotMessage(m.content ?? '', m.body ?? {});
-        console.log(m.content)
       },
       // 流式结束
       onFinished: async (m) => {

+ 2 - 2
src/pages/chat/components/InputBar/useMessage.ts

@@ -67,7 +67,7 @@ export const saveAgentChatContentToServer = async (currentRobotMessage: TRobotMe
     content = (currentRobotMessage?.content as string) ?? ''
   }else if(currentContentType === EContentType.AiseekQA ){
     // 也原样保存至服务器
-    content = (currentRobotMessage?.content as string) ?? ''
+    content = currentRobotMessage?.body?.content ?? currentRobotMessage.content as string
   }else{
     content = (currentRobotMessage?.content as string) ?? ''
   }
@@ -120,7 +120,7 @@ export const usePostMessage = (getCurrentRobotMessage: () => TRobotMessage | und
         scheduleNextSend();
         return;
       }
-
+      // qa 消息不会走此处的逻辑,因为是一次性就返回了
       const message = {
         ...msg,
         body: undefined,

+ 7 - 2
src/pages/chat/index.tsx

@@ -82,7 +82,7 @@ export default function Index() {
   const [isVoice, setIsVoice] = useState(false);
   const [disabled, setDisabled] = useState(false);
 
-  const { destroy, genSessionId } = useTextChat();
+  const { destroy, genSessionId, clearList } = useTextChat();
 
   // 统一的聊天输入逻辑 - 只在主组件中实例化一次
   const chatInputActions = useChatInput({
@@ -96,12 +96,17 @@ export default function Index() {
 
   // 页面显示时刷新数据
   useDidShow(() => {
-    resetAndLoadFirstPage()
+    // 需要清掉当前已经进行的聊天
+    // clearList(); 
   });
 
+  
+
   // 首次进入聊天生成 session id
   useEffect(() => {
     genSessionId();
+    // 重新拉取第一页数据,
+    resetAndLoadFirstPage()
   }, [genSessionId]);
 
   // 页面卸载时清理

+ 1 - 1
src/pages/editor-pages/editor-link-contact/index.tsx

@@ -105,7 +105,7 @@ export default function Index() {
   const renderContactList = () => {
     if (!picked.length) {
       return (
-        <EmptyData type='plane'></EmptyData>
+        <></>
       );  
     }
     return (

+ 2 - 3
src/pages/editor-pages/editor-qrcode/index.tsx

@@ -8,8 +8,7 @@ import style from "./index.module.less";
 import QrcodeUploadTips from "@/components/qrcode-upload-tips";
 import useEditContactCard from "@/hooks/useEditContactCard";
 import { useAgentStore } from "@/store/agentStore";
-import { pickAndUploadImage } from "@/utils/upload";
-import { EUploadFileScene } from "@/consts/enum";
+import { pickAndUploadImageWithCrop } from "@/utils/upload";
 import Taro from "@tarojs/taro";
 
 export default function Index() {
@@ -21,7 +20,7 @@ export default function Index() {
   const handleClick = async () => {
     Taro.showLoading();
     try{
-      const result = await pickAndUploadImage(["album"], EUploadFileScene.OTHER);
+      const result = await pickAndUploadImageWithCrop(["album"]);
       
       Taro.hideLoading();
       if (!result?.url) {

+ 3 - 2
src/pages/voice/components/MyVoiceList/index.tsx

@@ -18,7 +18,7 @@ import { deleteVoice, getVoiceStatus } from "@/service/voice";
 import ThinkingAnimation from "@/components/think-animation";
 import { useModalStore } from "@/store/modalStore";
 import { isSuccess } from "@/utils";
-import Taro from "@tarojs/taro";
+import Taro, { useDidShow } from "@tarojs/taro";
 
 import { getVoices, cloneVoice as _cloneVoice } from "@/service/voice";
 import { useLoadMoreInfinite, createKey } from "@/utils/loadMoreInfinite";
@@ -53,8 +53,9 @@ export default ({ onPlay, onSelect, agent }: Props) => {
     });
     return res.data;
   };
+  const entId = agent?.entId || 0
   const { list, mutate, loadMore } = useLoadMoreInfinite<TVoiceItem[]>(
-    createKey(`api/getvoices/${0}`, 10, [0]),
+    createKey(`api/getvoices/${entId}`, 10, [entId]),
     fetcher
   );
 

+ 1 - 1
src/service/chat.ts

@@ -106,7 +106,7 @@ export const requestStream = <T>({
     const uint8Array = new Uint8Array(chunk.data);
     // console.log('uint8Array: ', uint8Array);
     var string = new TextDecoder("utf-8").decode(uint8Array);
-    // console.log('chunked:', string);
+    console.log('chunked:', string, 8888);
     jsonParser.parseChunk(string, (m) => {
       // console.log('parseChunk', m);
       onReceived(m);

+ 14 - 3
src/store/textChatStore.ts

@@ -5,7 +5,7 @@
 import { create } from "zustand";
 import { generateUUID } from '@/utils/index'
 
-import { EChatRole, TAnyMessage, TRobotMessage, TMessage } from "@/types/bot";
+import { EChatRole, TAnyMessage, TRobotMessage, TMessage, EContentType } from "@/types/bot";
 import {getFormatNow} from '@/utils/timeUtils'
 
 
@@ -87,6 +87,11 @@ export const useTextChat = create<TextChat>((set, get) => ({
       sessionId
     })
   },
+  clearList: ()=> {
+    set({
+      list: [],
+    });
+  },
   // 重置
   destroy: () => {
     set({
@@ -177,8 +182,14 @@ export const useTextChat = create<TextChat>((set, get) => ({
         }
         return message; // 返回未修改的 message
       });
-      
-      return { list: updatedList, scrollTop:  state.autoScroll ?   state.scrollTop + 1 : state.scrollTop}; // 返回新的状态
+      // 如果是qa,由于是一次性输出的结果,需要增加滚动距离
+      let scrollTopValue = 0
+      if(body?.contentType === EContentType.AiseekQA){
+        scrollTopValue = state.scrollTop + 200
+      }else{
+        scrollTopValue = state.scrollTop + 1
+      }
+      return { list: updatedList, scrollTop:  state.autoScroll ?  scrollTopValue : state.scrollTop}; // 返回新的状态
     });
   },
   updateMessageReaction: (msgId, data)=> {

+ 1 - 1
src/utils/messageUtils.ts

@@ -2,7 +2,7 @@ import { EContentType, TAnyMessage } from "@/types/bot";
 import Taro from "@tarojs/taro";
 
 export function formatMessageItem(item: TAnyMessage){
-  if (item.contentType == EContentType.AiseekQA) {
+  if (item.contentType === EContentType.AiseekQA) {
     try {
       const contentJson = JSON.parse(item.content as string);
       item.content = contentJson.answer.text;

+ 47 - 0
src/utils/upload.ts

@@ -70,4 +70,51 @@ export async function pickAndUploadImage(
   } catch (error) {
     throw new Error('Failed to pick or upload image')
   }
+}
+
+export const pickAndUploadImageWithCrop = async (
+  sourceType: Array<'album' | 'camera'> = ['album', 'camera'],
+  cropScale?: keyof Taro.cropImage.CropScale,
+  scene?: EUploadFileScene,
+): Promise<{ url: string; size: number }| null> => {
+
+  const res = await Taro.chooseImage({
+    count: 1,
+    sizeType: ['compressed', 'original'],
+    sourceType,
+  })
+
+  const file = res.tempFiles[0]
+  return new Promise((resolve)=> {
+    Taro.cropImage({
+      src: file.path,
+      cropScale: cropScale ?? "1:1",
+      success: async (cropRes) => {
+        const path = cropRes.tempFilePath;
+        Taro.showLoading();
+        const _scene = scene ?? EUploadFileScene.OTHER
+        const result = await uploadFile(
+          path,
+          _scene,
+        );
+        
+        if(result && ('publicUrl' in result)){
+          Taro.showToast({
+            title: '上传成功',
+            icon: 'success',
+            duration: 1500,
+          })
+          resolve({
+            url: result.publicUrl,
+            size: file.size,
+          })
+          return
+        }
+        resolve(null)
+      },
+      fail(){
+        resolve(null)
+      }
+    });
+  })
 }