index.tsx 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  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. saveMedia(res.tempFilePath)
  114. }
  115. },
  116. fail: () => {
  117. Taro.hideLoading();
  118. },
  119. })
  120. };
  121. useDidShow(() => {
  122. mutate()
  123. })
  124. useUnload(()=> {
  125. Taro.hideLoading()
  126. })
  127. const renderMedia = (avatar: TAvatarItem, index: number) => {
  128. if (avatar.isVideo) {
  129. return <>
  130. <View className={style.videoContainer} onClick={() => handlePreview(index)}>
  131. <Video
  132. controls={false}
  133. showCenterPlayBtn={false}
  134. loop={true}
  135. muted={true}
  136. objectFit="cover"
  137. src={avatar.avatarUrl}
  138. className="w-full h-full"
  139. />
  140. <View className={style.blurBg}></View>
  141. <View className={style.durationStatus}>
  142. <IconPlayWhite24 />
  143. <View className="text-10">{formatSeconds(Math.round(avatar.videoSeconds))}</View>
  144. </View>
  145. </View>
  146. </>
  147. }
  148. return <Image src={`${avatar.avatarUrl}?x-oss-process=image/quality,Q_60/format,jpg`} mode="widthFix" className="w-full" onClick={() => handlePreview(index)} />;
  149. };
  150. const renderList = () => {
  151. if (!list.length) {
  152. return (
  153. <>
  154. {/* <EmptyData type={"search"}></EmptyData> */}
  155. </>
  156. );
  157. }
  158. return list.map((avatar, index) => {
  159. const currentUsed = agent?.avatarUrl === avatar.avatarUrl;
  160. const isCurrentSelected = current?.avatarId === avatar.avatarId;
  161. return (
  162. <View
  163. className={`${currentUsed ? style.gridItemActived : style.gridItem}`}
  164. >
  165. <View
  166. className={style.selected}
  167. onClick={(e) => handleSelect(e, avatar)}
  168. >
  169. <WemetaRadio theme="light" checkbox checked={isCurrentSelected} />
  170. </View>
  171. {currentUsed && <View className={style.gridItemCurrentUsedMark}></View>}
  172. {/* <View className={style.gridItemCurrentUsedMark}></View> */}
  173. {!avatar.isOriginal && <View className={style.aiTips}>图片由AI生成</View>}
  174. {renderMedia(avatar, index)}
  175. </View>
  176. );
  177. });
  178. };
  179. return (
  180. <PageCustom>
  181. <NavBarNormal>历史形象</NavBarNormal>
  182. <View className={style.container}>
  183. <ScrollView
  184. scrollY
  185. onScrollToLower={onScrollToLower}
  186. scrollTop={scrollTop}
  187. onScroll={(e) => {
  188. scrollPositionRef.current = e.detail.scrollTop;
  189. }}
  190. style={{
  191. flex: 1,
  192. height: "100%", // 高度自适应
  193. }}
  194. >
  195. <View className="w-full p-16 pb-120">
  196. <View className={style.grid}>
  197. <View className={style.gridItemCreateBtn} onClick={handleCreate}>
  198. <View className={style.icon}>
  199. <IconPlusBig></IconPlusBig>
  200. </View>
  201. <View className="pt-8 text-12 leading-20 text-gray-4">
  202. 创建新形象
  203. </View>
  204. </View>
  205. {renderList()}
  206. </View>
  207. </View>
  208. </ScrollView>
  209. <BottomBar className="pt-12 px-16">
  210. <WemetaButton type='danger' className="w-88 text-red" disabled={!current?.canDel} onClick={handleDelete}>删除</WemetaButton>
  211. <WemetaButton type='normal' className="w-102" disabled={!current} onClick={handleDownload}>下载</WemetaButton>
  212. <WemetaButton type="primary" className="flex-1" disabled={!current} onClick={handleSetBackground}>设置为聊天背景</WemetaButton>
  213. </BottomBar>
  214. </View>
  215. </PageCustom>
  216. );
  217. }