uniapp,h5

chat.vue 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494
  1. <template>
  2. <view class="page">
  3. <!-- 头部 -->
  4. <view class="header">
  5. <view class="back-button" @click="goBack()">
  6. <image src="/static/img/tabbar/out.png" alt="Back" />
  7. </view>
  8. <view class="title">
  9. {{ chaterName }}
  10. </view>
  11. </view>
  12. <!-- 消息展示区 -->
  13. <view class="message-area">
  14. <scroll-view :scroll-y="true" :scroll-top="scrollPosition.top" @scroll="handleScroll" ref="msgList">
  15. <view v-for="(message, index) in messages" :key="index"
  16. :class="['message', message.isMe ? 'me' : 'other']">
  17. <!-- 头像 (对方发送的消息) -->
  18. <view v-if="!message.isMe" class="avatar other-avatar">
  19. <image :src="message.avatar" alt="Avatar" />
  20. </view>
  21. <!-- 用户名和时间 -->
  22. <view class="username-time">
  23. <view v-if="!message.isMe" class="username">{{ message.username }}</view>
  24. <view v-if="!message.isMe" class="time">{{ message.time }}</view>
  25. <view v-if="message.isMe" class="username">{{ message.username }}</view>
  26. <view v-if="message.isMe" class="time">{{ message.time }}</view>
  27. <view class="message-content">
  28. <text>{{ message.content }}</text>
  29. </view>
  30. </view>
  31. <!-- 头像 (自己发送的消息) -->
  32. <view v-if="message.isMe" class="avatar me-avatar">
  33. <image :src="message.avatar" alt="Avatar" />
  34. </view>
  35. </view>
  36. </scroll-view>
  37. </view>
  38. <!-- 底部输入和工具栏 -->
  39. <view class="footer">
  40. <!-- 输入框 -->
  41. <view class="input-wrap">
  42. <input v-model="inputContent" type="text" :focus="isInputFocused" @confirm="sendConsultation" />
  43. </view>
  44. <!-- 工具栏 -->
  45. <view class="tools-wrap">
  46. <image src="/static/img/tabbar/guanzhu.png" @tap="openTools('emoji')" />
  47. <image src="/static/img/tabbar/guanzhu.png" @tap="openTools('plus')" />
  48. </view>
  49. <!-- 表情选择器 -->
  50. <view v-if="showEmojiPicker" class="emoji-picker-wrap">
  51. <tools ref="tools" />
  52. </view>
  53. <!-- 发送按钮 -->
  54. <view class="send-button">
  55. <button @click="sendConsultation">
  56. 发送
  57. </button>
  58. </view>
  59. </view>
  60. </view>
  61. </template>
  62. <script>
  63. import Tools from './Tools.vue'; // 引入工具栏组件
  64. import io from 'socket.io-client';
  65. export default {
  66. components: {
  67. Tools
  68. },
  69. data() {
  70. return {
  71. messages: [],
  72. scrollPosition: {
  73. top: 0
  74. },
  75. inputContent: '',
  76. isInputFocused: false,
  77. showEmojiPicker: false,
  78. meUserInfo: {
  79. avatar: '',
  80. username: '',
  81. userId: ''
  82. },
  83. otherUserInfo: {
  84. avatar: '',
  85. username: '',
  86. userId: ''
  87. },
  88. chaterId: '',
  89. chaterName: ''
  90. };
  91. },
  92. onLoad() {
  93. // 加载时的初始化
  94. const userInfo = uni.getStorageSync('userInfo');
  95. if (userInfo) {
  96. this.meUserInfo = {
  97. ...userInfo
  98. };
  99. }
  100. const chaterId = uni.getStorageSync('chaterId');
  101. const chaterName = uni.getStorageSync('chaterName');
  102. if (chaterId && chaterName) {
  103. this.chaterName = chaterName;
  104. this.chaterId = chaterId;
  105. // 添加这一行:强制更新,确保 created 中能访问到最新数据
  106. this.$nextTick(() => {});
  107. }
  108. },
  109. onShow: async function() {
  110. if (this.chaterId == "000") {
  111. try {
  112. await this.fetchMsgInfoAll();
  113. } catch (error) {
  114. console.error('Failed to load msg info all:', error);
  115. }
  116. return;
  117. } else {
  118. try {
  119. await this.fetchMsgInfoOne();
  120. } catch (error) {
  121. console.error('Failed to load msg info one:', error);
  122. }
  123. return;
  124. }
  125. },
  126. created() {
  127. this.socket = io('https://afanai.top:8089');
  128. // 确保在数据完全就绪后再继续执行
  129. this.$nextTick(() => {
  130. if (this.chaterId !== undefined) {
  131. // console.log(this.chaterId);
  132. if (this.chaterId === "000") {
  133. this.socket.on('responseMsg_all', this.handleResponseMsg);
  134. } else {
  135. this.socket.on('responseMsg_one2one', this.handleResponseMsg);
  136. }
  137. } else {
  138. console.warn("chaterId has not been set yet.");
  139. }
  140. });
  141. },
  142. destroyed() {
  143. if (this.chaterId == "000") {
  144. this.socket.off('responseMsg_all', this.handleResponseMsg);
  145. } else {
  146. this.socket.off('responseMsg_one2one', this.handleResponseMsg);
  147. }
  148. this.socket.disconnect();
  149. },
  150. methods: {
  151. handleScroll(event) {
  152. // console.log(event.detail);
  153. this.scrollPosition.top = event.detail.scrollTop;
  154. },
  155. sendConsultation() {
  156. // 发送咨询逻辑
  157. this.inputContent = (this.inputContent == '') ? "..." : this.inputContent;
  158. const date = this.formatDate(new Date(), "yyyy-MM-dd hh:mm:ss");
  159. // console.log(date);
  160. const newMessage = {
  161. isMe: true,
  162. avatar: this.meUserInfo.avatar,
  163. content: this.inputContent,
  164. username: this.meUserInfo.username,
  165. time: date
  166. };
  167. this.messages.push(newMessage);
  168. // 发送消息
  169. const roomId = (this.chaterId == "000") ? 'all' : 'one2one';
  170. const sendUserId = this.meUserInfo.userId;
  171. const msg2server = {
  172. roomId: roomId,
  173. sendUserId: sendUserId,
  174. resUserId: this.chaterId,
  175. content: newMessage.content,
  176. time: date,
  177. avatar: this.meUserInfo.avatar,
  178. username: this.meUserInfo.username,
  179. }
  180. this.socket.emit('message', msg2server);
  181. this.inputContent = ''; // 清空输入框
  182. this.scrollToBottom();
  183. },
  184. handleResponseMsg(rspData) {
  185. // console.log("收到消息:", rspData.sendUserId);
  186. const isMeSned = rspData.sendUserId == this.meUserInfo.userId;
  187. if (isMeSned) {
  188. return;
  189. }
  190. const newMessage = {
  191. isMe: isMeSned,
  192. avatar: rspData.avatar,
  193. content: rspData.content,
  194. username: rspData.username,
  195. time: rspData.time
  196. };
  197. this.messages.push(newMessage);
  198. },
  199. openTools(type) {
  200. if (type === 'emoji') {
  201. this.showEmojiPicker = true;
  202. this.$nextTick(() => {
  203. this.$refs.tools.showEmojiPicker();
  204. });
  205. } else if (type === 'plus') {
  206. this.$refs.tools.showPlusMenu();
  207. }
  208. },
  209. scrollToBottom() {
  210. // console.log(this.$refs.msgList);
  211. // 滚动到底部的逻辑
  212. this.scrollPosition.top = this.$refs.msgList.scrollHeight;
  213. },
  214. goBack() {
  215. uni.navigateBack();
  216. },
  217. formatDate(date, fmt) {
  218. var o = {
  219. "M+": date.getMonth() + 1, //月份
  220. "d+": date.getDate(), //日
  221. "h+": date.getHours(), //小时
  222. "m+": date.getMinutes(), //分
  223. "s+": date.getSeconds(), //秒
  224. "q+": Math.floor((date.getMonth() + 3) / 3), //季度
  225. "S": date.getMilliseconds() //毫秒
  226. };
  227. if (/(y+)/.test(fmt)) fmt = fmt.replace(RegExp.$1, (date.getFullYear() + "").substr(4 - RegExp.$1.length));
  228. for (var k in o)
  229. if (new RegExp("(" + k + ")").test(fmt)) fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[
  230. k]) : (("00" + o[k]).substr(("" + o[k]).length)));
  231. return fmt;
  232. },
  233. async fetchMsgInfoAll() {
  234. const response = await uni.request({
  235. url: 'https://afanai.top:8089/v1/chatMsg/get/all',
  236. method: 'GET',
  237. });
  238. this.InitMsgList(response);
  239. },
  240. async fetchMsgInfoOne() {
  241. const response = await uni.request({
  242. url: 'https://afanai.top:8089/v1/chatMsg/get/one2one/' + this.meUserInfo.userId + '/' +
  243. this.chaterId,
  244. method: 'GET',
  245. });
  246. this.InitMsgList(response);
  247. },
  248. InitMsgList(response) {
  249. // console.log(response.data);
  250. this.messages = [];
  251. for (var obj of response.data) {
  252. // console.log(obj);
  253. const isMeSned = obj.sendUserId == this.meUserInfo.userId;
  254. const newMessage = {
  255. isMe: isMeSned,
  256. avatar: obj.avatar,
  257. content: obj.content,
  258. username: obj.username,
  259. time: obj.time
  260. };
  261. this.messages.push(newMessage);
  262. }
  263. }
  264. },
  265. };
  266. </script>
  267. <!-- 使用Flex布局和定位来实现 -->
  268. <style scoped>
  269. .header {
  270. display: flex;
  271. align-items: center;
  272. justify-content: space-between;
  273. padding: 10rpx 20rpx;
  274. background-color: #fff;
  275. box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  276. position: fixed;
  277. /* 添加 */
  278. top: 40px;
  279. /* 添加 */
  280. left: 0;
  281. right: 0;
  282. z-index: 1;
  283. /* 确保头部在内容上方 */
  284. }
  285. .back-button {
  286. width: 40upx;
  287. /* 图标大小 */
  288. height: 40upx;
  289. }
  290. .back-button image {
  291. width: 100%;
  292. height: 100%;
  293. }
  294. .title {
  295. font-size: 36rpx;
  296. font-weight: bold;
  297. margin: 0 auto;
  298. }
  299. .page {
  300. display: flex;
  301. flex-direction: column;
  302. min-height: 100vh;
  303. }
  304. /* 消息展示区的样式 */
  305. .message-area {
  306. flex: 1;
  307. overflow-y: auto;
  308. padding: 20px;
  309. padding-bottom: 110px;
  310. padding-top: 40px;
  311. }
  312. .message {
  313. display: flex;
  314. margin: 10px 0;
  315. }
  316. /* 确保消息内容向左或向右对齐 */
  317. .me {
  318. justify-content: flex-end;
  319. }
  320. .other {
  321. justify-content: flex-start;
  322. }
  323. /* 头像的样式 */
  324. .avatar {
  325. border-radius: 20%;
  326. margin-top: 18rpx;
  327. }
  328. .other-avatar {
  329. margin-right: 10rpx;
  330. /* 为非当前用户的消息增加右侧边距 */
  331. }
  332. .me-avatar {
  333. margin-left: 10rpx;
  334. /* 为当前用户的消息增加左侧边距 */
  335. }
  336. /* 限制头像的大小 */
  337. .avatar image {
  338. width: 80upx;
  339. height: 80upx;
  340. border-radius: 12rpx;
  341. border: 2upx solid #abb0b6;
  342. object-fit: cover;
  343. }
  344. .username-time {
  345. display: flex;
  346. flex-direction: column;
  347. align-items: flex-start;
  348. top: 15rpx;
  349. }
  350. .username-time-p {
  351. display: flex;
  352. flex-direction: row;
  353. align-items: flex-start;
  354. gap: 15rpx;
  355. margin-left: 10rpx;
  356. margin-bottom: 2rpx;
  357. }
  358. .me .username-time-p {
  359. margin-right: 10rpx;
  360. }
  361. .me .username-time {
  362. align-items: flex-end;
  363. }
  364. .username {
  365. font-size: 24rpx;
  366. font-weight: bold;
  367. color: #666;
  368. }
  369. .time {
  370. font-size: 20rpx;
  371. color: #999;
  372. }
  373. /* 消息内容的样式 */
  374. .message-content {
  375. background-color: #E0E0E0;
  376. padding: 10px;
  377. border-radius: 10px;
  378. box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
  379. margin: 0 10px;
  380. position: relative;
  381. max-width: 75%;
  382. word-wrap: break-word;
  383. }
  384. /* 自己发送消息的样式 */
  385. .me .message-content {
  386. background-color: #64B5F6;
  387. color: #fff;
  388. }
  389. /* 为气泡效果添加三角形 */
  390. .message-content::before {
  391. content: "";
  392. position: absolute;
  393. width: 0;
  394. height: 0;
  395. border-style: solid;
  396. }
  397. .me .message-content::before {
  398. left: 100%;
  399. margin-left: -1upx;
  400. border-width: 12upx 0 12upx 12upx;
  401. border-color: transparent transparent transparent #64B5F6;
  402. top: 15rpx;
  403. margin-bottom: -5px;
  404. }
  405. .other .message-content::before {
  406. right: 100%;
  407. margin-right: -1upx;
  408. border-width: 12upx 12upx 12upx 0;
  409. border-color: transparent #E0E0E0 transparent transparent;
  410. top: 15rpx;
  411. margin-bottom: -5px;
  412. }
  413. /* 底部工具栏样式 */
  414. .footer {
  415. display: flex;
  416. align-items: center;
  417. justify-content: space-between;
  418. /* 新增,使工具图标与输入框/发送按钮之间保持均匀间隔 */
  419. padding: 10px;
  420. background-color: #f8f8f8;
  421. position: fixed;
  422. bottom: 0;
  423. left: 0;
  424. right: 0;
  425. box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);
  426. z-index: 2;
  427. }
  428. .input-wrap {
  429. flex: 1;
  430. }
  431. .send-button {
  432. margin-left: 10px;
  433. }
  434. /* 新增对工具图标的样式 */
  435. .tools-wrap {
  436. display: flex;
  437. align-items: center;
  438. /* 使图标上下居中 */
  439. }
  440. .tools-wrap image {
  441. width: 24px;
  442. /* 调整图标大小 */
  443. height: 24px;
  444. margin: 0 8px;
  445. /* 图标之间的间距 */
  446. }
  447. </style>