index.vue 38 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412
  1. <template>
  2. <div
  3. class="container"
  4. :style="{ height: `calc(100% - ${currentBottomHeight}px)` }"
  5. >
  6. <div class="chat-bg"></div>
  7. <!-- 顶部导航 -->
  8. <div class="header-chat">
  9. <svg-icon class="page-icon" name="lf-arrow" @click="goBack" />
  10. <div class="header-title" v-if="wsStore.toUserInfo.type == 'group'">{{ wsStore.toUserInfo.nickname || '群聊' }}({{ groupMembersArr.length }})</div>
  11. <div v-else class="header-title">{{ wsStore.toUserInfo.nickname }}</div>
  12. <svg-icon class="page-icon" name="more" @click="goDetail" />
  13. </div>
  14. <!-- 群公告 -->
  15. <div class="groupNotice" v-if="!noticeRead && wsStore.toUserInfo.type == 'group' && wsStore.toUserInfo.notice" @click="showNotice = true">
  16. <div class="m-ellipsis">群公告:{{ wsStore.toUserInfo.notice }}</div>
  17. <svg-icon class="item-icon" name="right1" />
  18. </div>
  19. <!-- 聊天消息区域 -->
  20. <div class="chat-list" ref="chatListRef">
  21. <van-loading class="load-box" size="24px" v-if="wsStore.loading">加载中...</van-loading>
  22. <template v-else>
  23. <div v-for="(item, index) in imMessages" :key="index">
  24. <div class="chat-time">
  25. {{ item.createDate || formatTime(Date.now()) }}
  26. </div>
  27. <div class="box" v-if="item.contentType !== MsgType.MSG_TYPE.NOTICE">
  28. <div
  29. class="list-item"
  30. :class="isSender(item.toUsername, item) ? '' : 'flex-reverse'"
  31. >
  32. <!-- 头像 -->
  33. <van-image
  34. class="list-img"
  35. :class="isSender(item.toUsername, item) ? 'mr12' : 'ml12'"
  36. round
  37. :src="formatAvatarUrl(item)"
  38. @click="goToPage(item)"
  39. />
  40. <!-- 内容 -->
  41. <div class="list-cont">
  42. <div>{{ item.sender?item.sender.nickname: (item.nickname || item.fromUsername || "匿名用户") }}</div>
  43. <div v-if="!item.isTemp || item.from == walletStore.account">
  44. <!-- 文本消息 -->
  45. <div class="content" v-if="item.contentType === MsgType.MSG_TYPE.TEXT" @click="onLongPress(item)">
  46. {{ item.content }}
  47. </div>
  48. <!-- 图片消息 -->
  49. <div
  50. class="img-message"
  51. v-else-if="item.contentType === MsgType.MSG_TYPE.IMAGE"
  52. >
  53. <van-image
  54. :src="item?.localUrl || IM_PATH + item.url"
  55. style="max-width: 120px; border-radius: 8px"
  56. @click="previewImage(item)"
  57. />
  58. </div>
  59. <!-- 名片消息 -->
  60. <div
  61. class="content card-message"
  62. v-else-if="item.contentType === 3"
  63. >
  64. <div class="card-title">名片</div>
  65. <div class="card-name">{{ item.content }}</div>
  66. </div>
  67. <!-- 录音消息 -->
  68. <div
  69. class="audio-message"
  70. v-else-if="item.contentType === MsgType.MSG_TYPE.AUDIO"
  71. >
  72. <messageAudio
  73. :src="item?.localUrl || IM_PATH + item.url"
  74. :isSender="isSender(item.toUsername, item)"
  75. />
  76. </div>
  77. <!-- 语音消息 -->
  78. <div class="content" v-if="item.contentType === Constant.REJECT_AUDIO_ONLINE || item.contentType === Constant.REJECT_VIDEO_ONLINE">[对方拒绝]</div>
  79. <div class="content" v-if="item.contentType === Constant.CANCELL_AUDIO_ONLINE || item.contentType === Constant.CANCELL_VIDEO_ONLINE">[通话结束]</div>
  80. <div v-if="item.quote > 0" class="quotation">{{ item.quoteMsg?.content }}</div>
  81. </div>
  82. <!-- 阅后即焚消息 -->
  83. <div
  84. v-else
  85. class="burn-message"
  86. @click="viewBurnMessage(item)"
  87. >
  88. <!-- 未查看 -->
  89. <div v-if="!item.view && item.content" class="burn-mask">
  90. 阅后即焚,点击查看
  91. </div>
  92. <!-- 已查看且正在倒计时 -->
  93. <template v-else-if="item.view && item.countdown > 0 && item.content">
  94. <!-- 文本类型 -->
  95. <div v-if="item.contentType === MsgType.MSG_TYPE.TEXT" class="burn-content">
  96. {{ item.content }}
  97. <span class="burn-countdown">{{ item.countdown }}s</span>
  98. </div>
  99. <!-- 录音类型 -->
  100. <div v-else-if="item.contentType === MsgType.MSG_TYPE.AUDIO" class="burn-audio">
  101. <messageAudio
  102. :src="item?.localUrl || IM_PATH + item.url"
  103. :isSender="isSender(item.toUsername, item)"
  104. />
  105. <span class="burn-countdown">{{ item.countdown }}s</span>
  106. </div>
  107. </template>
  108. <!-- 倒计时结束 -->
  109. <div v-else class="burn-destroyed">
  110. 该消息已焚毁
  111. </div>
  112. </div>
  113. </div>
  114. </div>
  115. </div>
  116. <div class="chat-time" v-if="item.contentType === MsgType.MSG_TYPE.NOTICE">
  117. {{ item.content }}
  118. </div>
  119. </div>
  120. </template>
  121. </div>
  122. <!-- 群公告组件 -->
  123. <GroupNotice
  124. v-model:show="showNotice"
  125. :notice="wsStore.toUserInfo.notice"
  126. @close="handleNoticeClose"
  127. />
  128. <!-- @页面 -->
  129. <AtUserList ref="atList" @select="onSelectUser" />
  130. <!-- 撤回弹窗 -->
  131. <van-action-sheet
  132. v-model:show="showActionSheet"
  133. :actions="actions"
  134. cancel-text="取消"
  135. @select="onActionSelect"
  136. />
  137. <!-- @样式 -->
  138. <div class="assign" v-if="wsStore.toUserInfo.type == 'group' && wsStore.isassign" @click="changAssign">
  139. <div class="assign-text">有人@我</div>
  140. </div>
  141. <!-- 引用消息展示 -->
  142. <div v-if="quoteMsg" class="quote-box">
  143. <div class="quote-content">{{ quoteMsg.fromUsername}}:{{ renderQuoteContent(quoteMsg) }}</div>
  144. <span class="quote-close" @click="cancelQuote">✕</span>
  145. </div>
  146. <!-- 输入框 -->
  147. <div class="page-foot">
  148. <div class="flex-box">
  149. <!-- 录音/文字切换按钮 -->
  150. <svg-icon
  151. type="button"
  152. class="page-icon"
  153. :name="voiceMode ? 'keyboard' : 'voice'"
  154. @click="toggleVoiceMode"
  155. />
  156. <!-- 文字输入框 或 按住说话按钮 -->
  157. <div class="box-input">
  158. <template v-if="!voiceMode">
  159. <van-field
  160. rows="1"
  161. type="textarea"
  162. :border="false"
  163. autosize
  164. class="input"
  165. v-model="text"
  166. @focus="onFocus"
  167. placeholder="输入文本"
  168. @keypress="handleKeyPress"
  169. @keyup.enter="sendMessage"
  170. />
  171. <!-- :disabled="deletefriend.includes(wsStore.toUserInfo.uuid)" -->
  172. </template>
  173. <template v-else>
  174. <div
  175. class="hold-talk-btn"
  176. @touchstart.prevent.stop="handleTouchStart"
  177. @touchmove.prevent.stop="handleTouchMove"
  178. @touchend.prevent.stop="handleTouchEnd"
  179. >
  180. 按住说话
  181. </div>
  182. </template>
  183. </div>
  184. <svg-icon
  185. class="page-icon mr12 emoji-toggle"
  186. name="emoji"
  187. @click="toggleAppBox(1)"
  188. />
  189. <svg-icon v-if="showBurn" @click="closeMessaageBurning"
  190. class="page-icon-qx"
  191. name="qx"
  192. />
  193. <svg-icon v-else
  194. class="page-icon tools-toggle"
  195. name="add2"
  196. @click="toggleAppBox(2)"
  197. />
  198. </div>
  199. <!-- 录音状态浮层 -->
  200. <div v-if="recording" class="recording-toast">
  201. <div class="mic-icon"></div>
  202. <div v-if="cancelRecording" class="cancel-msg">松开手指,取消发送</div>
  203. <div v-else class="send-msg">手指上滑,取消发送</div>
  204. </div>
  205. <!-- 表情面板 -->
  206. <div
  207. class="app-box"
  208. v-show="showEmoji"
  209. :style="{ height: `${appBoxHeight}px` }"
  210. ref="emojiRef"
  211. >
  212. <div>
  213. <span
  214. v-for="(emoji, index) in emojis"
  215. :key="index"
  216. class="emoji-item"
  217. @click="insertEmoji(emoji)"
  218. >
  219. {{ emoji }}
  220. </span>
  221. </div>
  222. </div>
  223. <!-- 工具栏面板 -->
  224. <div
  225. class="app-box"
  226. v-show="showTools"
  227. :style="{ height: `${appBoxHeight}px` }"
  228. ref="toolsRef"
  229. >
  230. <div class="tool-btn">
  231. <van-uploader :after-read="afterRead" :before-read="beforeRead">
  232. <svg-icon class="tool-icon" name="tp" />
  233. </van-uploader>
  234. <div>图片</div>
  235. </div>
  236. <!-- <div class="tool-btn">
  237. <svg-icon class="tool-icon" name="ps" />
  238. <div>拍摄</div>
  239. </div> -->
  240. <div class="tool-btn" v-if="wsStore.toUserInfo.type == 'user'">
  241. <svg-icon
  242. class="tool-icon"
  243. name="yyth"
  244. @click="startAudioOnline(Constant.DIAL_AUDIO_ONLINE, 'audio')"
  245. />
  246. <div>语音通话</div>
  247. </div>
  248. <div class="tool-btn" v-if="wsStore.toUserInfo.type == 'user'">
  249. <svg-icon
  250. class="tool-icon"
  251. name="spth"
  252. @click="startAudioOnline(Constant.DIAL_VIDEO_ONLINE, 'video')"
  253. />
  254. <div>视频通话</div>
  255. </div>
  256. <!-- <div class="tool-btn">
  257. <svg-icon class="tool-icon" name="mp" />
  258. <div>名片</div>
  259. </div> -->
  260. <div class="tool-btn" @click="openMessageBurning">
  261. <svg-icon class="tool-icon" name="mp" />
  262. <div>阅后即焚</div>
  263. </div>
  264. </div>
  265. </div>
  266. </div>
  267. </template>
  268. <script setup>
  269. import { useRouter, useRoute } from "vue-router";
  270. import { useWebSocketStore } from "@/stores/modules/webSocketStore.js";
  271. import { useWalletStore } from "@/stores/modules/walletStore.js";
  272. import { Keyboard } from "@capacitor/keyboard";
  273. import { Capacitor } from "@capacitor/core";
  274. import * as MsgType from "@/common/constant/msgType";
  275. import messageAudio from "@/views/im/components/messageAudio/index.vue";
  276. import { showToast, showImagePreview } from "vant";
  277. import { useWebRTCStore } from "@/stores/modules/webrtcStore";
  278. import * as Constant from "@/common/constant/Constant";
  279. import { soundVoice } from "@/utils/notifications.js";
  280. import AtUserList from "./components/AtUserList/index.vue";
  281. import GroupNotice from "./components/GroupNotice/index.vue";
  282. const IM_PATH = import.meta.env.VITE_IM_PATH_FIlE;
  283. // 路由 & store
  284. const router = useRouter();
  285. const route = useRoute();
  286. const wsStore = useWebSocketStore();
  287. const walletStore = useWalletStore();
  288. const rtcStore = useWebRTCStore();
  289. // 输入框文本
  290. const text = ref("");
  291. // 状态管理
  292. const keyboardHeight = ref(0);
  293. const showEmoji = ref(false);
  294. const showTools = ref(false);
  295. const appBoxHeight = ref(210);
  296. const chatListRef = ref(null);
  297. const emojiRef = ref(null);
  298. const toolsRef = ref(null);
  299. // 听筒模式
  300. const earMode = ref(false);
  301. // @成员
  302. const ccMsg = ref([]);
  303. // 消息置顶
  304. const stickTop = ref(0);
  305. // 引用消息
  306. const quoteMsg = ref(null);
  307. // 表情数组
  308. const emojis = [
  309. "😀", "😃", "😄", "😁", "😆", "😅", "😂", "🤣",
  310. "😊", "😇", "🙂", "🙃", "😉", "😌", "😍","😜","🤪","🫣","🤔","🤤","🥲","😋",
  311. "😀", "😃", "😄", "😁", "😆", "😅", "😂", "🤣",
  312. ];
  313. const atList = ref();
  314. const showBurn = ref(false) // 阅后即焚是否开启
  315. const showNotice = ref(false);
  316. const noticeRead = ref(false);
  317. const groupMembersArr = ref([]);
  318. const currentMsg = ref('');
  319. const showActionSheet = ref(false)
  320. const actions = ref([])
  321. const formatAvatarUrl = (item) => {
  322. const url = item?.sender?.avatar || item?.avatar || ''
  323. if (/^https?:\/\//.test(url)) {
  324. return url
  325. }
  326. return IM_PATH + url
  327. }
  328. const isSender = (toUsername, item) => {
  329. if(item?.sender){
  330. return walletStore.account !== item.sender.uuid;
  331. }
  332. return walletStore.account === toUsername;
  333. };
  334. const imMessages = computed(() =>{
  335. wsStore.funUpdateUnread(wsStore.toUserInfo.uuid);
  336. // const quotes = {};
  337. return wsStore.messages.
  338. // filter(item =>{
  339. // if(item.quote > 0){
  340. // quotes[item.quote] = wsStore.messages.find(val => val.id == item.quote)
  341. // }
  342. // if(item.messageType == MsgType.MESSAGE_TYPE_GROUP && wsStore.toUserInfo.type != 'user') {
  343. // return item
  344. // }
  345. // if(item.messageType == MsgType.MESSAGE_TYPE_USER && wsStore.toUserInfo.type == 'user') {
  346. // return item
  347. // }
  348. // }).
  349. map(item=>{
  350. item.quoteMsg = null;
  351. if (item.quote>0){
  352. item.quoteMsg = wsStore.messages.find(val => val.id == item.quote)
  353. }
  354. return item;
  355. });
  356. })
  357. const changAssign = () => {
  358. wsStore.isassign = '';
  359. }
  360. // 滚动到底部
  361. const scrollToBottom = () => {
  362. nextTick(() => {
  363. if (chatListRef.value) {
  364. const el = chatListRef.value;
  365. // el.scrollTop = el.scrollHeight;
  366. setTimeout(() => {
  367. el.scrollTo({
  368. left: 0, top: el.scrollHeight + 100,behavior: 'smooth'
  369. });
  370. }, 200);
  371. }
  372. });
  373. };
  374. watch(
  375. () => wsStore.indexs,
  376. (newVal,oldVal) => {
  377. if (newVal !== oldVal) {
  378. scrollToBottom();
  379. }
  380. }
  381. );
  382. watch(
  383. () => rtcStore.isEarpieceMode,
  384. (newVal) => {
  385. earMode.value = newVal;
  386. }
  387. );
  388. // 平台判断
  389. const isMobile = Capacitor.getPlatform() !== "web";
  390. // 计算当前底部总高度
  391. const currentBottomHeight = computed(() => {
  392. if (keyboardHeight.value > 0) return keyboardHeight.value;
  393. if (showEmoji.value || showTools.value) return appBoxHeight.value;
  394. return 0;
  395. });
  396. // 切换表情/工具面板
  397. const toggleAppBox = async (type) => {
  398. if(!wsStore.verifyChatAuth(wsStore.toUserInfo.uuid)){
  399. showToast(`${wsStore.toUserInfo.type == 'user' ? '对方已删除' : '您已不在群聊里面'}`);
  400. return;
  401. }
  402. voiceMode.value = false;
  403. if (isMobile) await Keyboard.hide();
  404. keyboardHeight.value = 0;
  405. if (type === 1) {
  406. showEmoji.value = !showEmoji.value;
  407. showTools.value = false;
  408. } else {
  409. showTools.value = !showTools.value;
  410. showEmoji.value = false;
  411. }
  412. scrollToBottom();
  413. };
  414. // 插入表情
  415. const insertEmoji = (emoji) => {
  416. text.value += emoji;
  417. };
  418. // 预览图片
  419. const previewImage = (item) => {
  420. const imageList = wsStore.messages
  421. .filter((m) => m.contentType === MsgType.MSG_TYPE.IMAGE)
  422. .map((m) => m.localUrl || IM_PATH + m.url);
  423. const index = imageList.findIndex(
  424. (url) => url === (item.localUrl || IM_PATH + item.url)
  425. );
  426. showImagePreview({
  427. images: imageList,
  428. startPosition: index,
  429. });
  430. };
  431. const onFocus = () => {
  432. // 隐藏所有面板
  433. showEmoji.value = false;
  434. showTools.value = false;
  435. if (isMobile) setupKeyboardListeners();
  436. scrollToBottom();
  437. };
  438. // 键盘监听
  439. const setupKeyboardListeners = async () => {
  440. Keyboard.addListener("keyboardWillShow", (info) => {
  441. keyboardHeight.value = info.keyboardHeight;
  442. });
  443. Keyboard.addListener("keyboardWillHide", () => {
  444. keyboardHeight.value = 0;
  445. });
  446. };
  447. // 设置消息引用
  448. const handleQuote = (item) => {
  449. quoteMsg.value = item //imMessages.find(m => m.id == item.id)
  450. };
  451. // 取消消息引用
  452. const cancelQuote = () => {
  453. quoteMsg.value = null; // 发送即删除
  454. };
  455. // 切换输出模式 true 听筒,false 扬声器
  456. const changeAudioOutputMode = async (flag) => {
  457. await rtcStore.setAudioOutputToEarpiece(flag);
  458. };
  459. // 群消息置顶操作
  460. const groupMessageStickTop = (message)=>{
  461. if(wsStore.toUserInfo.type == 'user'){
  462. return;
  463. }
  464. wsStore.sendMessage({
  465. content: JSON.stringify({
  466. content: message.content,
  467. msgId: `${message.id}`, // 消息id
  468. sticks: message.id, // 置顶
  469. }),
  470. contentType: message.contentType,
  471. messageType: MsgType.MESSAGE_STICKY_GROUP,
  472. });
  473. scrollToBottom();
  474. }
  475. // 开启消息阅后即焚
  476. const openMessageBurning = ()=>{
  477. showBurn.value = true
  478. showTools.value = false
  479. }
  480. // 关闭消息阅后即焚
  481. const closeMessaageBurning = ()=>{
  482. showBurn.value = false
  483. }
  484. // 撤回消息
  485. const revokeMessage = (message)=>{
  486. // if(message.to != walletStore.account){
  487. // return;
  488. // }
  489. wsStore.sendMessage({
  490. content: JSON.stringify({
  491. content: '',
  492. msgId: message.id + '',
  493. }),
  494. contentType: MsgType.MSG_TYPE.NOTICE,
  495. messageType: wsStore.toUserInfo.type == 'user'?MsgType.MESSAGE_REVOKE:MsgType.MESSAGE_REVOKE_GROUP,
  496. });
  497. }
  498. // 点击查看阅后即焚消息
  499. const viewBurnMessage = (message) => {
  500. console.log("查看阅后即焚消息:", message);
  501. if (!message.view) {
  502. message.view = true;
  503. message.countdown = 15; // 倒计时
  504. // 如果是录音,可以自动播放
  505. if (message.contentType === MsgType.MSG_TYPE.AUDIO) {
  506. const audio = new Audio(message?.localUrl || IM_PATH + message.url);
  507. audio.play();
  508. }
  509. const timer = setInterval(() => {
  510. if (message.countdown > 1) {
  511. message.countdown--;
  512. } else {
  513. clearInterval(timer);
  514. destroyMessage(message); // 发撤回通知
  515. message.content = '';
  516. message.countdown = 0; // 标记焚毁
  517. }
  518. }, 1000);
  519. }
  520. };
  521. // 真正销毁
  522. const destroyMessage = (message) => {
  523. message.content = ''; // 先清空本地显示
  524. // 如果是群消息, 本地更新消息销毁
  525. if (message.messageType == MsgType.MESSAGE_TYPE_GROUP) {
  526. const msg = {isTemp: true, content: '', contentType: message.contentType, messageType: message.messageType};
  527. // 更新消息
  528. wsStore.modifyMessage(msg, message.id, wsStore.toUserInfo.uuid);
  529. // 更新会话列表
  530. wsStore.updateSessionNewMessage(msg, wsStore.toUserInfo.uuid);
  531. return;
  532. }
  533. wsStore.sendMessage({
  534. content: JSON.stringify({
  535. content: '',
  536. msgId: message.id + '',
  537. isTemp: true,
  538. }),
  539. contentType: MsgType.MSG_TYPE.NOTICE,
  540. messageType: wsStore.toUserInfo.type == 'user'
  541. ? MsgType.MESSAGE_REVOKE
  542. : MsgType.MESSAGE_REVOKE_GROUP,
  543. });
  544. };
  545. // // 阅后销毁消息
  546. // const destroyMessage = (message)=>{
  547. // wsStore.sendMessage({
  548. // content: JSON.stringify({
  549. // content: '',
  550. // msgId: message.id + '',
  551. // isTemp: true,
  552. // }),
  553. // contentType: MsgType.MSG_TYPE.NOTICE,
  554. // messageType: wsStore.toUserInfo.type == 'user'?MsgType.MESSAGE_REVOKE:MsgType.MESSAGE_REVOKE_GROUP,
  555. // });
  556. // }
  557. // 当子组件关闭时触发---关闭群公告
  558. const handleNoticeClose = () => {
  559. console.log(11)
  560. showNotice.value = false
  561. // noticeRead.value = true
  562. }
  563. // 录音相关状态
  564. const voiceMode = ref(false); // false: 文字输入, true: 语音模式
  565. const recording = ref(false);
  566. const cancelRecording = ref(false);
  567. const startY = ref(0);
  568. let mediaRecorder = null;
  569. let audioChunks = [];
  570. // 切换文字/语音输入模式
  571. const toggleVoiceMode = () => {
  572. if(!wsStore.verifyChatAuth(wsStore.toUserInfo.uuid)){
  573. showToast(`${wsStore.toUserInfo.type == 'user' ? '对方已删除' : '您已不在群聊里面'}`);
  574. return;
  575. }
  576. voiceMode.value = !voiceMode.value;
  577. // 切换时关闭表情和工具面板,隐藏键盘
  578. showEmoji.value = false;
  579. showTools.value = false;
  580. if (isMobile) Keyboard.hide();
  581. keyboardHeight.value = 0;
  582. scrollToBottom();
  583. };
  584. // 录音事件
  585. const handleTouchStart = async (e) => {
  586. startY.value = e.touches[0].clientY;
  587. cancelRecording.value = false;
  588. recording.value = true;
  589. audioChunks = [];
  590. try {
  591. const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
  592. mediaRecorder = new MediaRecorder(stream, {
  593. mimeType: "audio/webm; codecs=opus",
  594. });
  595. mediaRecorder.ondataavailable = (ev) => {
  596. audioChunks.push(ev.data);
  597. };
  598. mediaRecorder.start(1000);
  599. console.log("开始录音");
  600. } catch (err) {
  601. console.error("麦克风权限获取失败", err);
  602. recording.value = false;
  603. }
  604. };
  605. // 录音
  606. const handleTouchMove = (e) => {
  607. const currentY = e.touches[0].clientY;
  608. if (startY.value - currentY > 50) {
  609. cancelRecording.value = true;
  610. } else {
  611. cancelRecording.value = false;
  612. }
  613. };
  614. // 录音发送
  615. const handleTouchEnd = () => {
  616. if (!mediaRecorder) return;
  617. mediaRecorder.stop();
  618. mediaRecorder.stream.getTracks().forEach((t) => t.stop());
  619. recording.value = false;
  620. mediaRecorder.onstop = async () => {
  621. if (cancelRecording.value) {
  622. console.log("录音取消");
  623. return;
  624. }
  625. if (audioChunks.length === 0) return;
  626. const audioBlob = new Blob(audioChunks, { type: "audio/webm" });
  627. const arrayBuffer = await audioBlob.arrayBuffer();
  628. const audioData = new Uint8Array(arrayBuffer);
  629. wsStore.sendMessage({
  630. // content: "",
  631. content: JSON.stringify({
  632. content: "",
  633. msgId: `ms${Date.now()}`, // 消息id
  634. quote: quoteMsg.value?.id, // 引用消息id
  635. cc: wsStore.toUserInfo.type == 'user'?"":ccMsg.value.join(","), // @成员
  636. isTemp: showBurn.value === true, // 消息阅后即焚
  637. }),
  638. contentType: MsgType.MSG_TYPE.AUDIO,
  639. messageType: wsStore.toUserInfo.type == 'user'?MsgType.MESSAGE_TYPE_USER:(ccMsg.value.length> 0?MsgType.MESSAGE_CC_GROUP:MsgType.MESSAGE_TYPE_GROUP),
  640. fileSuffix: "wav", // 使用webm后缀更准确
  641. file: audioData, // 将Uint8Array转为普通数组
  642. });
  643. cancelQuote(); // 取消引用
  644. ccMsg.value = []; // 清空 @成员
  645. scrollToBottom();
  646. console.log("语音已发送");
  647. };
  648. };
  649. // 监听输入 @
  650. const handleKeyPress = (e) => {
  651. console.log("监听输入 @", e);
  652. if(wsStore.toUserInfo.type == 'user') return;
  653. if (e.key === "@") {
  654. atList.value.open(); // 打开选人弹窗
  655. }
  656. };
  657. // 选择用户回填输入框
  658. const onSelectUser = (user) => {
  659. text.value += `${user.nickname} `;
  660. ccMsg.value.push(user)
  661. };
  662. const onLongPress = (msg) => {
  663. if(msg.toUsername == walletStore.account){
  664. actions.value = [
  665. { name: "复制", key: "copy" },
  666. { name: "引用", key: "quote" },
  667. // { name: "置顶", key: "stick" },
  668. ]
  669. }else{
  670. actions.value = [
  671. { name: "复制", key: "copy" },
  672. { name: "撤回", key: "revoke" },
  673. { name: "引用", key: "quote" },
  674. // { name: "置顶", key: "stick" },
  675. ]
  676. }
  677. currentMsg.value = msg;
  678. showActionSheet.value = true;
  679. };
  680. const onActionSelect = (action) => {
  681. if (!currentMsg.value) return;
  682. if (action.key === "copy") {
  683. navigator.clipboard.writeText(currentMsg.value.content);
  684. showToast("已复制");
  685. } else if (action.key === "revoke") {
  686. revokeMessage(currentMsg.value);
  687. currentMsg.value.revoked = true;
  688. } else if(action.key === "quote") {
  689. // 引用
  690. handleQuote(currentMsg.value)
  691. }
  692. showActionSheet.value = false;
  693. };
  694. // 渲染引用的内容(文字/图片等)
  695. const renderQuoteContent = (msg) => {
  696. if (msg.contentType === MsgType.MSG_TYPE.TEXT) {
  697. return msg.content;
  698. } else if (msg.contentType === MsgType.MSG_TYPE.IMAGE) {
  699. return "[图片]";
  700. } else if (msg.contentType === MsgType.MSG_TYPE.AUDIO) {
  701. return "[语音]";
  702. } else {
  703. return "[消息]";
  704. }
  705. };
  706. // 发送消息
  707. const sendMessage = () => {
  708. if(!wsStore.verifyChatAuth(wsStore.toUserInfo.uuid)){
  709. showToast(`${wsStore.toUserInfo.type == 'user' ? '对方已删除' : '您已不在群聊里面'}`);
  710. return;
  711. }
  712. if (!text.value.trim()) return;
  713. // 检测消息@成员
  714. let cc = [];
  715. if(ccMsg.value.length>0 && wsStore.toUserInfo.type != 'user'){
  716. const matchs = text.value.match(/(@([^ ]+) )/ig);
  717. cc = ccMsg.value.filter(val=> matchs.includes(`@${val.nickname} `)).map(val=>val.userId);
  718. // 检测@所有人
  719. if(matchs.includes(`@所有人 `)){
  720. cc = [0]
  721. }
  722. }
  723. console.log(wsStore.toUserInfo);
  724. const message = {
  725. content: JSON.stringify({
  726. content: text.value,
  727. msgId: `ms${Date.now()}`, // 消息id
  728. quote: quoteMsg.value?.id, // 引用消息id
  729. cc: wsStore.toUserInfo.type == 'user'?"":cc.join(","), // @成员
  730. isTemp: showBurn.value === true, // 消息阅后即焚
  731. }),
  732. contentType: MsgType.MSG_TYPE.TEXT, // 1: 文本消息
  733. messageType: wsStore.toUserInfo.type == 'user'?MsgType.MESSAGE_TYPE_USER:(cc.length> 0?MsgType.MESSAGE_CC_GROUP:MsgType.MESSAGE_TYPE_GROUP),
  734. };
  735. wsStore.sendMessage(message);
  736. text.value = "";
  737. cancelQuote(); // 取消引用
  738. ccMsg.value = []; // 清空 @成员
  739. scrollToBottom();
  740. };
  741. // 发送图片消息
  742. const afterRead = async (file) => {
  743. if(!wsStore.verifyChatAuth(wsStore.toUserInfo.uuid)){
  744. showToast(`${wsStore.toUserInfo.type == 'user' ? '对方已删除' : '您已不在群聊里面'}`);
  745. return;
  746. }
  747. const arrayBuffer = await file.file.arrayBuffer();
  748. const message = {
  749. // content: text.value, // 如果有文本内容
  750. content: JSON.stringify({
  751. content: text.value,
  752. msgId: `ms${Date.now()}`, // 消息id
  753. quote: quoteMsg.value?.id, // 引用消息id
  754. cc: wsStore.toUserInfo.type == 'user'?"":ccMsg.value.join(","), // @成员
  755. isTemp: showBurn.value === true, // 消息阅后即焚
  756. }),
  757. contentType: MsgType.MSG_TYPE.IMAGE, // 音频消息类型
  758. messageType: wsStore.toUserInfo.type == 'user'?MsgType.MESSAGE_TYPE_USER:(ccMsg.value.length> 0?MsgType.MESSAGE_CC_GROUP:MsgType.MESSAGE_TYPE_GROUP),
  759. fileSuffix: file.file.type, // 使用webm后缀更准确
  760. file: new Uint8Array(arrayBuffer), // 将Uint8Array转为普通数组
  761. };
  762. cancelQuote(); // 取消引用
  763. ccMsg.value = []; // 清空 @成员
  764. wsStore.sendMessage(message);
  765. scrollToBottom();
  766. };
  767. // 图片类型
  768. const beforeRead = (file) => {
  769. const realFile = file.file || file;
  770. const type = realFile.type;
  771. const name = realFile.name || "";
  772. if (type === "image/svg+xml" || name.endsWith(".svg")) {
  773. showToast("不支持上传 SVG 格式的图片");
  774. return false;
  775. }
  776. return true;
  777. };
  778. // 创建呼叫:开启语音电话: 调试用,正式逻辑读src/views/im/hook/messagesHook.js
  779. // ==== 1. 发起语音通话 ====
  780. const startAudioOnline = async (contentType, streamType) => {
  781. // 清理被呼叫者信息
  782. wsStore.toUserInfo.sender = null;
  783. // 调起通话界面
  784. rtcStore.streamType = streamType
  785. rtcStore.imSate.videoCallModal = true;
  786. // 设置被呼叫对象
  787. rtcStore.imSate.callAvatar = wsStore.toUserInfo.avatar;
  788. rtcStore.imSate.callName = wsStore.toUserInfo.nickname;
  789. // 设置呼叫者/被呼叫者
  790. rtcStore.isCaller = true;
  791. // 通知被呼叫者
  792. wsStore.sendMessage({
  793. messageType: wsStore.toUserInfo.type == 'user'?MsgType.MESSAGE_TYPE_USER:MsgType.MESSAGE_TYPE_GROUP,
  794. contentType,
  795. type: Constant.MESSAGE_TRANS_TYPE,
  796. avatar: walletStore.avatar,
  797. content: JSON.stringify({
  798. content: walletStore.username,
  799. sender: walletStore.account,
  800. })
  801. });
  802. // 播放铃声
  803. soundVoice.play()
  804. // 开启本地视频
  805. if (streamType === 'video') {
  806. rtcStore.getUserMedia({ audio: true, video: true })
  807. }
  808. };
  809. // 时间格式化
  810. const formatTime = (timestamp) => {
  811. const date = new Date(timestamp);
  812. const h = date.getHours().toString().padStart(2, "0");
  813. const m = date.getMinutes().toString().padStart(2, "0");
  814. return `${h}:${m}`;
  815. };
  816. // 页面生命周期
  817. onMounted(async () => {
  818. wsStore.toUserInfo.uuid = route.query.uuid;
  819. groupMembersArr.value = await wsStore.fetchGroupMembers();
  820. await wsStore.getMessages({
  821. uuid: wsStore.toUserInfo.type == 'user'?walletStore.account : route.query.uuid,
  822. messageType: wsStore.toUserInfo.type == 'user'?1:2, //1:个人 2:群组
  823. friendUsername: wsStore.toUserInfo.type == 'user'?route.query.uuid : walletStore.account
  824. });
  825. scrollToBottom();
  826. document.addEventListener("click", handleClickOutside);
  827. });
  828. onUnmounted(() => {
  829. if (isMobile) Keyboard.removeAllListeners();
  830. });
  831. onBeforeUnmount(() => {
  832. document.removeEventListener("click", handleClickOutside);
  833. });
  834. // 判断是否点击在元素外
  835. const handleClickOutside = (event) => {
  836. const emojiEl = emojiRef.value;
  837. const toolsEl = toolsRef.value;
  838. const target = event.target;
  839. if (
  840. showEmoji.value &&
  841. emojiEl &&
  842. !emojiEl.contains(target) &&
  843. !target.closest(".emoji-toggle")
  844. ) {
  845. showEmoji.value = false;
  846. }
  847. if (
  848. showTools.value &&
  849. toolsEl &&
  850. !toolsEl.contains(target) &&
  851. !target.closest(".tools-toggle")
  852. ) {
  853. showTools.value = false;
  854. }
  855. };
  856. // 页面跳转
  857. const goToPage = (item) => {
  858. if(!(isSender(item.toUsername))) return;
  859. router.push({
  860. path: 'personal',
  861. query:{
  862. uuid:wsStore.toUserInfo.type == 'user'?wsStore.toUserInfo.uuid:item.uuid,
  863. type:2
  864. }
  865. })
  866. }
  867. const goBack = () => router.push("im");
  868. const goDetail = () =>{
  869. if(!wsStore.verifyChatAuth(wsStore.toUserInfo.uuid)){
  870. showToast(`${wsStore.toUserInfo.type == 'user' ? '对方已删除' : '您已不在群聊里面'}`);
  871. return;
  872. }
  873. // 单聊的话跳转查看个人信息页面
  874. // if(wsStore.toUserInfo.type == 'user'){
  875. // router.push({
  876. // path: 'personal',
  877. // query:{
  878. // uuid:wsStore.toUserInfo.uuid,
  879. // type:2
  880. // }
  881. // })
  882. // return;
  883. // }
  884. router.push({
  885. path: 'detail',
  886. query:{ status:wsStore.toUserInfo.type == 'user'?1:2 }
  887. }) // 1:单聊 2:群聊
  888. }
  889. </script>
  890. <style lang="less" scoped>
  891. .load-box{
  892. text-align: center !important;
  893. margin-top: 50px !important;
  894. }
  895. .mr12 {
  896. margin-right: 12px;
  897. }
  898. .ml12 {
  899. margin-left: 12px;
  900. }
  901. .text-right {
  902. text-align: right;
  903. }
  904. .page-icon {
  905. width: 24px;
  906. height: 24px;
  907. flex-shrink: 0;
  908. }
  909. .page-icon-qx{
  910. width: 22px;
  911. height: 22px;
  912. flex-shrink: 0;
  913. }
  914. .container {
  915. display: flex;
  916. flex-direction: column;
  917. .chat-bg {
  918. height: 126px;
  919. background: linear-gradient(90deg, @theme-color1 0%, #40a4fb 100%);
  920. position: absolute;
  921. left: 0;
  922. right: 0;
  923. z-index: -1;
  924. }
  925. .header-chat {
  926. padding-top: 56px;
  927. margin: 0 16px;
  928. display: flex;
  929. align-items: center;
  930. color: @theme-color1;
  931. .header-title {
  932. flex: 1;
  933. font-family:
  934. PingFang SC,
  935. PingFang SC;
  936. font-weight: 500;
  937. font-size: 19px;
  938. color: #ffffff;
  939. text-align: center;
  940. }
  941. }
  942. .chat-list {
  943. background: #f7f8fa;
  944. border-radius: 30px 30px 0 0;
  945. flex: 1;
  946. overflow: auto;
  947. margin-top: 20px;
  948. padding: 0 16px 24px;
  949. .chat-time {
  950. font-family:
  951. PingFang SC,
  952. PingFang SC;
  953. font-weight: 400;
  954. font-size: 12px;
  955. color: #8d8d8d;
  956. text-align: center;
  957. margin: 20px 0;
  958. }
  959. .box {
  960. .list-item {
  961. display: flex;
  962. margin-bottom: 24px;
  963. .list-img {
  964. width: 44px;
  965. height: 44px;
  966. flex-shrink: 0;
  967. }
  968. .list-cont {
  969. font-family:
  970. PingFang SC,
  971. PingFang SC;
  972. font-weight: 400;
  973. font-size: 12px;
  974. color: #8d8d8d;
  975. max-width: 70%;
  976. display: flex;
  977. flex-direction: column;
  978. align-items: flex-start;
  979. .business-card {
  980. width: 199px;
  981. height: 93px;
  982. background: #ffffff;
  983. border-radius: 10px;
  984. margin-top: 8px;
  985. padding: 10px;
  986. box-sizing: border-box;
  987. .business-card-cont {
  988. display: flex;
  989. align-items: center;
  990. font-family:
  991. PingFang SC,
  992. PingFang SC;
  993. font-weight: 400;
  994. font-size: 15px;
  995. color: #000000;
  996. }
  997. .line {
  998. height: 1px;
  999. background: #f2f2f2;
  1000. margin: 10px 0 6px;
  1001. }
  1002. .business-card-text {
  1003. font-family:
  1004. PingFang SC,
  1005. PingFang SC;
  1006. font-weight: 400;
  1007. font-size: 10px;
  1008. color: #8d8d8d;
  1009. }
  1010. }
  1011. .content {
  1012. background: #ffffff; // 对方消息背景白色
  1013. color: #000;
  1014. border-radius: 10px;
  1015. margin-top: 8px;
  1016. padding: 8px 17px;
  1017. word-break: break-word;
  1018. white-space: pre-wrap;
  1019. max-width: 100%;
  1020. font-family:
  1021. PingFang SC,
  1022. PingFang SC;
  1023. font-weight: 400;
  1024. font-size: 15px;
  1025. }
  1026. .img-message {
  1027. margin-top: 8px;
  1028. }
  1029. }
  1030. }
  1031. .withdrawal {
  1032. display: flex;
  1033. justify-content: center;
  1034. margin-bottom: 24px;
  1035. .withdrawal-text {
  1036. width: 142px;
  1037. height: 29px;
  1038. line-height: 29px;
  1039. box-sizing: border-box;
  1040. background: #f2f2f2;
  1041. border-radius: 4px;
  1042. font-family:
  1043. PingFang SC,
  1044. PingFang SC;
  1045. font-weight: 400;
  1046. font-size: 12px;
  1047. color: #8d8d8d;
  1048. text-align: center;
  1049. }
  1050. }
  1051. .flex-reverse {
  1052. flex-direction: row-reverse;
  1053. }
  1054. .flex-reverse .list-cont {
  1055. align-items: flex-end;
  1056. .content {
  1057. background: #4d71ff; // 自己的消息是蓝色
  1058. color: #ffffff; // 白字
  1059. }
  1060. }
  1061. }
  1062. }
  1063. .chat-list::-webkit-scrollbar {
  1064. width: 0;
  1065. }
  1066. .page-foot {
  1067. position: relative;
  1068. background-color: #fff;
  1069. .flex-box {
  1070. padding: 8px 16px 16px;
  1071. display: flex;
  1072. align-items: center;
  1073. box-sizing: border-box;
  1074. .box-input{
  1075. flex: 1;
  1076. margin: 0 12px;
  1077. }
  1078. .input {
  1079. flex: 1;
  1080. background: #f2f2f2;
  1081. border-radius: 17px;
  1082. border: 1px solid #d8d8d8;
  1083. padding: 6px 16px;
  1084. font-weight: 500;
  1085. font-size: 15px;
  1086. // margin: 0 12px;
  1087. overflow-y: auto;
  1088. }
  1089. }
  1090. }
  1091. }
  1092. .app-box {
  1093. position: fixed;
  1094. bottom: 210px;
  1095. left: 0;
  1096. right: 0;
  1097. background: white;
  1098. transition: transform 0.3s ease;
  1099. transform: translateY(100%);
  1100. display: flex;
  1101. flex-wrap: wrap;
  1102. padding: 10px 16px;
  1103. overflow-y: auto;
  1104. box-sizing: border-box;
  1105. &.visible {
  1106. transform: translateY(0);
  1107. }
  1108. .tool-btn {
  1109. width: calc(100% / 4);
  1110. display: flex;
  1111. flex-direction: column;
  1112. align-items: center;
  1113. font-family:
  1114. PingFang SC,
  1115. PingFang SC;
  1116. font-weight: 400;
  1117. font-size: 12px;
  1118. color: #000000;
  1119. .tool-icon {
  1120. width: 56px;
  1121. height: 56px;
  1122. margin-bottom: 4px;
  1123. }
  1124. }
  1125. .emoji-item{
  1126. font-size: 20px;
  1127. margin: 0 4px;
  1128. }
  1129. }
  1130. .app-box::-webkit-scrollbar {
  1131. width: 0;
  1132. }
  1133. .page-foot {
  1134. position: relative;
  1135. z-index: 10; /* 确保输入框在上层 */
  1136. }
  1137. .local-video {
  1138. position: absolute;
  1139. bottom: 20px;
  1140. right: 20px;
  1141. width: 200px;
  1142. height: auto;
  1143. border: 2px solid white;
  1144. border-radius: 8px;
  1145. }
  1146. .remote-video {
  1147. width: 100%;
  1148. height: 100vh;
  1149. background: black;
  1150. object-fit: cover;
  1151. }
  1152. /* 按住说话按钮 */
  1153. .hold-talk-btn {
  1154. flex: 1;
  1155. text-align: center;
  1156. background: #f5f5f5;
  1157. padding: 6px 16px;
  1158. border-radius: 17px;
  1159. color: #666;
  1160. font-weight: 500;
  1161. font-size: 15px;
  1162. user-select: none;
  1163. -webkit-user-select: none;
  1164. -webkit-touch-callout: none;
  1165. // margin: 0 12px;
  1166. line-height: 24px;
  1167. border: 1px solid #f5f5f5;
  1168. }
  1169. /* 录音中的浮层提示 */
  1170. .recording-toast {
  1171. position: fixed;
  1172. bottom: 80px;
  1173. left: 50%;
  1174. transform: translateX(-50%);
  1175. width: 160px;
  1176. min-height: 160px;
  1177. background: rgba(0, 0, 0, 0.85);
  1178. border-radius: 12px;
  1179. padding: 16px;
  1180. box-sizing: border-box;
  1181. text-align: center;
  1182. display: flex;
  1183. flex-direction: column;
  1184. align-items: center;
  1185. justify-content: center;
  1186. .mic-icon {
  1187. width: 60px;
  1188. height: 60px;
  1189. background: url("https://img.icons8.com/ios-filled/100/ffffff/microphone.png") no-repeat center center;
  1190. background-size: contain;
  1191. margin-bottom: 16px;
  1192. }
  1193. .send-msg {
  1194. color: #fff;
  1195. font-size: 14px;
  1196. }
  1197. .cancel-msg {
  1198. color: #ff4d4f;
  1199. font-size: 14px;
  1200. }
  1201. }
  1202. .groupNotice{
  1203. margin-top: 15px;
  1204. background-color: #fff;
  1205. height: 40px;
  1206. line-height: 40px;
  1207. font-family: PingFang SC, PingFang SC;
  1208. font-weight: 400;
  1209. font-size: 12px;
  1210. color: #000000;
  1211. padding: 0 16px;
  1212. box-sizing: border-box;
  1213. display: flex;
  1214. justify-content: space-between;
  1215. align-items: center;
  1216. text-align: center;
  1217. .m-ellipsis{
  1218. white-space: nowrap; /* 不换行 */
  1219. overflow: hidden; /* 超出隐藏 */
  1220. text-overflow: ellipsis; /* 超出显示省略号 */
  1221. }
  1222. .item-icon{
  1223. width: 20px;
  1224. height: 20px;
  1225. color: #969799;
  1226. }
  1227. }
  1228. .revoked-msg {
  1229. color: #999;
  1230. font-size: 12px;
  1231. font-style: italic;
  1232. text-align: center;
  1233. margin: 5px 0;
  1234. }
  1235. .burn-tag {
  1236. font-size: 10px;
  1237. color: red;
  1238. margin-left: 6px;
  1239. }
  1240. .assign{
  1241. margin-bottom: 20px;
  1242. display: flex;
  1243. justify-content: flex-end;
  1244. .assign-text{
  1245. color: #fff;
  1246. background: #4765dd;
  1247. border-radius: 30px 0 0 30px;
  1248. padding: 3px 10px;
  1249. }
  1250. }
  1251. // 引用样式
  1252. .quote-box {
  1253. display: flex;
  1254. align-items: center;
  1255. justify-content: space-between;
  1256. background: #f2f2f2;
  1257. border-radius: 6px;
  1258. padding: 4px 16px;
  1259. // margin-bottom: 6px;
  1260. font-size: 14px;
  1261. color: #969696;
  1262. font-size: 12px;
  1263. }
  1264. .quote-content {
  1265. margin-bottom: 2px;
  1266. }
  1267. .quotation{
  1268. background: #f2f2f2;
  1269. border-radius: 5px;
  1270. padding: 5px 10px;
  1271. margin-top: 5px;
  1272. }
  1273. // 阅后即焚样式
  1274. .burn-message {
  1275. background: #fff;
  1276. color: #000;
  1277. border-radius: 10px;
  1278. margin-top: 8px;
  1279. padding: 8px 14px;
  1280. max-width: 100%;
  1281. font-size: 15px;
  1282. position: relative;
  1283. cursor: pointer;
  1284. word-break: break-word;
  1285. // white-space: pre-wrap;
  1286. transition: all 0.3s ease;
  1287. &.self {
  1288. background: #4d71ff;
  1289. color: #fff;
  1290. }
  1291. .burn-mask {
  1292. text-align: center;
  1293. color: #999;
  1294. font-size: 14px;
  1295. filter: blur(0px);
  1296. }
  1297. .burn-content,.burn-audio {
  1298. // animation: fadeIn 0.3s ease-in;
  1299. position: relative;
  1300. }
  1301. .burn-countdown {
  1302. position: absolute;
  1303. font-size: 12px;
  1304. color: #ff5b5b;
  1305. margin-left: 6px;
  1306. top: -15px;
  1307. right: -20px;
  1308. }
  1309. .burn-destroyed {
  1310. color: #999;
  1311. font-size: 13px;
  1312. text-align: center;
  1313. // animation: fadeOut 10s forwards;
  1314. }
  1315. }
  1316. // @keyframes fadeIn {
  1317. // from {
  1318. // opacity: 0;
  1319. // transform: scale(0.95);
  1320. // }
  1321. // to {
  1322. // opacity: 1;
  1323. // transform: scale(1);
  1324. // }
  1325. // }
  1326. // @keyframes fadeOut {
  1327. // from {
  1328. // opacity: 1;
  1329. // }
  1330. // to {
  1331. // opacity: 0;
  1332. // height: 0;
  1333. // }
  1334. // }
  1335. </style>