pages_live.tsx 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434
  1. import React, { useState, useEffect } from 'react';
  2. // import 'https://g.alicdn.com/apsara-media-box/imp-interaction/1.6.1/alivc-im.iife.js'
  3. // 从 SDK 中提取需要的类型
  4. type ImEngine = InstanceType<typeof AliVCInteraction.ImEngine>;
  5. type ImGroupManager = AliVCInteraction.AliVCIMGroupManager;
  6. type ImMessageManager = AliVCInteraction.AliVCIMMessageManager;
  7. type ImLogLevel = AliVCInteraction.ImLogLevel;
  8. type ImMessageLevel = AliVCInteraction.ImMessageLevel;
  9. interface ImUser {
  10. userId: string;
  11. userExtension?: string;
  12. }
  13. interface ImGroupMessage {
  14. groupId: string;
  15. type: number;
  16. data: string;
  17. sender?: ImUser;
  18. timestamp?: number;
  19. }
  20. interface ImGroupMemberChangeEvent {
  21. groupId: string;
  22. memberCount: number;
  23. joinUsers: ImUser[];
  24. leaveUsers: ImUser[];
  25. }
  26. interface ImGroupMuteChangeEvent {
  27. groupId: string;
  28. status: AliVCInteraction.ImGroupMuteStatus;
  29. }
  30. interface ImGroupInfoChangeEvent {
  31. groupId: string;
  32. info: AliVCInteraction.ImGroupInfoStatus;
  33. }
  34. interface ImError {
  35. code: number;
  36. msg: string;
  37. message?: string;
  38. }
  39. const { ImEngine: ImEngineClass, ImLogLevel, ImMessageLevel } = window.AliVCInteraction;
  40. // 请从控制台复制对应的值填入下面 AppId、 AppKey、AppSign 变量当中
  41. // 注意:这里仅作为本地快速体验使用,实际开发请勿在前端泄露 AppKey
  42. const AppId = '4c2ab5e1b1b0';
  43. const AppKey = '314bb5eee5b623549e8a41574ba3ff32';
  44. const AppSign = 'H4sIAAAAAAAE/wCQAG//zguHB+lYCilkv7diSkk4GmcvLuds+InRu9vFOFebMwm/jEgsK5bBT85Z0owObMxG58uXHyPFlPEBEDQm9FswNJ+KmX0VDYkcfdPPWkafA6Hc0B6F+p5De9yJfPEfHzwo/DHMaygbHfLmBgUtmKveq421sJr/gNBz9D04Ewsg39us+ao0NegzLt7xtXvFXXXJAAAA//8BAAD//yoav6aQAAAA';
  45. export const LivePage = () => {
  46. const [userId, setUserId] = useState<string>('');
  47. const [groupId, setGroupId] = useState<string>('');
  48. const [isLoggedIn, setIsLoggedIn] = useState<boolean>(false);
  49. const [isJoinedGroup, setIsJoinedGroup] = useState<boolean>(false);
  50. const [msgText, setMsgText] = useState<string>('');
  51. const [messageList, setMessageList] = useState<string[]>([]);
  52. const [errorMessage, setErrorMessage] = useState<string>('');
  53. const engine: ImEngine = ImEngineClass.createEngine();
  54. const [groupManager, setGroupManager] = useState<ImGroupManager | null>(null);
  55. const [messageManager, setMessageManager] = useState<ImMessageManager | null>(null);
  56. const [joinedGroupId, setJoinedGroupId] = useState<string | null>(null);
  57. const sha256 = async (message: string): Promise<string> => {
  58. const encoder = new TextEncoder();
  59. const data = encoder.encode(message);
  60. const result = await crypto.subtle.digest('SHA-256', data).then((buffer) => {
  61. let hash = Array.prototype.map.call(new Uint8Array(buffer), (x) => ('00' + x.toString(16)).slice(-2)).join('');
  62. return hash;
  63. });
  64. return result;
  65. };
  66. const getLoginAuth = async (userId: string, role: string): Promise<AliVCInteraction.ImAuth> => {
  67. const nonce = 'AK_4';
  68. const timestamp = Math.floor(Date.now() / 1000) + 3600 * 3;
  69. const pendingShaStr = `${AppId}${AppKey}${userId}${nonce}${timestamp}${role}`;
  70. const appToken = await sha256(pendingShaStr);
  71. return {
  72. nonce,
  73. timestamp,
  74. token: appToken,
  75. role,
  76. };
  77. };
  78. const showMessage = (text: string): void => {
  79. setMessageList([...messageList, text]);
  80. };
  81. const listenEngineEvents = (): void => {
  82. engine.on('connecting', () => {
  83. console.log('connecting');
  84. });
  85. engine.on('connectfailed', (err) => {
  86. console.log(`connect failed: ${err.message}`);
  87. });
  88. engine.on('connectsuccess', () => {
  89. console.log('connect success');
  90. });
  91. engine.on('disconnect', (code: number) => {
  92. console.log(`disconnect: ${code}`);
  93. });
  94. engine.on('tokenexpired', async (cb: (error: null, auth: AliVCInteraction.ImAuth) => void) => {
  95. console.log('token expired');
  96. // 这里需要更新为获取新的登录信息的代码
  97. // const auth = await getLoginAuth(userId, role);
  98. // cb(null, auth);
  99. });
  100. };
  101. const listenGroupEvents = (): void => {
  102. if (!groupManager) {
  103. return;
  104. }
  105. groupManager.on('exit', (groupId: string, reason: number) => {
  106. showMessage(`group ${groupId} close, reason: ${reason}`);
  107. });
  108. groupManager.on('memberchange', (groupId: string, memberCount: number, joinUsers: ImUser[], leaveUsers: ImUser[]) => {
  109. showMessage(`group ${groupId} member change, memberCount: ${memberCount}, joinUsers: ${joinUsers.map(u => u.userId).join(',')}, leaveUsers: ${leaveUsers.map(u => u.userId).join('')}`);
  110. });
  111. groupManager.on('mutechange', (groupId: string, status: AliVCInteraction.ImGroupMuteStatus) => {
  112. showMessage(`group ${groupId} mute change`);
  113. });
  114. groupManager.on('infochange', (groupId: string, info: AliVCInteraction.ImGroupInfoStatus) => {
  115. showMessage(`group ${groupId} info change`);
  116. });
  117. };
  118. const listenMessageEvents = (): void => {
  119. if (!messageManager) {
  120. return;
  121. }
  122. messageManager.on('recvgroupmessage', (msg: AliVCInteraction.ImMessage, groupId: string) => {
  123. console.log('recvgroupmessage', msg, groupId);
  124. showMessage(`receive group: ${groupId}, type: ${msg.type}, data: ${msg.data}`);
  125. });
  126. };
  127. const login = async (userId: string): Promise<void> => {
  128. try {
  129. await engine.init({
  130. deviceId: 'xxxx',
  131. appId: AppId,
  132. appSign: AppSign,
  133. logLevel: ImLogLevel.ERROR,
  134. });
  135. listenEngineEvents();
  136. const role = 'admin';
  137. const authData = await getLoginAuth(userId, role);
  138. await engine.login({
  139. user: {
  140. userId,
  141. userExtension: '{}',
  142. },
  143. userAuth: {
  144. timestamp: authData.timestamp,
  145. nonce: authData.nonce,
  146. role: authData.role,
  147. token: authData.token,
  148. },
  149. });
  150. const gm = engine.getGroupManager();
  151. const mm = engine.getMessageManager();
  152. setGroupManager(gm || null);
  153. setMessageManager(mm || null);
  154. setIsLoggedIn(true);
  155. setErrorMessage('');
  156. } catch (err: any) {
  157. setErrorMessage(`初始化、登录失败: ${err.code} ${err.msg}`);
  158. }
  159. };
  160. const logout = async (): Promise<void> => {
  161. try {
  162. await engine.logout();
  163. engine.unInit();
  164. setGroupManager(null);
  165. setMessageManager(null);
  166. setJoinedGroupId(null);
  167. setIsLoggedIn(false);
  168. setIsJoinedGroup(false);
  169. setErrorMessage('');
  170. } catch (err: any) {
  171. setErrorMessage(`登出失败: ${err.code} ${err.msg}`);
  172. }
  173. };
  174. const joinGroup = async (groupId: string): Promise<void> => {
  175. try {
  176. if (!groupManager) {
  177. return;
  178. }
  179. await groupManager.joinGroup(groupId);
  180. setJoinedGroupId(groupId);
  181. listenGroupEvents();
  182. listenMessageEvents();
  183. setIsJoinedGroup(true);
  184. setErrorMessage('');
  185. } catch (err: any) {
  186. setErrorMessage(`加入群组失败: ${err.code} ${err.msg}`);
  187. }
  188. };
  189. const leaveGroup = async (): Promise<void> => {
  190. try {
  191. if (!groupManager || !joinedGroupId) {
  192. return;
  193. }
  194. await groupManager.leaveGroup(joinedGroupId);
  195. groupManager.removeAllListeners();
  196. messageManager?.removeAllListeners();
  197. setJoinedGroupId(null);
  198. setIsJoinedGroup(false);
  199. setErrorMessage('');
  200. } catch (err: any) {
  201. setErrorMessage(`离开群组失败: ${err.code} ${err.msg}`);
  202. }
  203. };
  204. const createGroup = async (): Promise<void> => {
  205. try {
  206. if (!groupManager) {
  207. return;
  208. }
  209. const res = await groupManager.createGroup({
  210. groupId,
  211. groupName: 'xxx',
  212. groupMeta: 'xxx',
  213. });
  214. console.log('创建群组成功', res);
  215. setErrorMessage('');
  216. } catch (err: any) {
  217. setErrorMessage(`创建群组失败: ${err.code} ${err.msg}`);
  218. }
  219. };
  220. const sendMessage = async (): Promise<void> => {
  221. try {
  222. if (!messageManager || !joinedGroupId) {
  223. return;
  224. }
  225. const res = await messageManager.sendGroupMessage({
  226. groupId: joinedGroupId,
  227. data: msgText,
  228. type: 88888,
  229. skipAudit: false,
  230. skipMuteCheck: false,
  231. level: ImMessageLevel.NORMAL,
  232. noStorage: true,
  233. repeatCount: 1,
  234. });
  235. console.log('群消息发送成功', res);
  236. setMsgText('');
  237. setErrorMessage('');
  238. } catch (err: any) {
  239. setErrorMessage(`群消息发送失败: ${err.code} ${err.msg}`);
  240. }
  241. };
  242. const clearMessages = (): void => {
  243. setMessageList([]);
  244. };
  245. useEffect(() => {
  246. return () => {
  247. if (groupManager) {
  248. groupManager.removeAllListeners();
  249. }
  250. if (messageManager) {
  251. messageManager.removeAllListeners();
  252. }
  253. if (engine) {
  254. engine.removeAllListeners();
  255. }
  256. };
  257. }, []);
  258. return (
  259. <div className="container mx-auto p-4">
  260. <h1 className="text-2xl font-bold mb-4">互动消息 quick start</h1>
  261. <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
  262. <div>
  263. <form>
  264. <div className="mb-2">
  265. <label htmlFor="userId" className="block text-sm font-medium text-gray-700">用户ID</label>
  266. <input
  267. className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
  268. id="userId"
  269. placeholder="请输入英文字母或数字"
  270. value={userId}
  271. onChange={(e) => setUserId(e.target.value)}
  272. />
  273. </div>
  274. <div className="mb-2">
  275. <label htmlFor="groupId" className="block text-sm font-medium text-gray-700">群组ID</label>
  276. <input
  277. className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
  278. id="groupId"
  279. placeholder="加入群组前请确认是否已存在,不存在先创建"
  280. value={groupId}
  281. onChange={(e) => setGroupId(e.target.value)}
  282. />
  283. </div>
  284. <div className="mb-2 flex space-x-2">
  285. <button
  286. id="loginBtn"
  287. type="button"
  288. className={`px-3 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 ${isLoggedIn? 'opacity-50 cursor-not-allowed' : ''}`}
  289. disabled={isLoggedIn}
  290. onClick={() => login(userId)}
  291. >
  292. 登录
  293. </button>
  294. <button
  295. id="joinBtn"
  296. type="button"
  297. className={`px-3 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 ${isLoggedIn && isJoinedGroup? 'opacity-50 cursor-not-allowed' : ''}`}
  298. disabled={isLoggedIn && isJoinedGroup}
  299. onClick={() => joinGroup(groupId)}
  300. >
  301. 加入群组
  302. </button>
  303. <button
  304. id="createBtn"
  305. type="button"
  306. className={`px-3 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 ${!isLoggedIn? 'opacity-50 cursor-not-allowed' : ''}`}
  307. disabled={!isLoggedIn}
  308. onClick={() => createGroup()}
  309. >
  310. 创建群组
  311. </button>
  312. <button
  313. id="leaveBtn"
  314. type="button"
  315. className={`px-3 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-gray-600 hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500 ${!isLoggedIn ||!isJoinedGroup? 'opacity-50 cursor-not-allowed' : ''}`}
  316. disabled={!isLoggedIn ||!isJoinedGroup}
  317. onClick={() => leaveGroup()}
  318. >
  319. 离开群组
  320. </button>
  321. <button
  322. id="logoutBtn"
  323. type="button"
  324. className={`px-3 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-gray-600 hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500 ${!isLoggedIn? 'opacity-50 cursor-not-allowed' : ''}`}
  325. disabled={!isLoggedIn}
  326. onClick={() => logout()}
  327. >
  328. 登出
  329. </button>
  330. </div>
  331. <p className="mb-2 text-sm text-gray-600">假如群组ID已存在,可以一键登录+加入群组</p>
  332. <div className="mb-2 flex space-x-2">
  333. <button
  334. id="oneLoginBtn"
  335. type="button"
  336. className={`px-3 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 ${isLoggedIn && isJoinedGroup? 'opacity-50 cursor-not-allowed' : ''}`}
  337. disabled={isLoggedIn && isJoinedGroup}
  338. onClick={() => {
  339. if (userId && groupId) {
  340. login(userId).then(() => {
  341. joinGroup(groupId);
  342. });
  343. }
  344. }}
  345. >
  346. 一键登录+加入群组
  347. </button>
  348. <button
  349. id="oneLogoutBtn"
  350. type="button"
  351. className={`px-3 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-gray-600 hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500 ${!isLoggedIn ||!isJoinedGroup? 'opacity-50 cursor-not-allowed' : ''}`}
  352. disabled={!isLoggedIn ||!isJoinedGroup}
  353. onClick={() => {
  354. leaveGroup().then(() => {
  355. logout();
  356. });
  357. }}
  358. >
  359. 一键离开群组+登出
  360. </button>
  361. </div>
  362. </form>
  363. <form className="mt-4">
  364. <div className="mb-2">
  365. <label htmlFor="msgText" className="block text-sm font-medium text-gray-700">消息</label>
  366. <input
  367. className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
  368. id="msgText"
  369. value={msgText}
  370. onChange={(e) => setMsgText(e.target.value)}
  371. />
  372. </div>
  373. <div className="mb-2">
  374. <button
  375. id="sendBtn"
  376. type="button"
  377. className={`px-3 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 ${!isLoggedIn ||!isJoinedGroup? 'opacity-50 cursor-not-allowed' : ''}`}
  378. disabled={!isLoggedIn ||!isJoinedGroup}
  379. onClick={() => sendMessage()}
  380. >
  381. 发送
  382. </button>
  383. </div>
  384. </form>
  385. </div>
  386. <div>
  387. <h5 className="flex justify-between items-center text-lg font-medium mb-2">
  388. 消息展示
  389. <button
  390. id="clearBtn"
  391. type="button"
  392. className="px-3 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-gray-600 hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500"
  393. onClick={clearMessages}
  394. >
  395. 清空
  396. </button>
  397. </h5>
  398. <div id="msgList" className="mt-4 space-y-2">
  399. {messageList.map((msg, index) => (
  400. <div key={index} className="bg-gray-100 p-2 rounded-md">{msg}</div>
  401. ))}
  402. </div>
  403. </div>
  404. </div>
  405. {errorMessage && <div className="mt-2 text-red-500">{errorMessage}</div>}
  406. </div>
  407. );
  408. };