index.tsx 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. import { useEffect, useRef, useState } from "react";
  2. import { Image, View, ScrollView, Video } from "@tarojs/components";
  3. import { formatSeconds, saveMediaFile } from "@/utils/index";
  4. import PageCustom from "@/components/page-custom/index";
  5. import NavBarNormal from "@/components/NavBarNormal/index";
  6. import { checkPermission, showAuthModal } from "@/utils/auth";
  7. import { uploadAndNavToGenNewAvatar } from "@/utils/avatar";
  8. import IconPlusBig from "@/components/icon/icon-plus-big";
  9. import IconPlayWhite24 from '@/components/icon/IconPlayWhite20'
  10. import { deleteAvatar, fetchMyAvatars } from "@/service/storage";
  11. import style from "./index.module.less";
  12. import { TAvatarItem } from "@/service/storage";
  13. import { useAgentStore, useAgentStoreActions } from "@/store/agentStore";
  14. import Taro, { useDidShow, useUnload } from "@tarojs/taro";
  15. import { isSuccess } from "@/utils";
  16. import WemetaRadio from "@/components/WemetaRadio/index";
  17. import { useModalStore } from "@/store/modalStore";
  18. import { useLoadMoreInfinite, createKey } from "@/utils/loadMoreInfinite";
  19. import BottomBar from "@/components/BottomBar";
  20. import WemetaButton from "@/components/buttons/WemetaButton";
  21. export default function Index() {
  22. const agent = useAgentStore((state) => state.agent);
  23. const { setCurrentEditAvatar } = useAgentStoreActions()
  24. const [scrollTop, setScrollTop] = useState(0);
  25. const scrollPositionRef = useRef(0);
  26. const { showModal } = useModalStore((state) => state.actions);
  27. const fetcher = async ([_url, { pageIndex, pageSize }]) => {
  28. const res = await fetchMyAvatars({ pageIndex: pageIndex, pageSize });
  29. return res.data;
  30. };
  31. const { list, loadMore, mutate } = useLoadMoreInfinite<TAvatarItem[]>(
  32. createKey("fetchMyAvatars"),
  33. fetcher
  34. );
  35. const [current, setCurrent] = useState<TAvatarItem | null>(null);
  36. // 选择形象
  37. const handleSelect = async (e: any, item: TAvatarItem) => {
  38. e.stopPropagation()
  39. console.log(item);
  40. if (current?.avatarId === item.avatarId) {
  41. setCurrent(null)
  42. } else {
  43. setCurrent(item);
  44. }
  45. };
  46. const handleSetBackground = () => {
  47. current && setCurrentEditAvatar(current)
  48. Taro.navigateTo({ url: '/pages/agent-avatar-confirm/index' });
  49. }
  50. const onScrollToLower = () => {
  51. loadMore();
  52. console.log("lower");
  53. };
  54. const handleCreate = () => {
  55. uploadAndNavToGenNewAvatar();
  56. };
  57. const handleDelete = async (e: any) => {
  58. e.stopPropagation()
  59. if (!current || !current.canDel) {
  60. return
  61. }
  62. showModal({
  63. content: <>确认删除该形象?</>,
  64. async onConfirm() {
  65. const response = await deleteAvatar(current.avatarId);
  66. if (isSuccess(response.status)) {
  67. setCurrent(null)
  68. mutate();
  69. }
  70. },
  71. });
  72. };
  73. const handlePreview = (index: number) => {
  74. Taro.previewMedia({
  75. current: index,
  76. //@ts-ignore
  77. sources: list.map((item) => {
  78. return {
  79. url: item.avatarUrl,
  80. type: item.isVideo ? 'video' : 'image',
  81. }
  82. }),
  83. })
  84. }
  85. const saveMedia = async (tmpPath: string) => {
  86. const res = await saveMediaFile(tmpPath, current?.isVideo);
  87. if (res) {
  88. Taro.showToast({
  89. title: '保存成功'
  90. })
  91. return
  92. }
  93. Taro.showToast({
  94. title: '保存失败'
  95. })
  96. }
  97. const handleDownload = async () => {
  98. if (!current?.avatarUrl) {
  99. return
  100. }
  101. // 保存至相册
  102. Taro.showLoading();
  103. const authed = await checkPermission("scope.writePhotosAlbum");
  104. if (!authed) {
  105. Taro.hideLoading();
  106. showAuthModal("需要您相册权限");
  107. return;
  108. }
  109. Taro.downloadFile({
  110. url: current.avatarUrl,
  111. success: (res) => {
  112. if (res.statusCode === 200) {
  113. res.tempFilePath
  114. saveMedia(res.tempFilePath)
  115. }
  116. },
  117. fail: () => {
  118. Taro.hideLoading();
  119. },
  120. })
  121. };
  122. useDidShow(() => {
  123. mutate()
  124. })
  125. useUnload(()=> {
  126. Taro.hideLoading()
  127. })
  128. const renderMedia = (avatar: TAvatarItem, index: number) => {
  129. if (avatar.isVideo) {
  130. return <>
  131. <View className={style.videoContainer} onClick={() => handlePreview(index)}>
  132. <Video
  133. controls={false}
  134. showCenterPlayBtn={false}
  135. loop={true}
  136. muted={true}
  137. objectFit="cover"
  138. src={avatar.avatarUrl}
  139. className="w-full h-full"
  140. />
  141. <View className={style.blurBg}></View>
  142. <View className={style.durationStatus}>
  143. <IconPlayWhite24 />
  144. <View className="text-10">{formatSeconds(Math.round(avatar.videoSeconds))}</View>
  145. </View>
  146. </View>
  147. </>
  148. }
  149. return <Image src={`${avatar.avatarUrl}?x-oss-process=image/quality,Q_60/format,jpg`} mode="widthFix" className="w-full" onClick={() => handlePreview(index)} />;
  150. };
  151. const renderList = () => {
  152. if (!list.length) {
  153. return (
  154. <>
  155. {/* <EmptyData type={"search"}></EmptyData> */}
  156. </>
  157. );
  158. }
  159. return list.map((avatar, index) => {
  160. const currentUsed = agent?.avatarUrl === avatar.avatarUrl;
  161. const isCurrentSelected = current?.avatarId === avatar.avatarId;
  162. return (
  163. <View
  164. className={`${currentUsed ? style.gridItemActived : style.gridItem}`}
  165. >
  166. <View
  167. className={style.selected}
  168. onClick={(e) => handleSelect(e, avatar)}
  169. >
  170. <WemetaRadio theme="light" checkbox checked={isCurrentSelected} />
  171. </View>
  172. {currentUsed && <View className={style.gridItemCurrentUsedMark}></View>}
  173. {/* <View className={style.gridItemCurrentUsedMark}></View> */}
  174. {!avatar.isOriginal && <View className={style.aiTips}>图片由AI生成</View>}
  175. {renderMedia(avatar, index)}
  176. </View>
  177. );
  178. });
  179. };
  180. return (
  181. <PageCustom>
  182. <NavBarNormal>历史形象</NavBarNormal>
  183. <View className={style.container}>
  184. <ScrollView
  185. scrollY
  186. onScrollToLower={onScrollToLower}
  187. scrollTop={scrollTop}
  188. onScroll={(e) => {
  189. scrollPositionRef.current = e.detail.scrollTop;
  190. }}
  191. style={{
  192. flex: 1,
  193. height: "100%", // 高度自适应
  194. }}
  195. >
  196. <View className="w-full p-16 pb-120">
  197. <View className={style.grid}>
  198. <View className={style.gridItemCreateBtn} onClick={handleCreate}>
  199. <View className={style.icon}>
  200. <IconPlusBig></IconPlusBig>
  201. </View>
  202. <View className="pt-8 text-12 leading-20 text-gray-4">
  203. 创建新形象
  204. </View>
  205. </View>
  206. {renderList()}
  207. </View>
  208. </View>
  209. </ScrollView>
  210. <BottomBar className="pt-12 px-16">
  211. <WemetaButton type='danger' className="w-88 text-red" disabled={!current?.canDel} onClick={handleDelete}>删除</WemetaButton>
  212. <WemetaButton type='normal' className="w-102" disabled={!current} onClick={handleDownload}>下载</WemetaButton>
  213. <WemetaButton type="primary" className="flex-1" disabled={!current} onClick={handleSetBackground}>设置为聊天背景</WemetaButton>
  214. </BottomBar>
  215. </View>
  216. </PageCustom>
  217. );
  218. }