浏览代码

feat: 形象删除与下载

王晓东 4 月之前
父节点
当前提交
ad97cb3535
共有 28 个文件被更改,包括 581 次插入414 次删除
  1. 9 2
      project.private.config.json
  2. 16 0
      src/components/AvatarConfirm/index.module.less
  3. 50 9
      src/components/AvatarConfirm/index.tsx
  4. 5 4
      src/components/AvatarMedia/index.tsx
  5. 6 5
      src/components/buttons/WemetaButton.tsx
  6. 6 6
      src/components/custom-share/ShareWxaCard/index.tsx
  7. 7 0
      src/components/icon/IconCrop/index.tsx
  8. 3 0
      src/images/svgs/IconCrop.svg
  9. 49 16
      src/pages/agent-avatar-confirm/index.tsx
  10. 0 57
      src/pages/agent-avatar-confirm/useAvatars.ts
  11. 2 2
      src/pages/agent-avatars/index.module.less
  12. 66 21
      src/pages/agent-avatars/index.tsx
  13. 0 57
      src/pages/agent-avatars/useAvatars.ts
  14. 87 56
      src/pages/agent-gen/components/step/StepConfirm.tsx
  15. 61 5
      src/pages/agent-gen/components/step/StepPick.tsx
  16. 1 8
      src/pages/agent-gen/components/step/StepStart.tsx
  17. 13 6
      src/pages/agent-gen/components/step/index.module.less
  18. 20 12
      src/pages/agent/components/AgentSetting/components/AgentCard/index.tsx
  19. 4 3
      src/pages/agent/components/AgentSetting/components/AgentContactCard/index.tsx
  20. 1 1
      src/pages/agent/components/AgentSetting/components/AgentKnowledgeLib/index.tsx
  21. 1 1
      src/pages/agent/components/AgentSetting/index.tsx
  22. 37 22
      src/pages/agent/index.tsx
  23. 3 20
      src/pages/editor-contact/index.tsx
  24. 1 0
      src/pages/voice/components/VoiceList/index.tsx
  25. 1 8
      src/service/agent.ts
  26. 55 44
      src/store/agentStore.ts
  27. 33 32
      src/utils/index.ts
  28. 44 17
      src/utils/upload.ts

+ 9 - 2
project.private.config.json

@@ -8,12 +8,19 @@
   "condition": {
     "miniprogram": {
       "list": [
+        {
+          "name": "pages/agent-gen/index",
+          "pathName": "pages/agent-gen/index",
+          "query": "avatarUrl=https%3A%2F%2Fxlb-xly.oss-cn-hangzhou.aliyuncs.com%2Fu260727532%2F20250923%2FZgKVD10MiT_831383c0bbaa4f128cca7ebf92dddc68.jpeg",
+          "scene": null,
+          "launchMode": "default"
+        },
         {
           "name": "pages/agent-avatars/index",
           "pathName": "pages/agent-avatars/index",
           "query": "",
-          "scene": null,
-          "launchMode": "default"
+          "launchMode": "default",
+          "scene": null
         },
         {
           "name": "pages/agent/index",

+ 16 - 0
src/components/AvatarConfirm/index.module.less

@@ -5,6 +5,7 @@
   align-items: center;
 }
 .confirmRoundedAvatarWrap{
+  position: relative;
   width: 100px;
   height: 100px;
   margin-bottom: 24px;
@@ -80,3 +81,18 @@
   line-height: 12px;
   text-shadow: 1px 1px 0 rgba(#000, .3);
 }
+
+.cropButton{
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  position: absolute;
+  right: 0;
+  bottom: 0;
+  z-index: 1;
+  width: 36px;
+  height: 36px;
+  border-radius: 100%;
+  background: #FFFFFF;
+  box-shadow: 0 2px 4px 0 rgba(0,0,0, .1);
+}

+ 50 - 9
src/components/AvatarConfirm/index.tsx

@@ -6,24 +6,62 @@ import { uploadAndNavToGenNewAvatar } from "@/utils/avatar";
 import WemetaRadio from '@/components/WemetaRadio'
 import { AvatarMedia } from "@/components/AvatarMedia";
 import WemetaButton from '@/components/buttons/WemetaButton'
+import IconCrop from '@/components/icon/IconCrop'
 import Taro from "@tarojs/taro";
 import style from "./index.module.less";
-import { isSuccess } from "@/utils";
+import { cropImage } from "@/utils/upload";
 import { TAvatarItem } from "@/service/storage";
 interface IProps {
-  avatarItem: TAvatarItem
+  avatarItem: TAvatarItem,
+  enabledChatBg: boolean,
+  setEnabledChatBg: (enabled: boolean) => void
+  onCropDone: (url: string) => void
+  onChange: () => void
+  onConfirm: (edit: TAvatarItem & {enabledChatBg: boolean}) => void
 }
 
-export default function Index({avatarItem}: IProps) {
-
-
-  const [enabledChatBg, setEnabledChatBg] = useState(true)
-
+export default function Index({ avatarItem, enabledChatBg, setEnabledChatBg, onCropDone, onChange, onConfirm }: IProps) {
+  const [avatarData, setAvatarData] = useState({...avatarItem, enabledChatBg})
   const handleConfirm = async () => {
     console.log('confirm')
+    onConfirm({ ...avatarData,  enabledChatBg})
+  }
+  const handleChange = () => {
+    onChange()
+  }
+  const handleCrop = () => {
+    console.log(avatarItem,1111)
+    Taro.showLoading()
+    // url: `${avatarItem.avatarLogo}?x-oss-process=image/resize,w_450/quality,q_60`,
+    Taro.downloadFile({
+      url: avatarItem.avatarLogo,
+      success: async (res) => {
+        if (res.statusCode === 200) {
+          const cropRes = await cropImage(res.tempFilePath)
+          if(cropRes?.url){
+            onCropDone(cropRes.url)
+            setAvatarData({...avatarData, avatarLogo: cropRes.url})
+          }
+        }
+      },
+      fail: (err) => {
+        console.log(err);
+        Taro.hideLoading()
+        Taro.showToast({
+          title: '头像下载失败',
+          icon: 'error',
+          duration: 2000
+        })
+      },
+      complete: (err) => {
+        console.log(err);
+        Taro.hideLoading()
+      }
+    })
   }
-  const handleChange = ()=> {
 
+  const handleEnabledChatBg = ()=> {
+    setEnabledChatBg(!enabledChatBg)
   }
 
   return (
@@ -36,6 +74,9 @@ export default function Index({avatarItem}: IProps) {
               className={style.confirmRoundedAvatar}
               src={avatarItem?.avatarLogo}
             ></Image>
+            <View className={style.cropButton} onClick={handleCrop}>
+              <IconCrop />
+            </View>
           </View>
           <View className={style.confirmChatAvatarBg}>
             <View className={style.confirmChatAvatarImage}>
@@ -53,7 +94,7 @@ export default function Index({avatarItem}: IProps) {
               <View className={style.block3}></View>
             </View>
           </View>
-          <View className="flex-center gap-8 text-14 font-medium leading-22 text-black" onClick={() => setEnabledChatBg((prev) => !prev)}>
+          <View className="flex-center gap-8 text-14 font-medium leading-22 text-black" onClick={handleEnabledChatBg}>
             <WemetaRadio checked={enabledChatBg} checkbox></WemetaRadio>
             启用聊天背景
           </View>

+ 5 - 4
src/components/AvatarMedia/index.tsx

@@ -9,10 +9,11 @@ interface Props {
   className: string;
   mode?: keyof ImageProps.Mode | undefined
   roundedFull?: boolean
+  lazyLoad?: boolean
 }
 
-export const AvatarMedia = ({ source, className, roundedFull = true, mode = 'widthFix' }: Props) => {
-  
+export const AvatarMedia = ({ source, className, roundedFull = true, lazyLoad=true, mode = 'widthFix' }: Props) => {
+
   const videoRef = useRef<React.ComponentType<VideoProps>|null>(null);
   const videoContext = useRef<Taro.VideoContext|null>(null);
   const videoId = useRef(`video-${Math.random().toString(36).substr(2, 9)}`);
@@ -55,7 +56,7 @@ export const AvatarMedia = ({ source, className, roundedFull = true, mode = 'wid
         // src="https://cdn.wehome.cn/cmn/mp4/3/META-H8UKWHWU-YAUTZH7ECGRDC57FD3NI3-CUGVCS8M-CD.mp4"
         src={source}
       />
-    );  
+    );
   }
 
   const _source = source?.length ? `${addOssProcessLowQualityParam(source)}` : DEFAULT_AVATAR
@@ -65,7 +66,7 @@ export const AvatarMedia = ({ source, className, roundedFull = true, mode = 'wid
         mode={mode}
         className={`${className}`}
         src={_source}
-        lazyLoad
+        lazyLoad={lazyLoad}
       />
     </View>
   );

+ 6 - 5
src/components/buttons/WemetaButton.tsx

@@ -1,13 +1,14 @@
 import { View } from "@tarojs/components";
 
 interface IProps {
-  onClick?: ()=> void
+  onClick?: (e:any)=> void
   disabled?: boolean
   className?: string
   type?: 'primary'|'normal'|'danger'
   children: JSX.Element|JSX.Element[]|string
+  style?: React.CSSProperties
 }
-export default function WemetaButton({ onClick, type = 'primary', disabled = false, className='', children }:IProps) {
+export default function WemetaButton({ onClick, type = 'primary', disabled = false, className='', style, children }:IProps) {
   let typeClass = ''
   if(type ==='danger'){
     typeClass = 'bg-white text-red'
@@ -17,12 +18,12 @@ export default function WemetaButton({ onClick, type = 'primary', disabled = fal
     typeClass = 'bg-white'
   }
 
-  const handleClick = ()=> {
+  const handleClick = (e:any)=> {
     if(!disabled){
-      onClick && onClick()
+      onClick && onClick(e)
     }
   }
   return (
-    <View className={`p-14 leading-20 flex font-normal rounded-8 items-center justify-center  ${typeClass} ${disabled ? 'opacity-50': ''} ${className}`} onClick={handleClick}>{children}</View>
+    <View style={style} className={`p-14 leading-20 flex font-normal rounded-8 items-center justify-center font-pingfangSCMedium  ${typeClass} ${disabled ? 'opacity-50': ''} ${className}`} onClick={handleClick}>{children}</View>
   );
 }

+ 6 - 6
src/components/custom-share/ShareWxaCard/index.tsx

@@ -4,7 +4,7 @@
 
 import { Canvas } from "@tarojs/components";
 import style from "./index.module.less";
-import { getCanvasTempPath, savePicture } from "@/utils/index";
+import { getCanvasTempPath, saveFile } from "@/utils/index";
 import { checkPermission, showAuthModal } from "@/utils/auth";
 import { APP_NAME_TEXT, DEFAULT_AVATAR } from '@/config'
 
@@ -38,7 +38,7 @@ export default forwardRef(({ agent }: Props, ref) => {
 
   // 保存至相册
   const saveCardToAlbum = async () => {
-    
+
     Taro.showLoading();
     // 延迟获取 canvas
     await initCanvas();
@@ -51,12 +51,12 @@ export default forwardRef(({ agent }: Props, ref) => {
       showAuthModal("需要您相册权限");
       return;
     }
-    const res = await savePicture(tmpImage);
+    const res = await saveFile(tmpImage);
     if(res){
       Taro.showToast({
         title: '保存成功'
       })
-      return 
+      return
     }
     Taro.showToast({
       title: '保存失败'
@@ -70,7 +70,7 @@ export default forwardRef(({ agent }: Props, ref) => {
       { width: canvasWidth, height: canvasHeight },
       null
     );
-    
+
     stage = await app.init();
     if(!stage){
       return
@@ -156,7 +156,7 @@ export default forwardRef(({ agent }: Props, ref) => {
     return stage;
   };
 
-  
+
 
   useImperativeHandle(ref, () => {
     return {

+ 7 - 0
src/components/icon/IconCrop/index.tsx

@@ -0,0 +1,7 @@
+import { Image } from '@tarojs/components'
+import Icon from '@/images/svgs/IconCrop.svg'
+export default () => {
+  return (
+    <Image src={Icon} mode="widthFix" style={{width: '18px', height: '18px'}}></Image>
+  )
+}

+ 3 - 0
src/images/svgs/IconCrop.svg

@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 18" class="design-iconfont">
+  <path d="M9.75000001,11.25 L9.75000001,12.75 L3,12.75 C2.58578644,12.75 2.25,12.4142136 2.25,12 L2.25,3.75000001 L0,3.75000001 L0,2.25 L2.25,2.25 L2.25,0 L3.75000001,0 L3.75000001,11.25 L9.75000001,11.25 Z M11.25,15 L11.25,3.75 L5.25000001,3.75 L5.25000001,2.25 L12,2.25 C12.4142136,2.25 12.75,2.58578644 12.75,3.00000001 L12.75,11.25 L15,11.25 L15,12.75 L12.75,12.75 L12.75,15 L11.25,15 L11.25,15 Z" transform="translate(1.53 1.53)" fill="#327BF9" fill-rule="nonzero"/>
+</svg>

+ 49 - 16
src/pages/agent-avatar-confirm/index.tsx

@@ -1,30 +1,57 @@
-import { useEffect, useRef, useState } from "react";
-import { Image, View, ScrollView, Video } from "@tarojs/components";
-import { formatSeconds } from "@/utils/index";
+import { useState } from "react";
 import PageCustom from "@/components/page-custom/index";
 import NavBarNormal from "@/components/NavBarNormal/index";
 import AvatarConfirm from '@/components/AvatarConfirm/index'
-import WemetaButton from '@/components/buttons/WemetaButton'
-import Taro from "@tarojs/taro";
-import style from "./index.module.less";
 
-import { isSuccess } from "@/utils";
+import Taro, { nextTick } from "@tarojs/taro";
+
 import { TAvatarItem } from "@/service/storage";
-import { useAgentStore } from "@/store/agentStore";
+import { useAgentStore, useAgentStoreActions } from "@/store/agentStore";
 interface IProps {
-  avatarItem: TAvatarItem
+  avatarItem?: TAvatarItem
 }
 
-export default function Index({avatarItem}: IProps) {
-
-  const currentEditAvatar = useAgentStore( state => state.currentEditAvatar)
-  const [enabledChatBg, setEnabledChatBg] = useState(true)
+export default function Index({ avatarItem }: IProps) {
+  // 可以由父组件传入或从 agentStore 中获取
+  const currentEditAvatar = avatarItem ?? useAgentStore(state => state.currentEditAvatar)
+  const agentEdit = useAgentStore(state => state.agentEdit)
+  const { updateEditAgent, saveAgent } = useAgentStoreActions()
+  const currentItem = { ...(currentEditAvatar || {}) }
+  const [enabledChatBg, setEnabledChatBg] = useState(agentEdit?.enabledChatBg ?? false)
 
-  const handleConfirm = async () => {
+  const handleConfirm = async (edit: TAvatarItem & { enabledChatBg: boolean }) => {
     console.log('confirm')
+    console.log(edit, 2222)
+    if (edit.avatarLogo && edit.avatarUrl) {
+      updateEditAgent({...edit})
+      // 如果是编辑,直接编辑形象成功
+      if(agentEdit?.agentId){
+        nextTick(()=> {
+          saveAgent()
+          Taro.showToast({title: '形象设置成功', icon: 'success'})
+          setTimeout(()=> {
+            Taro.navigateBack()
+          }, 2000)
+        })
+        return
+      }
+      // 如果是新建智能体,只是暂时设置并不更新至服务器
+      Taro.showToast({title: '形象设置成功', icon: 'success'})
+      setTimeout(()=> {
+        Taro.navigateBack()
+      }, 2000)
+    }
+
+  }
+  const handleChange = () => {
+    Taro.navigateBack()
   }
-  const handleChange = ()=> {
 
+  const handleCropDone = (url: string) => {
+    console.log('handleCropDone', url)
+    if (currentItem?.avatarLogo) {
+      currentItem.avatarLogo = url
+    }
   }
 
 
@@ -32,7 +59,13 @@ export default function Index({avatarItem}: IProps) {
     <PageCustom>
       <NavBarNormal>形象确认</NavBarNormal>
       <>
-        {!!currentEditAvatar && <AvatarConfirm avatarItem={currentEditAvatar} />}
+        {!!currentEditAvatar && <AvatarConfirm
+          avatarItem={currentEditAvatar}
+          enabledChatBg={enabledChatBg}
+          setEnabledChatBg={setEnabledChatBg}
+          onCropDone={handleCropDone}
+          onChange={handleChange}
+          onConfirm={handleConfirm} />}
       </>
     </PageCustom>
   );

+ 0 - 57
src/pages/agent-avatar-confirm/useAvatars.ts

@@ -1,57 +0,0 @@
-import { fetchMyAvatars, TAvatarItem } from "@/service/storage";
-import { isSuccess } from "@/utils";
-import { useState, useCallback } from "react";
-
-export const useAvatars = (initPageSize: number = 20) => {
-  const [avatars, setAvatars] = useState<TAvatarItem[]>([]);
-  const [pageIndex, setPageIndex] = useState(1);
-  const [pageSize] = useState(initPageSize);
-  const [totalCount, setTotalCount] = useState(0);
-  const [isLoading, setIsLoading] = useState(false);
-  const [hasMore, setHasMore] = useState(true);
-
-  const loadAvatars = useCallback(async (reset: boolean = false) => {
-    if (isLoading) return;
-    setIsLoading(true);
-    const currentPage = reset ? 1 : pageIndex;
-    try {
-      const response = await fetchMyAvatars({ pageSize, pageIndex: currentPage });
-      if (isSuccess(response.status)) {
-        const { data, totalCount: total } = response.data;
-        setTotalCount(total);
-        if (reset) {
-          setAvatars(data);
-        } else {
-          setAvatars(prev => [...prev, ...data]);
-        }
-        setHasMore((currentPage * pageSize) < total);
-        setPageIndex(currentPage + 1);
-      }
-    } finally {
-      setIsLoading(false);
-    }
-  }, [isLoading, pageIndex, pageSize]);
-
-  const resetAvatars = useCallback(() => {
-    setAvatars([]);
-    setPageIndex(1);
-    setTotalCount(0);
-    setHasMore(true);
-  }, []);
-
-  // 首次加载
-  const initLoad = useCallback(async () => {
-    resetAvatars();
-    await loadAvatars(true);
-  }, [resetAvatars, loadAvatars]);
-
-  return {
-    avatars,
-    isLoading,
-    hasMore,
-    totalCount,
-    loadAvatars,
-    resetAvatars,
-    initLoad,
-  };
-};

+ 2 - 2
src/pages/agent-avatars/index.module.less

@@ -74,8 +74,8 @@
   display: flex;
   align-items: center;
   justify-content: center;
-  width: 28px;
-  height: 28px;
+  width: 40px;
+  height: 40px;
 }
 
 .videoContainer {

+ 66 - 21
src/pages/agent-avatars/index.tsx

@@ -1,9 +1,9 @@
 import { useEffect, useRef, useState } from "react";
 import { Image, View, ScrollView, Video } from "@tarojs/components";
-import { formatSeconds } from "@/utils/index";
+import { formatSeconds, saveMediaFile } from "@/utils/index";
 import PageCustom from "@/components/page-custom/index";
 import NavBarNormal from "@/components/NavBarNormal/index";
-
+import { checkPermission, showAuthModal } from "@/utils/auth";
 import { uploadAndNavToGenNewAvatar } from "@/utils/avatar";
 import IconPlusBig from "@/components/icon/icon-plus-big";
 import IconPlayWhite24 from '@/components/icon/IconPlayWhite20'
@@ -14,7 +14,7 @@ import style from "./index.module.less";
 import { TAvatarItem } from "@/service/storage";
 import { useAgentStore, useAgentStoreActions } from "@/store/agentStore";
 
-import Taro, { useDidShow } from "@tarojs/taro";
+import Taro, { useDidShow, useUnload } from "@tarojs/taro";
 import { isSuccess } from "@/utils";
 import WemetaRadio from "@/components/WemetaRadio/index";
 import { useModalStore } from "@/store/modalStore";
@@ -25,7 +25,7 @@ import WemetaButton from "@/components/buttons/WemetaButton";
 export default function Index() {
   const agent = useAgentStore((state) => state.agent);
 
-  const {setCurrentEditAvatar} =  useAgentStoreActions()
+  const { setCurrentEditAvatar } = useAgentStoreActions()
   const [scrollTop, setScrollTop] = useState(0);
   const scrollPositionRef = useRef(0);
 
@@ -44,12 +44,12 @@ export default function Index() {
   const [current, setCurrent] = useState<TAvatarItem | null>(null);
 
   // 选择形象
-  const handleSelect = async (e:any, item: TAvatarItem) => {
+  const handleSelect = async (e: any, item: TAvatarItem) => {
     e.stopPropagation()
     console.log(item);
-    if(current?.avatarId === item.avatarId){
+    if (current?.avatarId === item.avatarId) {
       setCurrent(null)
-    }else{
+    } else {
       setCurrent(item);
     }
 
@@ -67,22 +67,23 @@ export default function Index() {
   };
 
   const handleDelete = async (e: any) => {
-    e.stopPropagation();
-    if(!current || !current.canDel){
-        return
-      }
+    e.stopPropagation()
+    if (!current || !current.canDel) {
+      return
+    }
     showModal({
       content: <>确认删除该形象?</>,
       async onConfirm() {
         const response = await deleteAvatar(current.avatarId);
         if (isSuccess(response.status)) {
+          setCurrent(null)
           mutate();
         }
       },
     });
   };
 
-  const handlePreview = (index: number)=> {
+  const handlePreview = (index: number) => {
 
     Taro.previewMedia({
       current: index,
@@ -95,11 +96,55 @@ export default function Index() {
       }),
     })
   }
+  const saveMedia = async (tmpPath: string) => {
+    const res = await saveMediaFile(tmpPath, current?.isVideo);
+    if (res) {
+      Taro.showToast({
+        title: '保存成功'
+      })
+      return
+    }
+    Taro.showToast({
+      title: '保存失败'
+    })
+  }
+  const handleDownload = async () => {
+    if (!current?.avatarUrl) {
+      return
+    }
+
+    // 保存至相册
+    Taro.showLoading();
+    const authed = await checkPermission("scope.writePhotosAlbum");
+    if (!authed) {
+      Taro.hideLoading();
+      showAuthModal("需要您相册权限");
+      return;
+    }
+
+    Taro.downloadFile({
+      url: current.avatarUrl,
+      success: (res) => {
+        if (res.statusCode === 200) {
+          res.tempFilePath
+          saveMedia(res.tempFilePath)
+        }
+      },
+      fail: () => {
+        Taro.hideLoading();
+      },
+    })
+
+  };
 
   useDidShow(() => {
     mutate()
   })
 
+  useUnload(()=> {
+    Taro.hideLoading()
+  })
+
   const renderMedia = (avatar: TAvatarItem, index: number) => {
     if (avatar.isVideo) {
       return <>
@@ -142,14 +187,15 @@ export default function Index() {
         >
 
           <View
-              className={style.selected}
-              onClick={(e) => handleSelect(e, avatar)}
-            >
+            className={style.selected}
+            onClick={(e) => handleSelect(e, avatar)}
+          >
             <WemetaRadio theme="light" checkbox checked={isCurrentSelected} />
           </View>
-          {currentUsed && <View className={style.gridItemActivedMark}></View>}
+
+          {currentUsed && <View className={style.gridItemCurrentUsedMark}></View>}
           {/* <View className={style.gridItemCurrentUsedMark}></View> */}
-          {!avatar.isOriginal && <View className={style.aiTips}>图片由AI生成</View> }
+          {!avatar.isOriginal && <View className={style.aiTips}>图片由AI生成</View>}
           {renderMedia(avatar, index)}
         </View>
       );
@@ -187,10 +233,9 @@ export default function Index() {
           </View>
         </ScrollView>
         <BottomBar className="pt-12 px-16">
-
-            <WemetaButton type='danger' className="w-88 text-red" disabled={!current?.canDel} onClick={handleDelete}>删除</WemetaButton>
-            <WemetaButton type='normal' className="w-102" disabled={!current}>下载</WemetaButton>
-            <WemetaButton type="primary" className="flex-1" disabled={!current} onClick={handleSetBackground}>设置为聊天背景</WemetaButton>
+          <WemetaButton type='danger' className="w-88 text-red" disabled={!current?.canDel} onClick={handleDelete}>删除</WemetaButton>
+          <WemetaButton type='normal' className="w-102" disabled={!current} onClick={handleDownload}>下载</WemetaButton>
+          <WemetaButton type="primary" className="flex-1" disabled={!current} onClick={handleSetBackground}>设置为聊天背景</WemetaButton>
 
         </BottomBar>
       </View>

+ 0 - 57
src/pages/agent-avatars/useAvatars.ts

@@ -1,57 +0,0 @@
-import { fetchMyAvatars, TAvatarItem } from "@/service/storage";
-import { isSuccess } from "@/utils";
-import { useState, useCallback } from "react";
-
-export const useAvatars = (initPageSize: number = 20) => {
-  const [avatars, setAvatars] = useState<TAvatarItem[]>([]);
-  const [pageIndex, setPageIndex] = useState(1);
-  const [pageSize] = useState(initPageSize);
-  const [totalCount, setTotalCount] = useState(0);
-  const [isLoading, setIsLoading] = useState(false);
-  const [hasMore, setHasMore] = useState(true);
-
-  const loadAvatars = useCallback(async (reset: boolean = false) => {
-    if (isLoading) return;
-    setIsLoading(true);
-    const currentPage = reset ? 1 : pageIndex;
-    try {
-      const response = await fetchMyAvatars({ pageSize, pageIndex: currentPage });
-      if (isSuccess(response.status)) {
-        const { data, totalCount: total } = response.data;
-        setTotalCount(total);
-        if (reset) {
-          setAvatars(data);
-        } else {
-          setAvatars(prev => [...prev, ...data]);
-        }
-        setHasMore((currentPage * pageSize) < total);
-        setPageIndex(currentPage + 1);
-      }
-    } finally {
-      setIsLoading(false);
-    }
-  }, [isLoading, pageIndex, pageSize]);
-
-  const resetAvatars = useCallback(() => {
-    setAvatars([]);
-    setPageIndex(1);
-    setTotalCount(0);
-    setHasMore(true);
-  }, []);
-
-  // 首次加载
-  const initLoad = useCallback(async () => {
-    resetAvatars();
-    await loadAvatars(true);
-  }, [resetAvatars, loadAvatars]);
-
-  return {
-    avatars,
-    isLoading,
-    hasMore,
-    totalCount,
-    loadAvatars,
-    resetAvatars,
-    initLoad,
-  };
-};

+ 87 - 56
src/pages/agent-gen/components/step/StepConfirm.tsx

@@ -1,73 +1,104 @@
 import { View, Image } from "@tarojs/components";
 import React, { useState } from "react";
-import style from "./index.module.less";
-import WemetaRadio from '@/components/WemetaRadio'
-import { AvatarMedia } from "@/components/AvatarMedia";
-import WemetaButton from '@/components/buttons/WemetaButton'
-import Taro from "@tarojs/taro";
+import Taro, { nextTick } from "@tarojs/taro";
 import { TAvatarItem } from "@/service/storage";
-// import { editAgentAvatar } from "@/service/agent";
+import AvatarConfirm from '@/components/AvatarConfirm/index'
 import { useAgentStore, useAgentStoreActions } from "@/store/agentStore";
 interface IProps {
   prev: ()=>void
   pickedAvatar: TAvatarItem
 }
 export default React.memo(function Index({prev, pickedAvatar}:IProps) {
-  const agent = useAgentStore((state) => state.agent);
-  const {fetchAgent} = useAgentStoreActions();
+  const currentItem = { ...(pickedAvatar || {}) }
+  const agentEdit = useAgentStore((state) => state.agentEdit);
+  const { saveAgent, updateEditAgent} = useAgentStoreActions();
   const [enabledChatBg, setEnabledChatBg] = useState(true)
 
-  const handleConfirm = async () => {
-    if(!agent?.agentId){
-      return
+  const handleConfirm = async (edit: TAvatarItem & { enabledChatBg: boolean }) => {
+    if (edit.avatarLogo && edit.avatarUrl) {
+      updateEditAgent({...edit})
+      // 如果是编辑,直接编辑形象成功
+      if(agentEdit?.agentId){
+        nextTick(()=> {
+          saveAgent()
+          Taro.showToast({title: '形象设置成功', icon: 'success'})
+          setTimeout(()=> {
+            Taro.navigateBack()
+          }, 2000)
+        })
+        return
+      }
+      // 如果是新建智能体,只是暂时设置并不更新至服务器
+      Taro.showToast({title: '形象设置成功', icon: 'success'})
+      setTimeout(()=> {
+        Taro.navigateBack()
+      }, 2000)
     }
-    // await editAgentAvatar(
-    //   agent.agentId,
-    //   pickedAvatar.avatarId,
-    //   enabledChatBg,
-    // );
-    await fetchAgent(agent.agentId)
 
-    Taro.redirectTo({url: '/pages/agent/index'})
+
+
+
+  }
+  const handleChange = () => {
+    prev()
+  }
+
+  const handleCropDone = (url: string) => {
+    console.log('handleCropDone', url)
+    if(url){
+      currentItem.avatarLogo = url
+      updateEditAgent({avatarLogo: url})
+    }
   }
   return (
-    <View>
-      <View className={style.confirmContainer}>
-        <View className={style.confirmRoundedAvatarWrap}>
-          <Image
-            mode='aspectFill'
-            className={style.confirmRoundedAvatar}
-            src={pickedAvatar?.avatarLogo}
-          ></Image>
-        </View>
-        <View className={style.confirmChatAvatarBg}>
-          <View className={style.confirmChatAvatarImage}>
-            <AvatarMedia roundedFull={false}  source={pickedAvatar?.avatarUrl} className={style.confirmChatAvatarImage} />
-            {!pickedAvatar.isOriginal && <View className={style.aiTips}>图片由AI生成</View> }
-            {/* <Image
-              mode="widthFix"
-              className="w-full"
-              src={pickedAvatar.avatarUrl}
-            ></Image> */}
-          </View>
-          <View className={style.confirmChatAvatarBgCover}>
-              <View className={style.block1}></View>
-              <View className={style.block2}></View>
-              <View className={style.block3}></View>
-          </View>
-        </View>
-        <View className="flex-center gap-8 text-14 font-medium leading-22 text-black" onClick={()=> setEnabledChatBg((prev)=> !prev)}>
-          <WemetaRadio checked={enabledChatBg} checkbox></WemetaRadio>
-          启用聊天背景
-        </View>
-      </View>
+    <>
+    {!!currentItem && <AvatarConfirm
+      avatarItem={currentItem}
+      enabledChatBg={enabledChatBg}
+      setEnabledChatBg={setEnabledChatBg}
+      onCropDone={handleCropDone}
+      onChange={handleChange}
+      onConfirm={handleConfirm} />}
+  </>
+  )
+  // return (
+  //   <View>
+  //     <View className={style.confirmContainer}>
+  //       <View className={style.confirmRoundedAvatarWrap}>
+  //         <Image
+  //           mode='aspectFill'
+  //           className={style.confirmRoundedAvatar}
+  //           src={pickedAvatar?.avatarLogo}
+  //         ></Image>
+  //       </View>
+  //       <View className={style.confirmChatAvatarBg}>
+  //         <View className={style.confirmChatAvatarImage}>
+  //           <AvatarMedia roundedFull={false}  source={pickedAvatar?.avatarUrl} className={style.confirmChatAvatarImage} />
+  //           {!pickedAvatar.isOriginal && <View className={style.aiTips}>图片由AI生成</View> }
+  //           {/* <Image
+  //             mode="widthFix"
+  //             className="w-full"
+  //             src={pickedAvatar.avatarUrl}
+  //           ></Image> */}
+  //         </View>
+  //         <View className={style.confirmChatAvatarBgCover}>
+  //             <View className={style.block1}></View>
+  //             <View className={style.block2}></View>
+  //             <View className={style.block3}></View>
+  //         </View>
+  //       </View>
+  //       <View className="flex-center gap-8 text-14 font-medium leading-22 text-black" onClick={()=> setEnabledChatBg((prev)=> !prev)}>
+  //         <WemetaRadio checked={enabledChatBg} checkbox></WemetaRadio>
+  //         启用聊天背景
+  //       </View>
+  //     </View>
 
-      <View className="bottom-bar">
-        <View className="grid grid-cols-2 gap-8 px-20 py-12">
-          <View className={`button-rounded`} onClick={prev}>更换形象</View>
-          <WemetaButton className="flex-1" onClick={handleConfirm}>确定</WemetaButton>
-        </View>
-      </View>
-    </View>
-  );
+  //     <View className="bottom-bar">
+  //       <View className="grid grid-cols-2 gap-8 px-20 py-12">
+  //         <View className={`button-rounded`} onClick={prev}>更换形象</View>
+  //         <WemetaButton className="flex-1" onClick={handleConfirm}>确定</WemetaButton>
+  //       </View>
+  //     </View>
+  //   </View>
+  // );
 });

+ 61 - 5
src/pages/agent-gen/components/step/StepPick.tsx

@@ -23,6 +23,37 @@ export default React.memo(function Index({ prev, next, taskId, avatars, videos,
   const [shouldPoll, setShouldPoll] = useState(false);
   const [videoGenerating, setVideoGenerating] = useState(false);
   const [currentTaskId, setCurrentTaskId] = useState<string|number>(taskId);
+  const [displayText, setDisplayText] = useState<'initial' | 'updated'>('initial');
+
+  // Reset display text when videoGenerating changes
+  useEffect(() => {
+    setDisplayText('initial');
+  }, [videoGenerating]);
+
+  // Handle the display text timing
+  useEffect(() => {
+    let timer: NodeJS.Timeout;
+
+    if (!videoGenerating) {
+      // For non-video generation
+      if (displayText === 'initial') {
+        timer = setTimeout(() => {
+          setDisplayText('updated');
+        }, 10000); // 10 seconds
+      }
+    } else {
+      // For video generation
+      if (displayText === 'initial') {
+        timer = setTimeout(() => {
+          setDisplayText('updated');
+        }, 20000); // 20 seconds
+      }
+    }
+
+    return () => {
+      if (timer) clearTimeout(timer);
+    };
+  }, [videoGenerating, displayText]);
 
   const avatarsList = (videoGenerating) ? videos : avatars
 
@@ -95,6 +126,7 @@ export default React.memo(function Index({ prev, next, taskId, avatars, videos,
     setShouldPoll(true);
   }, [taskId])
 
+  const isTaskProcessing = data?.data.status !== 'process_fail' && data?.data.status !== 'process_success'
 
   const genVideo = async () => {
     const a  = avatars[currentSwiperIndex]
@@ -179,6 +211,25 @@ export default React.memo(function Index({ prev, next, taskId, avatars, videos,
     );
   };
 
+  // 生成中提示文本
+  const renderStatusText = () => {
+    if (!videoGenerating) {
+      // Non-video generation flow
+      if (displayText === 'initial') {
+        return <>预计等待时间:20秒</>;
+      } else {
+        return <>退出不影响生成</>;
+      }
+    } else {
+      // Video generation flow
+      if (displayText === 'initial') {
+        return <>预计等待时间:1分钟</>;
+      } else {
+        return <>退出不影响生成速度</>;
+      }
+    }
+  };
+
 
 
   const renderProcessingStatus = () => {
@@ -199,9 +250,14 @@ export default React.memo(function Index({ prev, next, taskId, avatars, videos,
     }
 
     return <View className={`${style.pickAvatarCard} ${style.pickGenCard}`}>
-      <View className="flex items-center gap-4">
-        <IconStarColor></IconStarColor>{" "}
-        {renderProcessingStatus()}
+      <View className="flex flex-col justify-center items-center gap-16">
+        <View className="flex items-center gap-4">
+          <IconStarColor></IconStarColor>{" "}
+          {renderProcessingStatus()}
+        </View>
+        {isTaskProcessing && <View className="leading-18 text-12 text-primary">
+          {renderStatusText()}
+        </View>}
       </View>
     </View>
   }
@@ -216,8 +272,8 @@ export default React.memo(function Index({ prev, next, taskId, avatars, videos,
 
       <View className="bottom-bar">
         <View className="flex items-center gap-8 px-20 py-12">
-          <WemetaButton type="normal" disabled={videoGenerating && !videos.length}  className="w-[76px]" onClick={handlePrev}>上一步</WemetaButton>
-          {!videoGenerating &&  <WemetaButton className="flex-1" type="normal" onClick={genVideo} disabled={!avatars.length} >生成微视频</WemetaButton> }
+          <WemetaButton type="normal" disabled={videoGenerating && !videos.length}  className="w-[88px]" onClick={handlePrev}>重新上传</WemetaButton>
+          {!videoGenerating &&  <WemetaButton className={`${style.pickBtnGenVideo}`} type="normal" onClick={genVideo} disabled={!avatars.length} >生成微视频</WemetaButton> }
           <WemetaButton className="flex-1" onClick={goNext} disabled={(!avatars.length && !videoGenerating) || (videoGenerating && !videos.length )} >使用这张</WemetaButton>
         </View>
       </View>

+ 1 - 8
src/pages/agent-gen/components/step/StepStart.tsx

@@ -68,15 +68,8 @@ export default React.memo(function StepStart({ next, setTaskId }: IProps) {
           </View>
           <Image className={style.startCardImage} src={currentAvatarUrl} mode="widthFix"></Image>
         </View>
+        <View className="text-12 text-gray-4 mt-16 text-center">用于头像或聊天背景</View>
       </View>
-      {/* <View className="mb-24">
-        <View className="text-14 font-medium text-black mb-10">创意描述<Text className="text-12 text-gray-4">(非必填)</Text></View>
-        <WemetaTextarea
-          value={value}
-          onInput={handleInput}
-          placeholder="描述你想要生成的画面和动作。例如:职场精英在点头微笑"
-        />
-      </View> */}
       <View className="bottom-bar">
         <View className="px-20 py-12">
           <View className={`button-rounded-big gap-4 ${style.startGenButton}`} onClick={handleClick}>

+ 13 - 6
src/pages/agent-gen/components/step/index.module.less

@@ -1,6 +1,7 @@
 .startContainer{
   padding: 12px 0 0;
   display: flex;
+  flex-direction: column;
   align-items: center;
   justify-content: center;
 }
@@ -10,8 +11,8 @@
   align-items: center;
   justify-content: center;
   width: 300px;
-  height: 554px;
-  border-radius: 16px;
+  height: 530px;
+  border-radius: 12px;
   overflow: hidden;
   background-color: white;
 }
@@ -49,7 +50,7 @@
 }
 .pickAvatarCard{
   width: 300px;
-  height: 533px;
+  height: 530px;
   border-radius: 12px;
 }
 .pickGenCard{
@@ -60,6 +61,11 @@
   background-size: auto 400%;
   // animation: gradientMove 4s linear infinite;
 }
+.pickBtnGenVideo{
+  flex: 1;
+  color: white;
+  background-image: linear-gradient(-56deg, #317CFA 0%, #FF2DF8 100%);
+}
 
 @keyframes gradientMove {
   0% {
@@ -70,6 +76,7 @@
   }
 }
 
+
 .mySwiper{
   width: 300px;
   height: 533px;
@@ -144,7 +151,7 @@
   width: 100px;
   height: 100px;
   overflow: hidden;
-  
+
   border-radius: 100%;
 }
 .confirmChatAvatarBg{
@@ -176,7 +183,7 @@
   padding: 0 24px 54px;
   border-radius: 24px;
   gap: 8px;
-  background: linear-gradient(180deg, rgba(255, 255, 255, 0) 50.12%, #D6D6D6 100%); 
+  background: linear-gradient(180deg, rgba(255, 255, 255, 0) 50.12%, #D6D6D6 100%);
 }
 .block{
   background-color: white;
@@ -209,4 +216,4 @@
   color: rgba(#fff, .5);
   line-height: 12px;
   text-shadow: 1px 1px 0 rgba(#000, .3);
-}
+}

+ 20 - 12
src/pages/agent/components/AgentSetting/components/AgentCard/index.tsx

@@ -5,27 +5,35 @@ import Taro from "@tarojs/taro";
 import { uploadAndNavToGenNewAvatar } from "@/utils/avatar";
 import WemetaRadio from "@/components/WemetaRadio";
 import { useAgentStore, useAgentStoreActions } from "@/store/agentStore";
-import { editAgentChatBg } from "@/service/agent";
+
 import { isSuccess } from "@/utils";
 import { fetchMyAvatars } from "@/service/storage";
 import { AvatarMedia } from "@/components/AvatarMedia";
 
 export default () => {
   const agentEdit = useAgentStore((state) => state.agentEdit);
-  const { fetchAgent } = useAgentStoreActions();
+  const { saveAgent, updateEditAgent } = useAgentStoreActions();
 
   const handleChange = async (isChecked: boolean) => {
-    if (!agentEdit?.agentId || !agentEdit.avatarUrl) {
+    if (!agentEdit?.agentId ) {
+      // 如果是新建,只是改变状态,不保存至服务器
+      updateEditAgent({ enabledChatBg: isChecked })
       return;
     }
-    const response = await editAgentChatBg(agentEdit.agentId, isChecked);
-    if (isSuccess(response.status)) {
-      Taro.showToast({
-        title: isChecked ? "启用成功" : "取消启用",
-        icon: "success",
-      });
-      fetchAgent(agentEdit.agentId); // 刷新智能体信息
+    Taro.showLoading()
+    try{
+      const agent = await saveAgent({enabledChatBg: isChecked});
+      Taro.hideLoading()
+      if (agent) {
+        Taro.showToast({
+          title: isChecked ? "启用成功" : "取消启用",
+          icon: "success",
+        });
+      }
+    }catch(e){
+      Taro.hideLoading()
     }
+
   };
   const handleClick = async () => {
     const response = await fetchMyAvatars({ pageIndex: 1, pageSize: 2 });
@@ -61,8 +69,8 @@ export default () => {
 
     return (
       <View className="relative overflow-hidden w-full h-full">
-        <AvatarMedia source={agentEdit.avatarUrl} className={style.card} />
-        {agentEdit?.enabledChatBg &&  <View className={style.confirmChatAvatarBgCover}>
+        <AvatarMedia mode="aspectFill" source={agentEdit.avatarUrl} className={style.card} lazyLoad={false} />
+        {agentEdit?.avatarUrl &&  <View className={style.confirmChatAvatarBgCover}>
           <View className={style.block1}></View>
           <View className={style.block2}></View>
           <View className={style.block3}></View>

+ 4 - 3
src/pages/agent/components/AgentSetting/components/AgentContactCard/index.tsx

@@ -2,6 +2,7 @@ import { View, Text, Input} from "@tarojs/components"
 import style from './index.module.less'
 import IconPlusBlue from "@/components/icon/icon-plus-blue"
 import { useAgentStore, useAgentStoreActions } from "@/store/agentStore";
+import Taro from "@tarojs/taro";
 // import ContactIcon from '@/components/ContactIcon'
 export default () => {
   const agentEdit = useAgentStore((state)=> state.agentEdit);
@@ -18,9 +19,9 @@ export default () => {
   };
 
   const handleEdit = ()=> {
-    // Taro.navigateTo({
-    //   url: `/pages/editor-contact/index?agentId=${agent?.agentId}`,
-    // })
+    Taro.navigateTo({
+      url: `/pages/editor-contact/index?agentId=${agentEdit?.agentId}`,
+    })
   }
 
   return(

+ 1 - 1
src/pages/agent/components/AgentSetting/components/AgentKnowledgeLib/index.tsx

@@ -41,7 +41,7 @@ export default function Index() {
       </CardListItem>
       <View className="">
         <View className="flex flex-col gap-12 px-16 pb-16">
-          <View className="flex rounded-8 p-16 gap-16 bg-gray-3">
+          <View className="flex rounded-8 p-16 gap-16 bg-gray-1">
             <View className="flex-1">
               <View className="text-14 font-medium leading-22 text-black pb-2">
                 个人知识

+ 1 - 1
src/pages/agent/components/AgentSetting/index.tsx

@@ -33,7 +33,7 @@ export default forwardRef(function Index({save}: {save: ()=> void}, ref) {
   const buttonText = agentEdit?.agentId ? '保存' : '创建'
 
   return (
-    <View className="pb-64">
+    <View className="pb-118">
       <AgentCard></AgentCard>
       <View className="mb-20">
         <AgentContactCard></AgentContactCard>

+ 37 - 22
src/pages/agent/index.tsx

@@ -17,24 +17,29 @@ import { useConfirms } from './hooks/useConfirms'
 
 export default function Index() {
   restrictedPage()
+
   const router = useRouter();
-  const headerHeight = useAppStore((state) => state.headerHeight);
   const agentId = router.params.agentId;
+
+  const headerHeight = useAppStore((state) => state.headerHeight);
   const agentEdit = useAgentStore((state) => state.agentEdit);
-  const { fetchAgent, saveAgent, updateEditAgent } = useAgentStoreActions();
+  const { fetchAgent, saveAgent, createAgent, updateEditAgent } = useAgentStoreActions();
   const { fetchMyEntList } = useUserStore();
   const { setComponentList } = useComponentStore();
   const { confirmNavBack, confirmCreateAgent, confirmEditWebsite, confirmPersonalityAndGreeting, confirmSaveAvatar } = useConfirms();
-  const [tabIndex, setTabIndex] = useState('1');
+  const { fetchAgents } = useAgentStoreActions();
+
+  const [tabIndex, setTabIndex] = useState<'agent'|'website'>('agent');
 
-  // Create ref for AgentSetting component
+  // 润色方法引用
   const agentSettingRef = useRef<{ polishAllTexts: () => Promise<void> }>(null);
 
-  const handleTabIndexChange = async (index: string) => {
-    if(index === '2' && !agentEdit?.agentId){
-      const result = await confirmCreateAgent()
-      if(result){
-        // await saveAgent()
+  const handleTabIndexChange = async (index: 'agent' | 'website') => {
+    // 如果要编辑微官网组件且是新建智能体,则提醒用户是否先创建智能体,只有创建智能体后才能编辑微官网
+    if(index === 'website' && !agentEdit?.agentId){
+      const isConfirm = await confirmCreateAgent()
+      if(isConfirm){
+        await createAgent()
         setTabIndex(index)
       }
       return
@@ -55,20 +60,15 @@ export default function Index() {
 
   const tabList = [
     {
-      key: "1",
+      key: "agent",
       label: "智能体",
     },
     {
-      key: "2",
+      key: "website",
       label: "微官网",
     },
   ];
 
-  const { fetchAgents } = useAgentStoreActions();
-
-  useEffect(()=> {
-    fetchAgents()
-  }, [])
 
   const handleSave  = async () => {
     if(!agentId && !agentEdit?.agentId){
@@ -91,14 +91,25 @@ export default function Index() {
           return;
         }
       }
+      const res = await createAgent()
+      if(res){
+        const goEditWebsite = await confirmEditWebsite()
+        if(goEditWebsite){
+          setTabIndex('website')
+        }else{
+          Taro.navigateBack()
+        }
+      }
+      return
+    }
+    const res = await saveAgent()
+    if(res){
+      Taro.showToast({title: '保存成功',icon: 'success'})
     }
-
-    // await saveAgent()
   }
 
   const handleNavBack = async (): Promise<boolean | void> => {
-    confirmEditWebsite()
-    return false
+
     // 创建 智能体没有名字时拦截弹窗
     if(!agentEdit?.agentId && !agentEdit?.name){
       const result = await confirmNavBack()
@@ -113,6 +124,10 @@ export default function Index() {
     }
   })
 
+  useEffect(()=> {
+    fetchAgents()
+  }, [])
+
   return (
     <PageCustom>
       <NavBarNormal  onNavBack={handleNavBack}>编辑智能体</NavBarNormal>
@@ -131,10 +146,10 @@ export default function Index() {
           </View>
         </View>
         <View className="pt-52">
-          <View className={`${tabIndex === "1" ? "block" : "hidden"}`}>
+          <View className={`${tabIndex === "agent" ? "block" : "hidden"}`}>
             <AgentSetting ref={agentSettingRef} save={handleSave}></AgentSetting>
           </View>
-          <View className={`${tabIndex === "2" ? "block" : "hidden"}`}>
+          <View className={`${tabIndex === "website" ? "block" : "hidden"}`}>
             <View className="pt-36 pb-80">
               <AgentWebsite></AgentWebsite>
             </View>

+ 3 - 20
src/pages/editor-contact/index.tsx

@@ -9,28 +9,13 @@ import TagCertificated from "@/components/tag-certificated";
 import Taro, { useRouter } from "@tarojs/taro";
 import { useAgentStore } from "@/store/agentStore";
 import { TAgentDetail } from "@/types/agent";
-import  PickerSingleColumn from "@/components/Picker/PickerSingleColumn";
-import useEditContactCard from "@/hooks/useEditContactCard";
 import { useState } from "react";
+
 const RenderEntCard = (
   agent: TAgentDetail | null,
   navToUrl: (url: string) => void
 ) => {
   const agentContactCard = useAgentStore((state)=> state.agentContactCard)
-  // const { submit } = useEditContactCard('position', agentContactCard?.position)
-  // // 当前选中的值
-  // const options = ['销售人员', '客服与售后支持', '市场与商务合作人员', '新员工 / 培训岗位', '管理者 / 内容运营者']
-  // // 是否显示选择器
-  // const [showPicker, setShowPicker] = useState(false)
-
-  // // 当前选中的值
-  // const [selected, setSelected] = useState(options[0])
-
-  // const handlePicked = (value: string) => {
-  //   setSelected(value)
-  //   submit(value, false)
-  // }
-
 
   if (!agent?.isEnt) {
     return <View className="px-16 w-full pt-12">
@@ -55,13 +40,11 @@ const RenderEntCard = (
                   <View className="flex-1 font-normal">职位</View>
                   <View className="text-gray-5 mr-8">{agent?.position}</View>
                 </View>
-              {/* <PickerSingleColumn headerTitle="您的岗位" options={options} selected={selected} onPicked={handlePicked} showPicker={showPicker} setShowPicker={setShowPicker}>
-
-              </PickerSingleColumn> */}
             </ListRow>
         </ListWrapper>
       </View>;
   }
+
   return (
     <View className="px-16 w-full pt-12">
       <ListWrapper>
@@ -195,7 +178,7 @@ export default function Index() {
         </ListWrapper>
       </View>
 
-      {RenderEntCard(agent, navToUrl)}
+      {/* {RenderEntCard(agent, navToUrl)} */}
     </PageCustom>
   );
 }

+ 1 - 0
src/pages/voice/components/VoiceList/index.tsx

@@ -59,6 +59,7 @@ export default function Index({
                       }}>
                         <View className="text-primary px-12 text-12" onClick={(e)=> handleTry(e, item)}>试听</View>
                         <WemetaRadio
+                          checkbox
                           checked={isEqualToClonedVoice}
                         ></WemetaRadio>
                       </View>

+ 1 - 8
src/service/agent.ts

@@ -25,7 +25,7 @@ export const createNewAgent = (data: Omit<TAgentRequiredData, 'agentId'>) => {
 }
 // 编辑智能体
 export const editAgent = (data: TAgentRequiredData) => {
-  return request.put<TAgentDetail>(`${bluebookAiAgent}api/v1/my/agent/${data.agentId}`, data)
+  return request.put(`${bluebookAiAgent}api/v1/my/agent/${data.agentId}`, data)
 }
 
 // 设置当前我的默认智能体
@@ -114,13 +114,6 @@ export const getEntAgentPartners = (params: {
 //   })
 // }
 
-// 编辑智能体--是否启用背景聊天效果
-// deprecated
-export const editAgentChatBg = (agentId: string, enabledChatBg: boolean) => {
-  return request.put(`${bluebookAiAgent}api/v1/my/agent/${agentId}/avatar/chatBg`, {
-    enabledChatBg,
-  })
-}
 
 // 编辑智能体--名片部份内容
 // address (string, optional): 地址,长度最多250 ,

+ 55 - 44
src/store/agentStore.ts

@@ -8,7 +8,7 @@ import {
   editAgentCard as _editAgentCard,
   editAgentCharacter as _editAgentCharacter,
   editAgentWebsite as _editAgentWebsite,
-  createNewAgent as _createNewAgent,
+  createNewAgent,
   editAgent as _editAgent,
   getNewAgentInfo,
 } from "@/service/agent";
@@ -59,15 +59,17 @@ export interface AgentStoreState {
     deleteAgent: (agentId: string) => Promise<void>;
     updateEditAgent: (updates: Partial<TAgentDetail>) => void;
     saveAgent: (agentFields?: Partial<TAgentDetail>) => Promise<Partial<TAgentDetail> | null>;
+    createAgent: (agentFields?: Partial<TAgentDetail>) => Promise<Partial<TAgentDetail> | null>;
     resetData: () => void;
     clearEditAgent: () => void;
-    setCurrentEditAvatar: (avatar: TAvatarItem|null) => void;
+    setCurrentEditAvatar: (avatar: TAvatarItem | null) => void;
   };
 }
 
 export const useAgentStore = create<AgentStoreState>((set, get) => ({
   agents: [],
   agent: null,
+  // agentEdit 编辑时临时存储的智能体信息, 如果直接用 agent 存储,则切页面时会有空白数据的瞬间
   agentEdit: null,
   agentProfile: null,
   agentContactCard: null,
@@ -106,6 +108,7 @@ export const useAgentStore = create<AgentStoreState>((set, get) => ({
         const agent = response.data;
         set({
           agent: agent,
+          agentEdit: agent,
         });
 
         return response.data;
@@ -151,7 +154,7 @@ export const useAgentStore = create<AgentStoreState>((set, get) => ({
       }
       return null;
     },
-    saveAgent: async (agentFields = {} ) => {
+    saveAgent: async (agentFields = {}) => {
       const { agentEdit } = get();
 
       // 如果当前agent有agentId,则执行编辑操作
@@ -161,6 +164,7 @@ export const useAgentStore = create<AgentStoreState>((set, get) => ({
           ...agentEdit,
           ...agentFields,
         };
+        console.log(mixedAgent,3333333)
         const cleanAgent = {
           ...mixedAgent,
           address: mixedAgent.address ?? undefined,
@@ -183,56 +187,63 @@ export const useAgentStore = create<AgentStoreState>((set, get) => ({
         const response = await _editAgent(updateData);
         const result = isSuccess(response.status);
 
-        if (result && response.data) {
-          // 更新当前agent数据
-          set({ agent: response.data });
+        if (result) {
+          // 重新获取 agent 详情
+          const response = await get().actions.fetchAgent(updateData.agentId)
           // 重新获取agents列表以更新缓存
           await get().actions.fetchAgents();
-          return response.data;
+          return response;
         }
         return null;
-      } else {
-        // 如果没有agentId,则创建新智能体
-        if (!agentEdit) {
-          return null;
-        }
+      }
+      return null
+    },
+    createAgent: async (agentFields = {}) => {
+      const { agentEdit } = get();
+      if (!agentEdit) {
+        return null;
+      }
+      const mixedAgent = {
+        ...agentFields,
+        ...agentEdit,
+      }
 
-        const createData: Omit<TAgentRequiredData, 'agentId'> = {
-          address: agentEdit.address ?? undefined,
-          avatarLogo: agentEdit.avatarLogo ?? undefined,
-          avatarUrl: agentEdit.avatarUrl ?? undefined,
-          email: agentEdit.email ?? undefined,
-          enabledChatBg: agentEdit.enabledChatBg ?? undefined,
-          enabledPersonalKb: agentEdit.enabledPersonalKb ?? undefined,
-          entId: agentEdit.entId ? Number(agentEdit.entId) : undefined,
-          entName: agentEdit.entName ?? undefined,
-          greeting: agentEdit.greeting ?? undefined,
-          mobile: agentEdit.mobile ?? undefined,
-          name: agentEdit.name ?? undefined,
-          personality: agentEdit.personality ?? undefined,
-          position: agentEdit.position ?? undefined,
-          qrCodeUrl: agentEdit.qrCodeUrl ?? undefined,
-          questionGuides: agentEdit.questionGuides ?? undefined,
-          voiceId: agentEdit.voiceId ?? undefined,
-          voiceName: agentEdit.voiceName ?? undefined,
-        };
+      const createData: Omit<TAgentRequiredData, 'agentId'> = {
+        address: mixedAgent.address ?? undefined,
+        avatarLogo: mixedAgent.avatarLogo ?? undefined,
+        avatarUrl: mixedAgent.avatarUrl ?? undefined,
+        email: mixedAgent.email ?? undefined,
+        enabledChatBg: mixedAgent.enabledChatBg ?? false,
+        enabledPersonalKb: mixedAgent.enabledPersonalKb ?? true,
+        entId: mixedAgent.entId ? Number(mixedAgent.entId) : undefined,
+        entName: mixedAgent.entName ?? undefined,
+        greeting: mixedAgent.greeting ?? undefined,
+        mobile: mixedAgent.mobile ?? undefined,
+        name: mixedAgent.name ?? undefined,
+        personality: mixedAgent.personality ?? undefined,
+        position: mixedAgent.position ?? undefined,
+        qrCodeUrl: mixedAgent.qrCodeUrl ?? undefined,
+        questionGuides: mixedAgent.questionGuides ?? undefined,
+        voiceId: mixedAgent.voiceId ?? undefined,
+        voiceName: mixedAgent.voiceName ?? undefined,
+      };
 
-        const response = await _createNewAgent(createData);
-        const result = isSuccess(response.status);
+      const response = await createNewAgent(createData);
+      const result = isSuccess(response.status);
 
-        if (result && response.data) {
-          const newAgent = await get().actions.fetchAgent(response.data.agentId);
-          if (newAgent) {
-            set((state) => ({
-              agents: [...state.agents, newAgent as TAgent],
-              agent: newAgent,
-              defaultAgent: newAgent,
-            }));
-            return newAgent;
-          }
+      if (result) {
+        // 创建成功后立即设置为当前默认智能体
+        const newAgent = await get().actions.setDefaultAgent(response.data.agentId);
+        if (newAgent) {
+          set((state) => ({
+            agents: [...state.agents, newAgent as TAgent],
+            agent: newAgent,
+            agentEdit: newAgent, // 创建成功后,将新智能体数据回填至 agentEdit
+          }));
+          return newAgent;
         }
-        return null;
       }
+      return null;
     },
     // 更新 agent 字段
     updateEditAgent: async (updates: Partial<TAgentDetail>) => {

+ 33 - 32
src/utils/index.ts

@@ -7,6 +7,7 @@ import {
 import {LOGIN_ID_STORAGE_KEY } from '@/xiaolanbenlib/constant'
 
 import Taro, { FileSystemManager } from "@tarojs/taro";
+import { error } from "console";
 import { useRef, useCallback, useEffect } from "react";
 
 const appTokenKey = `${APP_NAME}+${APP_VERSION}token`;
@@ -282,8 +283,8 @@ export const getCanvasTempPath = (
   });
 };
 
-export const savePicture = async (tmpPath: string): Promise<boolean> => {
-  return new Promise((resove, reject) => {
+export const saveMediaFile = async (tmpPath: string, isVideo: boolean = false): Promise<boolean> => {
+  return new Promise((resove) => {
     const params = {
       filePath: tmpPath,
       success() {
@@ -291,11 +292,11 @@ export const savePicture = async (tmpPath: string): Promise<boolean> => {
         resove(true);
       },
       fail(error: any) {
-        console.log(error);
-        reject(false);
+        console.log("save fail", error);
+        resove(false);
       },
     };
-    Taro.saveImageToPhotosAlbum(params);
+    isVideo ? Taro.saveVideoToPhotosAlbum(params) : Taro.saveImageToPhotosAlbum(params);
   });
 };
 
@@ -368,14 +369,14 @@ export function generateUUID(): string {
 
 export const pickNonEmpty = <T extends object>(obj: T): Partial<T> => {
   return Object.fromEntries(
-    Object.entries(obj).filter(([_, value]) => 
+    Object.entries(obj).filter(([_, value]) =>
       value !== "" && value != null
     )
   ) as Partial<T>;
 };
 
 export const getLoginId = () => {
-  return Taro.getStorageSync(LOGIN_ID_STORAGE_KEY) // 打开小程序时创建 login_uuid 
+  return Taro.getStorageSync(LOGIN_ID_STORAGE_KEY) // 打开小程序时创建 login_uuid
 }
 
 export const isSuccess = (status: number)=> {
@@ -408,33 +409,33 @@ export interface FileSizeOptions {
  * @returns 转换后的文件大小字符串或数字
  */
 export function convertFileSize(size: number, options: FileSizeOptions = {}): string | number {
-  const { 
-    precision = 2, 
-    binary = true, 
-    unit, 
-    withUnit = true 
+  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;
@@ -442,39 +443,39 @@ export function convertFileSize(size: number, options: FileSizeOptions = {}): st
       // 如果目标是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;
 }
-// 
+//
 export const pxToRpx = (px: number) => {
   const systemInfo = Taro.getSystemInfoSync()
   return (750 / systemInfo.windowWidth) * px
@@ -502,10 +503,10 @@ export function addOssProcessLowQualityParam(url: string): string {
 
   // 分离URL和查询参数
   const [baseUrl, existingQuery] = url.split('?');
-  
+
   // 构建新的查询参数
   const ossParam = 'x-oss-process=image/quality,Q_60/format,jpg';
-  const newQuery = existingQuery 
+  const newQuery = existingQuery
     ? `${existingQuery}&${ossParam}`  // 已有查询参数,追加OSS参数
     : ossParam;                       // 没有查询参数,直接使用OSS参数
 
@@ -518,13 +519,13 @@ export function addOssProcessLowQualityParam(url: string): string {
 // const exampleUrl2 = "https://example.com/image.png?width=200&height=200";
 // const exampleUrl3 = "https://example.com/image.webp?x-oss-process=image/resize,w_300";
 
-// console.log(addOssProcessParam(exampleUrl1)); 
+// console.log(addOssProcessParam(exampleUrl1));
 // // 输出: https://example.com/image.jpg?x-oss-process=image/quality,Q_60/format,jpg
 
-// console.log(addOssProcessParam(exampleUrl2)); 
+// console.log(addOssProcessParam(exampleUrl2));
 // // 输出: https://example.com/image.png?width=200&height=200&x-oss-process=image/quality,Q_60/format,jpg
 
-// console.log(addOssProcessParam(exampleUrl3)); 
+// console.log(addOssProcessParam(exampleUrl3));
 // // 输出: https://example.com/image.webp?x-oss-process=image/resize,w_300 (保持不变,因为已包含OSS参数)
 
 
@@ -537,7 +538,7 @@ export function restrictedPage() {
       withShareTicket: false,
       menus: []  // 不提供任何分享菜单
     })
-    
+
     // 禁止分享到朋友圈
     Taro.hideShareMenu({
       menus: ['shareAppMessage', 'shareTimeline']

+ 44 - 17
src/utils/upload.ts

@@ -8,8 +8,8 @@ const FILE_LIMIT = 100 * 1024 * 1024 // 100MB
 export async function pickAndUploadImage(
   sourceType: Array<'album' | 'camera'> = ['album', 'camera'],
   scene: EUploadFileScene,
-  maxSize?: {maxWidth: number, maxHeight: number}
-): Promise<{ url: string; size: number }| null> {
+  maxSize?: { maxWidth: number, maxHeight: number }
+): Promise<{ url: string; size: number } | null> {
   try {
     const res = await Taro.chooseImage({
       count: 1,
@@ -18,7 +18,7 @@ export async function pickAndUploadImage(
     })
 
     const file = res.tempFiles[0]
-    
+
     if (file.size >= IMAGE_LIMIT) {
       Taro.showToast({
         title: '图片大小超限',
@@ -28,19 +28,19 @@ export async function pickAndUploadImage(
       return null
     }
 
-    if(maxSize){
-      const {width, height} = await new Promise<{width: number, height: number}>((resolve) => {
+    if (maxSize) {
+      const { width, height } = await new Promise<{ width: number, height: number }>((resolve) => {
         Taro.getImageInfo({
           src: file.path,
           success(result) {
-            resolve({width: result.width, height: result.height})
+            resolve({ width: result.width, height: result.height })
           },
           fail(res) {
-            resolve({width: 0, height: 0})
+            resolve({ width: 0, height: 0 })
           },
         })
       })
-      if(width > maxSize.maxWidth || height > maxSize.maxHeight){
+      if (width > maxSize.maxWidth || height > maxSize.maxHeight) {
         Taro.showToast({
           title: '图片宽高超限',
           icon: 'error',
@@ -49,13 +49,13 @@ export async function pickAndUploadImage(
         return null
       }
     }
-    
+
     const result = await uploadFile(
       file.path,
       scene,
     );
-    
-    if(result && ('publicUrl' in result)){
+
+    if (result && ('publicUrl' in result)) {
       Taro.showToast({
         title: '上传成功',
         icon: 'success',
@@ -76,7 +76,7 @@ export const pickAndUploadImageWithCrop = async (
   sourceType: Array<'album' | 'camera'> = ['album', 'camera'],
   cropScale?: keyof Taro.cropImage.CropScale,
   scene?: EUploadFileScene,
-): Promise<{ url: string; size: number }| null> => {
+): Promise<{ url: string; size: number } | null> => {
 
   const res = await Taro.chooseImage({
     count: 1,
@@ -85,7 +85,7 @@ export const pickAndUploadImageWithCrop = async (
   })
 
   const file = res.tempFiles[0]
-  return new Promise((resolve)=> {
+  return new Promise((resolve) => {
     Taro.cropImage({
       src: file.path,
       cropScale: cropScale ?? "1:1",
@@ -97,8 +97,8 @@ export const pickAndUploadImageWithCrop = async (
           path,
           _scene,
         );
-        
-        if(result && ('publicUrl' in result)){
+
+        if (result && ('publicUrl' in result)) {
           Taro.showToast({
             title: '上传成功',
             icon: 'success',
@@ -112,9 +112,36 @@ export const pickAndUploadImageWithCrop = async (
         }
         resolve(null)
       },
-      fail(){
+      fail() {
         resolve(null)
       }
     });
   })
-}
+}
+
+export const cropImage = (filePath: string, cropScale?: keyof Taro.cropImage.CropScale,): Promise<{ url: string } | null> => {
+  return new Promise((resolve) => {
+    Taro.cropImage({
+      src: filePath,
+      cropScale: cropScale ?? "1:1",
+      success: async (cropRes) => {
+        const path = cropRes.tempFilePath;
+        const _scene = EUploadFileScene.OTHER
+        const result = await uploadFile(
+          path,
+          _scene,
+        );
+        if (result && ('publicUrl' in result)) {
+          resolve({
+            url: result.publicUrl,
+          })
+          return
+        }
+        resolve(null)
+      },
+      fail() {
+        resolve(null)
+      },
+    });
+  })
+}