index.tsx 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  1. import EmptyData from "@/components/empty-data";
  2. import CardListItem from "@/components/list/card-list-item/index";
  3. import WemetaRadio from "@/components/WemetaRadio/index";
  4. import IconWave from "@/images/icon-wave-20.png";
  5. import { Image, View } from "@tarojs/components";
  6. import { useEffect, useRef, useState } from "react";
  7. import Popup from "@/components/popup/popup";
  8. import PopupRecorder, { ECloneStatus } from "./components/popup-recorder/index";
  9. import style from "./index.module.less";
  10. import { useVoiceStore } from "@/store/voiceStore";
  11. import { EVoiceStatus, TVoiceItem } from "@/types/voice";
  12. import { TAgentDetail } from "@/types/agent";
  13. import { deleteVoice, getVoiceStatus } from "@/service/voice";
  14. import ThinkingAnimation from "@/components/think-animation";
  15. import { useModalStore } from "@/store/modalStore";
  16. import { isSuccess } from "@/utils";
  17. interface Props {
  18. agent: TAgentDetail | null;
  19. onPlay?: (voice: any) => void;
  20. }
  21. type TTask = {
  22. message: string;
  23. status: "processing" | "success" | "process_fail";
  24. taskId: string;
  25. };
  26. export default ({ onPlay, agent }: Props) => {
  27. const intervalRef = useRef<NodeJS.Timeout | null>(null);
  28. const [show, setShow] = useState(false);
  29. const {showModal} = useModalStore()
  30. const { getVoices } = useVoiceStore();
  31. const myVoices = useVoiceStore((state) => state.myVoices);
  32. const voices = useVoiceStore((state) => state.voices);
  33. // {
  34. // message: '生成中',
  35. // taskId: 'abc',
  36. // status: 'processing'
  37. // }
  38. const [cloning, setCloning] = useState<TTask>();
  39. //获取一个默认声音
  40. const getDefaultVoice = ()=> {
  41. // 优先使用我的声音
  42. if(myVoices.length){
  43. return myVoices[0]
  44. }
  45. // 其次使用系统声音
  46. const systemVoices = voices.filter((item)=> item.isSystem)
  47. if(systemVoices.length){
  48. return systemVoices[0]
  49. }
  50. return null
  51. }
  52. // 检查需要不要改变默认声音
  53. const syncDefaultVoice = (item: TVoiceItem)=> {
  54. // 说明删除的声音是当前使用的声音
  55. if(agent?.voiceId === item.voiceId){
  56. // 需要默认找一个声音
  57. const defaultVoice = getDefaultVoice()
  58. if(defaultVoice){
  59. handleSelect(defaultVoice)
  60. }
  61. }
  62. }
  63. const handleSelect = (item: TVoiceItem) => {
  64. if (item.status == EVoiceStatus.DONE) {
  65. if (item.voiceName) {
  66. onPlay && onPlay(item);
  67. }
  68. }
  69. };
  70. const handleLongPress = (item: TVoiceItem) => {
  71. if(voices.length <= 1){
  72. return;
  73. }
  74. showModal({
  75. content: '确认删除该声音?',
  76. onConfirm: async () => {
  77. const response = await deleteVoice(item.voiceId)
  78. if(isSuccess(response.status)){
  79. await getVoices();
  80. syncDefaultVoice(item);
  81. }
  82. }
  83. })
  84. };
  85. // 克隆按钮状态
  86. const handleCloneStatus = (status: ECloneStatus) => {
  87. console.log(status);
  88. };
  89. const fetchVoiceList = async (taskId: string) => {
  90. const response = await getVoiceStatus(taskId);
  91. console.log(response.data);
  92. if (response.data.status === "processing") {
  93. setCloning({
  94. message: response.data.message,
  95. status: response.data.status,
  96. taskId: response.data.taskId,
  97. });
  98. intervalRef.current = setTimeout(() => fetchVoiceList(taskId), 3000);
  99. return;
  100. }
  101. if (
  102. response.data.status === "process_fail" ||
  103. response.data.status === "success"
  104. ) {
  105. setCloning({
  106. message: response.data.message,
  107. status: response.data.status,
  108. taskId: response.data.taskId,
  109. });
  110. stopTimer();
  111. getVoices();
  112. }
  113. };
  114. // 声音录制完成
  115. const onRecordEnd = (taskId: string) => {
  116. console.log("onRecordEnd:", taskId);
  117. fetchVoiceList(taskId);
  118. };
  119. const stopTimer = () => {
  120. if (intervalRef.current !== null) {
  121. clearTimeout(intervalRef.current);
  122. intervalRef.current = null;
  123. }
  124. };
  125. useEffect(() => {
  126. getVoices();
  127. }, []);
  128. // 清除定时器
  129. useEffect(() => {
  130. return () => {
  131. stopTimer();
  132. };
  133. }, []);
  134. // 克隆列表右侧操作栏
  135. const renderRightColumn = (item: TVoiceItem) => {
  136. if (item.status == EVoiceStatus.DONE) {
  137. return (
  138. <View className="flex items-center h-full">
  139. <WemetaRadio checked={item.voiceId === agent?.voiceId}></WemetaRadio>
  140. </View>
  141. );
  142. }
  143. return <></>;
  144. };
  145. // 克隆状态栏
  146. const renderCloneStatus = (item: TTask) => {
  147. if (item.status === "success") {
  148. return (
  149. <View className={`text-12 leading-20 text-green`}>{item.message}</View>
  150. );
  151. }
  152. if (item.status === "processing") {
  153. return (
  154. <View className={`text-12 leading-20 text-primary`}>{item.message}<ThinkingAnimation /></View>
  155. );
  156. }
  157. if (item.status === "process_fail") {
  158. return <View className={`text-12 leading-20 text-orange`}>{item.message}</View>;
  159. }
  160. console.log(item.status)
  161. return <></>;
  162. };
  163. const renderItem = (item) => {
  164. if (item.taskId) {
  165. return <CardListItem
  166. underline
  167. leftRenderer={() => {
  168. return (
  169. <View className={style.listIcon}>
  170. <Image src={IconWave} className={style.iconImage}></Image>
  171. </View>
  172. );
  173. }}
  174. >
  175. <View className="flex items-center h-full ">{renderCloneStatus(item)}</View>
  176. </CardListItem>;
  177. }
  178. return (
  179. <CardListItem
  180. underline
  181. leftRenderer={() => {
  182. return (
  183. <View className={style.listIcon}>
  184. <Image src={IconWave} className={style.iconImage}></Image>
  185. </View>
  186. );
  187. }}
  188. rightRenderer={() => {
  189. return renderRightColumn(item);
  190. }}
  191. onLongPress={()=> handleLongPress(item)}
  192. onClick={() => handleSelect(item)}
  193. >
  194. <View className="flex flex-col gap-4 py-16">
  195. <View className="flex items-center leading-22">{item.voiceName}</View>
  196. <View className="text-12 text-gray-45 leading-20">{item.createTime.slice(0,7)}创建</View>
  197. </View>
  198. </CardListItem>
  199. );
  200. };
  201. // 渲染克隆列表
  202. const renderCloneList = () => {
  203. //
  204. const voices = cloning ? [cloning, ...myVoices] : myVoices;
  205. if (!voices.length) {
  206. return <EmptyData></EmptyData>;
  207. }
  208. return (
  209. <View className="px-16 flex flex-col w-full">
  210. {voices.map((item, _index) => {
  211. return renderItem(item);
  212. })}
  213. </View>
  214. );
  215. };
  216. return (
  217. <View className={`flex flex-col`}>
  218. {renderCloneList()}
  219. <View
  220. className={style.addButton}
  221. onClick={() => {
  222. setShow(true);
  223. }}
  224. >
  225. <View className="button-rounded-big font-medium">
  226. <View>添加克隆声音</View>
  227. </View>
  228. </View>
  229. <Popup show={show} setShow={setShow}>
  230. <PopupRecorder
  231. onRecordEnd={onRecordEnd}
  232. show={show}
  233. setShow={setShow}
  234. setCloneStatus={(status) => handleCloneStatus(status)}
  235. ></PopupRecorder>
  236. </Popup>
  237. </View>
  238. );
  239. };