index.tsx 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. import EmptyData from "@/components/empty-data";
  2. import CardListItem from "@/components/list/card-list-item/index";
  3. import CardList from "@/components/list/card-list/index";
  4. import { IVoicePlayerBar } from "@/components/voice-player-bar/index";
  5. import WemetaRadio from "@/components/wemeta-radio/index";
  6. import { CloneVoiceStatus } from "@/consts/enum";
  7. import IconWave from "@/images/icon-wave-20.png";
  8. import { voiceCloneConfirm } from "@/service/character";
  9. import { useAppStore } from "@/store/appStore";
  10. import { useCharacterStore } from "@/store/characterStore";
  11. import { ICharacter, TEntityVoiceCloneRecord } from "@/types";
  12. import { formatDateFull, getCloneVoiceIdentifier } from "@/utils/index";
  13. import { Image, View } from "@tarojs/components";
  14. import { useEffect, useRef, useState } from "react";
  15. import PopupContainer from "./components/popup-container";
  16. import PopupRecorder, { ECloneStatus } from "./components/popup-recorder/index";
  17. import PopupTryout from "./components/popup-tryout/index";
  18. import style from "./index.module.less";
  19. interface Props {
  20. value?: ICharacter;
  21. setValue?: (value: ICharacter) => void;
  22. profileId: string;
  23. onPlay?: (voice: any) => void;
  24. }
  25. export default ({ profileId, onPlay }: Props) => {
  26. const playerRef = useRef<IVoicePlayerBar>(null);
  27. const intervalRef = useRef<NodeJS.Timeout | null>(null);
  28. const [show, setShow] = useState(false);
  29. const [popupType, setPopupType] = useState<"clone" | "try" | "reclone">(
  30. "clone",
  31. );
  32. const [voiceName, setVoiceName] = useState("");
  33. const [voiceIndex, setVoiceIndex] = useState(-1);
  34. const { saveCharacter, fetchCharacter, fetchVoiceCloneHistory } =
  35. useCharacterStore();
  36. const character = useCharacterStore((state) => state.character);
  37. const voiceList = useCharacterStore((state) => state.voiceList);
  38. const appConfig = useAppStore((state) => state.appConfig);
  39. const createVoiceNameText = (voiceName: string) => {
  40. const i = voiceList.findIndex((item) => item.voiceName === voiceName);
  41. console.log(voiceName, i);
  42. if (i === -1) return "";
  43. return `克隆声音 0${i + 1}`;
  44. };
  45. // 克隆列表操作
  46. const renderRightColumn = (item?: TEntityVoiceCloneRecord) => {
  47. if (item?.status == CloneVoiceStatus.CloneVoiceStatusExpired) {
  48. return <WemetaRadio disabled></WemetaRadio>;
  49. }
  50. if (item?.status == CloneVoiceStatus.CloneVoiceStatusSuccess) {
  51. const notSystemVoice = (character?.voice !== character?.defaultSystemVoice)
  52. const isSameVoice = (item.voiceName === character?.voice)
  53. return (
  54. <WemetaRadio
  55. checked={ isSameVoice && notSystemVoice}
  56. ></WemetaRadio>
  57. );
  58. }
  59. if (item?.status == CloneVoiceStatus.CloneVoiceStatusUnconfirmed) {
  60. return (
  61. <View className="text-14 bg-primary text-black leading-22 px-12 py-6 rounded-28 active:pressed-button">
  62. 试听
  63. </View>
  64. );
  65. }
  66. return <></>;
  67. };
  68. // 克隆状态
  69. const renderCloneStatus = (item: TEntityVoiceCloneRecord) => {
  70. if (item.status === CloneVoiceStatus.CloneVoiceStatusSuccess) {
  71. return (
  72. <View className={`text-12 leading-20 text-gray-45`}>
  73. {item.createdAt && formatDateFull(new Date(item.createdAt))}
  74. </View>
  75. );
  76. }
  77. if (item.status === CloneVoiceStatus.CloneVoiceStatusUnconfirmed) {
  78. return <View className={`text-12 leading-20 text-orange`}>试听确认</View>;
  79. }
  80. if (item.status == CloneVoiceStatus.CloneVoiceStatusExpired) {
  81. return <View className={`text-12 leading-20 text-orange`}>已过期</View>;
  82. }
  83. return <View>当前时间</View>;
  84. };
  85. const renderCloneList = () => {
  86. if (!voiceList.length) {
  87. return <EmptyData></EmptyData>;
  88. }
  89. return (
  90. <CardList>
  91. {voiceList.map((item, _index) => {
  92. return (
  93. <CardListItem
  94. rightRenderer={() => {
  95. return renderRightColumn(item);
  96. }}
  97. onClick={() => handleSelect(item, _index)}
  98. >
  99. <View className="flex items-center gap-16">
  100. <View className={style.listIcon}>
  101. <Image src={IconWave} className={style.iconImage}></Image>
  102. </View>
  103. <View className="flex flex-col gap-4">
  104. <View className={style.ListItemText}>
  105. {/* deprecated getCloneVoiceIdentifier */}
  106. {item.voiceAlias ?? getCloneVoiceIdentifier(_index + 1)}
  107. </View>
  108. {renderCloneStatus(item)}
  109. </View>
  110. </View>
  111. </CardListItem>
  112. );
  113. })}
  114. </CardList>
  115. );
  116. };
  117. const handleSelect = (item: TEntityVoiceCloneRecord, index: number) => {
  118. setVoiceIndex(index);
  119. if (item.status == CloneVoiceStatus.CloneVoiceStatusSuccess) {
  120. if (item.voiceName) {
  121. setVoiceName(item.voiceName);
  122. onPlay &&
  123. onPlay({
  124. voiceName: item.voiceName,
  125. voiceAlias: item.voiceAlias,
  126. voiceIndex: index,
  127. });
  128. }
  129. saveCharacter({
  130. profileId: profileId,
  131. voice: item.voiceName,
  132. });
  133. }
  134. if (item.status == CloneVoiceStatus.CloneVoiceStatusUnconfirmed) {
  135. // 未确认,弹出试听框
  136. setPopupType("try");
  137. setShow(true);
  138. }
  139. };
  140. // 克隆按钮状态
  141. const handleCloneStatus = (status: ECloneStatus) => {
  142. console.log(status);
  143. };
  144. const fetchVoiceList = async () => {
  145. if (profileId) {
  146. const r = await fetchVoiceCloneHistory(profileId);
  147. const result = r.find((item) => item.status === "pending");
  148. if (result) {
  149. intervalRef.current = setTimeout(() => fetchVoiceList(), 3000);
  150. } else {
  151. stopTimer();
  152. }
  153. }
  154. };
  155. const handleSureAction = async () => {
  156. setShow(false);
  157. await voiceCloneConfirm(voiceList[voiceIndex].voiceName!);
  158. fetchVoiceList();
  159. };
  160. const handleRecloneAction = () => {
  161. setPopupType("reclone");
  162. setShow(true);
  163. };
  164. // 声音录制完成
  165. const onRecordEnd = (r: string) => {
  166. console.log("onRecordEnd:", r);
  167. fetchVoiceList();
  168. };
  169. const stopTimer = () => {
  170. if (intervalRef.current !== null) {
  171. clearTimeout(intervalRef.current);
  172. intervalRef.current = null;
  173. }
  174. };
  175. const calcRemainCloneNum = () => {
  176. if (appConfig?.maxCloneNum) {
  177. const clonedNum = voiceList.filter(
  178. (item) =>
  179. item.status === CloneVoiceStatus.CloneVoiceStatusSuccess ||
  180. item.status === CloneVoiceStatus.CloneVoiceStatusUnconfirmed,
  181. ).length;
  182. const remainNum = appConfig.maxCloneNum - clonedNum;
  183. return remainNum < 0 ? 0 : remainNum;
  184. }
  185. return voiceList.length;
  186. };
  187. const initPage = async (profileId: string) => {
  188. await fetchCharacter(profileId);
  189. fetchVoiceCloneHistory(profileId);
  190. character?.voice && setVoiceName(character.voice);
  191. };
  192. useEffect(() => {
  193. profileId && initPage(profileId);
  194. }, [profileId]);
  195. // 清除定时器
  196. useEffect(() => {
  197. return () => {
  198. stopTimer();
  199. };
  200. }, []);
  201. return (
  202. <View className={`flex flex-col`}>
  203. <View className="flex-1">
  204. {/* <VoicePlayerBar
  205. ref={playerRef}
  206. voiceName={voiceName}
  207. voiceNameText={createVoiceNameText(voiceName)}
  208. /> */}
  209. {renderCloneList()}
  210. </View>
  211. <PopupContainer show={show} setShow={setShow}>
  212. {(popupType == "clone" || popupType == "reclone") && (
  213. <PopupRecorder
  214. onRecordEnd={onRecordEnd}
  215. show={show}
  216. setShow={setShow}
  217. setCloneStatus={(status) => handleCloneStatus(status)}
  218. voiceName={
  219. popupType == "reclone" ? voiceList[voiceIndex].voiceName! : ""
  220. }
  221. ></PopupRecorder>
  222. )}
  223. {popupType == "try" && (
  224. <PopupTryout
  225. show={show}
  226. onSure={handleSureAction}
  227. onReclone={handleRecloneAction}
  228. voiceName={voiceList[voiceIndex].voiceName!}
  229. showName={getCloneVoiceIdentifier(voiceIndex + 1)}
  230. ></PopupTryout>
  231. )}
  232. </PopupContainer>
  233. <View
  234. className={style.addButton}
  235. onClick={() => {
  236. setPopupType("clone");
  237. setShow(true);
  238. }}
  239. >
  240. <View className="button-rounded-big font-medium">
  241. <View>添加克隆声音</View>
  242. <View className="font-normal text-12 leading-0">
  243. (剩{calcRemainCloneNum()}次)
  244. </View>
  245. </View>
  246. </View>
  247. </View>
  248. );
  249. };