瀏覽代碼

Merge branch 'dev' of http://1.94.207.143:3000/xyy/xyy-web into dev

# Conflicts:
#	.env.development
#	src/components/Profile/Home/Tabs.vue
songzhen 1 月之前
父節點
當前提交
f65826d40a
共有 88 個文件被更改,包括 6578 次插入89 次删除
  1. 7 3
      nuxt.config.ts
  2. 1 0
      package.json
  3. 8 14
      pnpm-lock.yaml
  4. 3 0
      server/api/getGroupTypeList.js
  5. 二進制
      src/assets/audio/message.mp3
  6. 655 11
      src/assets/iconfont/demo_index.html
  7. 117 5
      src/assets/iconfont/iconfont.css
  8. 0 0
      src/assets/iconfont/iconfont.js
  9. 198 2
      src/assets/iconfont/iconfont.json
  10. 57 1
      src/assets/iconfont/iconfont.svg
  11. 二進制
      src/assets/iconfont/iconfont.ttf
  12. 二進制
      src/assets/iconfont/iconfont.woff
  13. 二進制
      src/assets/iconfont/iconfont.woff2
  14. 二進制
      src/assets/img/chat/add.png
  15. 二進制
      src/assets/img/chat/close.png
  16. 二進制
      src/assets/img/chat/downArrow.png
  17. 二進制
      src/assets/img/chat/downArrowGray.png
  18. 二進制
      src/assets/img/chat/downArrowWhite.png
  19. 二進制
      src/assets/img/chat/emoji_icon.png
  20. 二進制
      src/assets/img/chat/loading.gif
  21. 二進制
      src/assets/img/chat/message-search.png
  22. 二進制
      src/assets/img/chat/more.png
  23. 二進制
      src/assets/img/chat/no-chat.png
  24. 二進制
      src/assets/img/chat/pic_icon.png
  25. 二進制
      src/assets/img/chat/qrCode.png
  26. 二進制
      src/assets/img/chat/radio.png
  27. 二進制
      src/assets/img/chat/radio2.png
  28. 二進制
      src/assets/img/chat/radio_circle_gray.png
  29. 二進制
      src/assets/img/chat/radio_circle_orange.png
  30. 二進制
      src/assets/img/chat/reduce.png
  31. 二進制
      src/assets/img/chat/type1.png
  32. 二進制
      src/assets/img/chat/type2.png
  33. 二進制
      src/assets/img/chat/type3.png
  34. 二進制
      src/assets/img/chat/type4.png
  35. 二進制
      src/assets/img/chat/type5.png
  36. 二進制
      src/assets/img/chat/type6.png
  37. 二進制
      src/assets/img/chat/type7.png
  38. 二進制
      src/assets/img/chat/type8.png
  39. 二進制
      src/assets/img/chat/type9.png
  40. 二進制
      src/assets/img/profile/profile_my_bg.png
  41. 二進制
      src/assets/img/profile/profile_my_boy.png
  42. 二進制
      src/assets/img/profile/profile_my_girl.png
  43. 二進制
      src/assets/img/search.png
  44. 29 0
      src/assets/style/common.scss
  45. 8 1
      src/assets/style/element.scss
  46. 202 0
      src/components/MultiHeader/index.vue
  47. 16 2
      src/components/NavBar/Login.client.vue
  48. 15 5
      src/components/Profile/Home/Tabs.vue
  49. 67 0
      src/components/XDialog/index.vue
  50. 8 0
      src/middleware/01.intercept-components.global.js
  51. 78 0
      src/pages/chat/components/ActiveMessage.vue
  52. 141 0
      src/pages/chat/components/ApplyJoinGroup.vue
  53. 99 0
      src/pages/chat/components/ApplyJoinList.vue
  54. 538 0
      src/pages/chat/components/Chat.vue
  55. 156 0
      src/pages/chat/components/ChatHistory.vue
  56. 168 0
      src/pages/chat/components/ChatList.vue
  57. 202 0
      src/pages/chat/components/Complain.vue
  58. 47 0
      src/pages/chat/components/ContextMenu.vue
  59. 388 0
      src/pages/chat/components/CreateGroup.vue
  60. 309 0
      src/pages/chat/components/FansList.vue
  61. 105 0
      src/pages/chat/components/FriendInfo.vue
  62. 77 0
      src/pages/chat/components/GroupInfo.vue
  63. 52 0
      src/pages/chat/components/GroupIntro.vue
  64. 72 0
      src/pages/chat/components/GroupQrCode.vue
  65. 365 0
      src/pages/chat/components/GroupSetting.vue
  66. 210 0
      src/pages/chat/components/GroupSquare.vue
  67. 82 0
      src/pages/chat/components/GroupTips.vue
  68. 156 0
      src/pages/chat/components/InviteGroup.vue
  69. 16 0
      src/pages/chat/components/MsgStatus.vue
  70. 119 0
      src/pages/chat/components/NewFans.vue
  71. 154 0
      src/pages/chat/components/RemoveMember.vue
  72. 174 0
      src/pages/chat/components/SetGroupType.vue
  73. 60 0
      src/pages/chat/components/SystemMessage.vue
  74. 89 0
      src/pages/chat/components/TextMsg.vue
  75. 8 0
      src/pages/chat/const.js
  76. 108 0
      src/pages/chat/emoji.js
  77. 7 0
      src/pages/chat/followSta.js
  78. 152 0
      src/pages/chat/friends_mock.js
  79. 30 0
      src/pages/chat/groupTypeList.js
  80. 236 0
      src/pages/chat/index.vue
  81. 169 0
      src/pages/profile/components/followModal/followList.vue
  82. 73 0
      src/pages/profile/components/followModal/index.vue
  83. 221 37
      src/pages/profile/index.client.vue
  84. 8 2
      src/pages/profile/userinfo/index.vue
  85. 20 0
      src/plugins/websocket.js
  86. 237 0
      src/stores/chat.js
  87. 43 6
      src/utils/index.js
  88. 18 0
      src/utils/request.js

+ 7 - 3
nuxt.config.ts

@@ -7,7 +7,10 @@ export default defineNuxtConfig({
   srcDir: 'src',
   compatibilityDate: '2024-04-03',
   devtools: { enabled: true },
-  css: ['./src/assets/iconfont/iconfont.css'],
+  css: [
+    './src/assets/iconfont/iconfont.css',
+    './src/assets/style/common.scss'
+  ],
   sourcemap: {
     server: true,
     client: true
@@ -44,7 +47,8 @@ export default defineNuxtConfig({
   },
   runtimeConfig: {
     public: {
-      baseApi: process.env.VITE_APP_BASE_URL
+      baseApi: process.env.VITE_APP_BASE_URL,
+      baseIM:process.env.VITE_APP_IM_URL
       // baseApi: '1234123'
     }
   },
@@ -75,7 +79,7 @@ export default defineNuxtConfig({
     css: {
       preprocessorOptions: {
         scss: {
-          additionalData: '@use "@/assets/style/element.scss";'
+          additionalData: '@use "@/assets/style/element.scss";',
         }
       }
     },

+ 1 - 0
package.json

@@ -17,6 +17,7 @@
     "@popperjs/core": "^2.11.8",
     "@vueuse/core": "^11.1.0",
     "@vueuse/nuxt": "^11.1.0",
+    "accounting": "^0.4.1",
     "dayjs": "^1.11.13",
     "dayjs-nuxt": "^2.1.11",
     "lodash-es": "^4.17.21",

+ 8 - 14
pnpm-lock.yaml

@@ -20,6 +20,9 @@ importers:
       '@vueuse/nuxt':
         specifier: ^11.1.0
         version: 11.1.0(magicast@0.3.5)(nuxt@3.13.2(@parcel/watcher@2.4.1)(@types/node@22.7.3)(ioredis@5.4.1)(magicast@0.3.5)(rollup@4.22.4)(sass@1.79.3)(terser@5.33.0)(vite@5.4.7(@types/node@22.7.3)(sass@1.79.3)(terser@5.33.0))(webpack-sources@3.2.3))(rollup@4.22.4)(vue@3.5.8)(webpack-sources@3.2.3)
+      accounting:
+        specifier: ^0.4.1
+        version: 0.4.1
       dayjs:
         specifier: ^1.11.13
         version: 1.11.13
@@ -985,35 +988,30 @@ packages:
     engines: {node: '>= 10.0.0'}
     cpu: [arm]
     os: [linux]
-    libc: [glibc]
 
   '@parcel/watcher-linux-arm64-glibc@2.4.1':
     resolution: {integrity: sha512-BJ7mH985OADVLpbrzCLgrJ3TOpiZggE9FMblfO65PlOCdG++xJpKUJ0Aol74ZUIYfb8WsRlUdgrZxKkz3zXWYA==}
     engines: {node: '>= 10.0.0'}
     cpu: [arm64]
     os: [linux]
-    libc: [glibc]
 
   '@parcel/watcher-linux-arm64-musl@2.4.1':
     resolution: {integrity: sha512-p4Xb7JGq3MLgAfYhslU2SjoV9G0kI0Xry0kuxeG/41UfpjHGOhv7UoUDAz/jb1u2elbhazy4rRBL8PegPJFBhA==}
     engines: {node: '>= 10.0.0'}
     cpu: [arm64]
     os: [linux]
-    libc: [musl]
 
   '@parcel/watcher-linux-x64-glibc@2.4.1':
     resolution: {integrity: sha512-s9O3fByZ/2pyYDPoLM6zt92yu6P4E39a03zvO0qCHOTjxmt3GHRMLuRZEWhWLASTMSrrnVNWdVI/+pUElJBBBg==}
     engines: {node: '>= 10.0.0'}
     cpu: [x64]
     os: [linux]
-    libc: [glibc]
 
   '@parcel/watcher-linux-x64-musl@2.4.1':
     resolution: {integrity: sha512-L2nZTYR1myLNST0O632g0Dx9LyMNHrn6TOt76sYxWLdff3cB22/GZX2UPtJnaqQPdCRoszoY5rcOj4oMTtp5fQ==}
     engines: {node: '>= 10.0.0'}
     cpu: [x64]
     os: [linux]
-    libc: [musl]
 
   '@parcel/watcher-wasm@2.4.1':
     resolution: {integrity: sha512-/ZR0RxqxU/xxDGzbzosMjh4W6NdYFMqq2nvo2b8SLi7rsl/4jkL8S5stIikorNkdR50oVDvqb/3JT05WM+CRRA==}
@@ -1165,55 +1163,46 @@ packages:
     resolution: {integrity: sha512-j63YtCIRAzbO+gC2L9dWXRh5BFetsv0j0va0Wi9epXDgU/XUi5dJKo4USTttVyK7fGw2nPWK0PbAvyliz50SCQ==}
     cpu: [arm]
     os: [linux]
-    libc: [glibc]
 
   '@rollup/rollup-linux-arm-musleabihf@4.22.4':
     resolution: {integrity: sha512-dJnWUgwWBX1YBRsuKKMOlXCzh2Wu1mlHzv20TpqEsfdZLb3WoJW2kIEsGwLkroYf24IrPAvOT/ZQ2OYMV6vlrg==}
     cpu: [arm]
     os: [linux]
-    libc: [musl]
 
   '@rollup/rollup-linux-arm64-gnu@4.22.4':
     resolution: {integrity: sha512-AdPRoNi3NKVLolCN/Sp4F4N1d98c4SBnHMKoLuiG6RXgoZ4sllseuGioszumnPGmPM2O7qaAX/IJdeDU8f26Aw==}
     cpu: [arm64]
     os: [linux]
-    libc: [glibc]
 
   '@rollup/rollup-linux-arm64-musl@4.22.4':
     resolution: {integrity: sha512-Gl0AxBtDg8uoAn5CCqQDMqAx22Wx22pjDOjBdmG0VIWX3qUBHzYmOKh8KXHL4UpogfJ14G4wk16EQogF+v8hmA==}
     cpu: [arm64]
     os: [linux]
-    libc: [musl]
 
   '@rollup/rollup-linux-powerpc64le-gnu@4.22.4':
     resolution: {integrity: sha512-3aVCK9xfWW1oGQpTsYJJPF6bfpWfhbRnhdlyhak2ZiyFLDaayz0EP5j9V1RVLAAxlmWKTDfS9wyRyY3hvhPoOg==}
     cpu: [ppc64]
     os: [linux]
-    libc: [glibc]
 
   '@rollup/rollup-linux-riscv64-gnu@4.22.4':
     resolution: {integrity: sha512-ePYIir6VYnhgv2C5Xe9u+ico4t8sZWXschR6fMgoPUK31yQu7hTEJb7bCqivHECwIClJfKgE7zYsh1qTP3WHUA==}
     cpu: [riscv64]
     os: [linux]
-    libc: [glibc]
 
   '@rollup/rollup-linux-s390x-gnu@4.22.4':
     resolution: {integrity: sha512-GqFJ9wLlbB9daxhVlrTe61vJtEY99/xB3C8e4ULVsVfflcpmR6c8UZXjtkMA6FhNONhj2eA5Tk9uAVw5orEs4Q==}
     cpu: [s390x]
     os: [linux]
-    libc: [glibc]
 
   '@rollup/rollup-linux-x64-gnu@4.22.4':
     resolution: {integrity: sha512-87v0ol2sH9GE3cLQLNEy0K/R0pz1nvg76o8M5nhMR0+Q+BBGLnb35P0fVz4CQxHYXaAOhE8HhlkaZfsdUOlHwg==}
     cpu: [x64]
     os: [linux]
-    libc: [glibc]
 
   '@rollup/rollup-linux-x64-musl@4.22.4':
     resolution: {integrity: sha512-UV6FZMUgePDZrFjrNGIWzDo/vABebuXBhJEqrHxrGiU6HikPy0Z3LfdtciIttEUQfuDdCn8fqh7wiFJjCNwO+g==}
     cpu: [x64]
     os: [linux]
-    libc: [musl]
 
   '@rollup/rollup-win32-arm64-msvc@4.22.4':
     resolution: {integrity: sha512-BjI+NVVEGAXjGWYHz/vv0pBqfGoUH0IGZ0cICTn7kB9PyjrATSkX+8WkguNjWoj2qSr1im/+tTGRaY+4/PdcQw==}
@@ -1418,6 +1407,9 @@ packages:
     resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
     engines: {node: '>= 0.6'}
 
+  accounting@0.4.1:
+    resolution: {integrity: sha512-RU6KY9Y5wllyaCNBo1W11ZOTnTHMMgOZkIwdOOs6W5ibMTp72i4xIbEA48djxVGqMNTUNbvrP/1nWg5Af5m2gQ==, tarball: https://registry.npmmirror.com/accounting/-/accounting-0.4.1.tgz}
+
   acorn-import-attributes@1.9.5:
     resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==}
     peerDependencies:
@@ -5687,6 +5679,8 @@ snapshots:
       mime-types: 2.1.35
       negotiator: 0.6.3
 
+  accounting@0.4.1: {}
+
   acorn-import-attributes@1.9.5(acorn@8.12.1):
     dependencies:
       acorn: 8.12.1

+ 3 - 0
server/api/getGroupTypeList.js

@@ -0,0 +1,3 @@
+export default defineEventHandler(async (event) => {
+    
+})

二進制
src/assets/audio/message.mp3


+ 655 - 11
src/assets/iconfont/demo_index.html

@@ -55,6 +55,174 @@
           <ul class="icon_lists dib-box">
           
             <li class="dib">
+              <span class="icon iconfont">&#xe7d6;</span>
+                <div class="name">star</div>
+                <div class="code-name">&amp;#xe7d6;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe7d7;</span>
+                <div class="name">edit-1</div>
+                <div class="code-name">&amp;#xe7d7;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe7d8;</span>
+                <div class="name">logout</div>
+                <div class="code-name">&amp;#xe7d8;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe7d9;</span>
+                <div class="name">chat-message</div>
+                <div class="code-name">&amp;#xe7d9;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe7da;</span>
+                <div class="name">chevron-down</div>
+                <div class="code-name">&amp;#xe7da;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe7db;</span>
+                <div class="name">caret-down-small</div>
+                <div class="code-name">&amp;#xe7db;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe7dc;</span>
+                <div class="name">cart</div>
+                <div class="code-name">&amp;#xe7dc;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe7d5;</span>
+                <div class="name">delete-three</div>
+                <div class="code-name">&amp;#xe7d5;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe7d3;</span>
+                <div class="name">copy</div>
+                <div class="code-name">&amp;#xe7d3;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe7d4;</span>
+                <div class="name">quote</div>
+                <div class="code-name">&amp;#xe7d4;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe7d2;</span>
+                <div class="name">send</div>
+                <div class="code-name">&amp;#xe7d2;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe7cd;</span>
+                <div class="name">comment-two</div>
+                <div class="code-name">&amp;#xe7cd;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe7ce;</span>
+                <div class="name">eit</div>
+                <div class="code-name">&amp;#xe7ce;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe7cf;</span>
+                <div class="name">comments</div>
+                <div class="code-name">&amp;#xe7cf;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe7d0;</span>
+                <div class="name">comment-one</div>
+                <div class="code-name">&amp;#xe7d0;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe7d1;</span>
+                <div class="name">like</div>
+                <div class="code-name">&amp;#xe7d1;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe7c7;</span>
+                <div class="name">set-top</div>
+                <div class="code-name">&amp;#xe7c7;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe7c6;</span>
+                <div class="name">pic-two</div>
+                <div class="code-name">&amp;#xe7c6;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe7c8;</span>
+                <div class="name">close-remind</div>
+                <div class="code-name">&amp;#xe7c8;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe7c9;</span>
+                <div class="name">log</div>
+                <div class="code-name">&amp;#xe7c9;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe7ca;</span>
+                <div class="name">jubaoguanli</div>
+                <div class="code-name">&amp;#xe7ca;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe7cb;</span>
+                <div class="name">setting</div>
+                <div class="code-name">&amp;#xe7cb;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe7cc;</span>
+                <div class="name">delete-one</div>
+                <div class="code-name">&amp;#xe7cc;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe7c0;</span>
+                <div class="name">close-one</div>
+                <div class="code-name">&amp;#xe7c0;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe7c1;</span>
+                <div class="name">slightly-smiling-face</div>
+                <div class="code-name">&amp;#xe7c1;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe7c2;</span>
+                <div class="name">pic</div>
+                <div class="code-name">&amp;#xe7c2;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe7c4;</span>
+                <div class="name">voice-one</div>
+                <div class="code-name">&amp;#xe7c4;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe7c5;</span>
+                <div class="name">peoples-two</div>
+                <div class="code-name">&amp;#xe7c5;</div>
+              </li>
+          
+            <li class="dib">
               <span class="icon iconfont">&#xe8ed;</span>
                 <div class="name">caret-up</div>
                 <div class="code-name">&amp;#xe8ed;</div>
@@ -122,7 +290,7 @@
           
             <li class="dib">
               <span class="icon iconfont">&#xe7c3;</span>
-                <div class="name">delete</div>
+                <div class="name">delete-two</div>
                 <div class="code-name">&amp;#xe7c3;</div>
               </li>
           
@@ -210,10 +378,10 @@
 <pre><code class="language-css"
 >@font-face {
   font-family: 'iconfont';
-  src: url('iconfont.woff2?t=1736217339971') format('woff2'),
-       url('iconfont.woff?t=1736217339971') format('woff'),
-       url('iconfont.ttf?t=1736217339971') format('truetype'),
-       url('iconfont.svg?t=1736217339971#iconfont') format('svg');
+  src: url('iconfont.woff2?t=1736823174249') format('woff2'),
+       url('iconfont.woff?t=1736823174249') format('woff'),
+       url('iconfont.ttf?t=1736823174249') format('truetype'),
+       url('iconfont.svg?t=1736823174249#iconfont') format('svg');
 }
 </code></pre>
           <h3 id="-iconfont-">第二步:定义使用 iconfont 的样式</h3>
@@ -240,6 +408,258 @@
         <ul class="icon_lists dib-box">
           
           <li class="dib">
+            <span class="icon iconfont icon-star1"></span>
+            <div class="name">
+              star
+            </div>
+            <div class="code-name">.icon-star1
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-edit-1"></span>
+            <div class="name">
+              edit-1
+            </div>
+            <div class="code-name">.icon-edit-1
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-logout"></span>
+            <div class="name">
+              logout
+            </div>
+            <div class="code-name">.icon-logout
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-chat-message"></span>
+            <div class="name">
+              chat-message
+            </div>
+            <div class="code-name">.icon-chat-message
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-chevron-down"></span>
+            <div class="name">
+              chevron-down
+            </div>
+            <div class="code-name">.icon-chevron-down
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-caret-down-small"></span>
+            <div class="name">
+              caret-down-small
+            </div>
+            <div class="code-name">.icon-caret-down-small
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-cart"></span>
+            <div class="name">
+              cart
+            </div>
+            <div class="code-name">.icon-cart
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-delete-three"></span>
+            <div class="name">
+              delete-three
+            </div>
+            <div class="code-name">.icon-delete-three
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-copy"></span>
+            <div class="name">
+              copy
+            </div>
+            <div class="code-name">.icon-copy
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-quote"></span>
+            <div class="name">
+              quote
+            </div>
+            <div class="code-name">.icon-quote
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-send"></span>
+            <div class="name">
+              send
+            </div>
+            <div class="code-name">.icon-send
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-comment-two"></span>
+            <div class="name">
+              comment-two
+            </div>
+            <div class="code-name">.icon-comment-two
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-eit"></span>
+            <div class="name">
+              eit
+            </div>
+            <div class="code-name">.icon-eit
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-comments"></span>
+            <div class="name">
+              comments
+            </div>
+            <div class="code-name">.icon-comments
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-comment-one"></span>
+            <div class="name">
+              comment-one
+            </div>
+            <div class="code-name">.icon-comment-one
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-like"></span>
+            <div class="name">
+              like
+            </div>
+            <div class="code-name">.icon-like
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-set-top"></span>
+            <div class="name">
+              set-top
+            </div>
+            <div class="code-name">.icon-set-top
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-pic-two"></span>
+            <div class="name">
+              pic-two
+            </div>
+            <div class="code-name">.icon-pic-two
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-close-remind"></span>
+            <div class="name">
+              close-remind
+            </div>
+            <div class="code-name">.icon-close-remind
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-log"></span>
+            <div class="name">
+              log
+            </div>
+            <div class="code-name">.icon-log
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-jubaoguanli"></span>
+            <div class="name">
+              jubaoguanli
+            </div>
+            <div class="code-name">.icon-jubaoguanli
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-setting-one"></span>
+            <div class="name">
+              setting
+            </div>
+            <div class="code-name">.icon-setting-one
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-delete-one"></span>
+            <div class="name">
+              delete-one
+            </div>
+            <div class="code-name">.icon-delete-one
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-close-one"></span>
+            <div class="name">
+              close-one
+            </div>
+            <div class="code-name">.icon-close-one
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-slightly-smiling-face"></span>
+            <div class="name">
+              slightly-smiling-face
+            </div>
+            <div class="code-name">.icon-slightly-smiling-face
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-pic"></span>
+            <div class="name">
+              pic
+            </div>
+            <div class="code-name">.icon-pic
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-voice-one"></span>
+            <div class="name">
+              voice-one
+            </div>
+            <div class="code-name">.icon-voice-one
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-peoples-two"></span>
+            <div class="name">
+              peoples-two
+            </div>
+            <div class="code-name">.icon-peoples-two
+            </div>
+          </li>
+          
+          <li class="dib">
             <span class="icon iconfont icon-caret-up"></span>
             <div class="name">
               caret-up
@@ -339,11 +759,11 @@
           </li>
           
           <li class="dib">
-            <span class="icon iconfont icon-delete"></span>
+            <span class="icon iconfont icon-delete-two"></span>
             <div class="name">
-              delete
+              delete-two
             </div>
-            <div class="code-name">.icon-delete
+            <div class="code-name">.icon-delete-two
             </div>
           </li>
           
@@ -475,6 +895,230 @@
           
             <li class="dib">
                 <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-star1"></use>
+                </svg>
+                <div class="name">star</div>
+                <div class="code-name">#icon-star1</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-edit-1"></use>
+                </svg>
+                <div class="name">edit-1</div>
+                <div class="code-name">#icon-edit-1</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-logout"></use>
+                </svg>
+                <div class="name">logout</div>
+                <div class="code-name">#icon-logout</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-chat-message"></use>
+                </svg>
+                <div class="name">chat-message</div>
+                <div class="code-name">#icon-chat-message</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-chevron-down"></use>
+                </svg>
+                <div class="name">chevron-down</div>
+                <div class="code-name">#icon-chevron-down</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-caret-down-small"></use>
+                </svg>
+                <div class="name">caret-down-small</div>
+                <div class="code-name">#icon-caret-down-small</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-cart"></use>
+                </svg>
+                <div class="name">cart</div>
+                <div class="code-name">#icon-cart</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-delete-three"></use>
+                </svg>
+                <div class="name">delete-three</div>
+                <div class="code-name">#icon-delete-three</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-copy"></use>
+                </svg>
+                <div class="name">copy</div>
+                <div class="code-name">#icon-copy</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-quote"></use>
+                </svg>
+                <div class="name">quote</div>
+                <div class="code-name">#icon-quote</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-send"></use>
+                </svg>
+                <div class="name">send</div>
+                <div class="code-name">#icon-send</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-comment-two"></use>
+                </svg>
+                <div class="name">comment-two</div>
+                <div class="code-name">#icon-comment-two</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-eit"></use>
+                </svg>
+                <div class="name">eit</div>
+                <div class="code-name">#icon-eit</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-comments"></use>
+                </svg>
+                <div class="name">comments</div>
+                <div class="code-name">#icon-comments</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-comment-one"></use>
+                </svg>
+                <div class="name">comment-one</div>
+                <div class="code-name">#icon-comment-one</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-like"></use>
+                </svg>
+                <div class="name">like</div>
+                <div class="code-name">#icon-like</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-set-top"></use>
+                </svg>
+                <div class="name">set-top</div>
+                <div class="code-name">#icon-set-top</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-pic-two"></use>
+                </svg>
+                <div class="name">pic-two</div>
+                <div class="code-name">#icon-pic-two</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-close-remind"></use>
+                </svg>
+                <div class="name">close-remind</div>
+                <div class="code-name">#icon-close-remind</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-log"></use>
+                </svg>
+                <div class="name">log</div>
+                <div class="code-name">#icon-log</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-jubaoguanli"></use>
+                </svg>
+                <div class="name">jubaoguanli</div>
+                <div class="code-name">#icon-jubaoguanli</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-setting-one"></use>
+                </svg>
+                <div class="name">setting</div>
+                <div class="code-name">#icon-setting-one</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-delete-one"></use>
+                </svg>
+                <div class="name">delete-one</div>
+                <div class="code-name">#icon-delete-one</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-close-one"></use>
+                </svg>
+                <div class="name">close-one</div>
+                <div class="code-name">#icon-close-one</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-slightly-smiling-face"></use>
+                </svg>
+                <div class="name">slightly-smiling-face</div>
+                <div class="code-name">#icon-slightly-smiling-face</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-pic"></use>
+                </svg>
+                <div class="name">pic</div>
+                <div class="code-name">#icon-pic</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-voice-one"></use>
+                </svg>
+                <div class="name">voice-one</div>
+                <div class="code-name">#icon-voice-one</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-peoples-two"></use>
+                </svg>
+                <div class="name">peoples-two</div>
+                <div class="code-name">#icon-peoples-two</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
                   <use xlink:href="#icon-caret-up"></use>
                 </svg>
                 <div class="name">caret-up</div>
@@ -563,10 +1207,10 @@
           
             <li class="dib">
                 <svg class="icon svg-icon" aria-hidden="true">
-                  <use xlink:href="#icon-delete"></use>
+                  <use xlink:href="#icon-delete-two"></use>
                 </svg>
-                <div class="name">delete</div>
-                <div class="code-name">#icon-delete</div>
+                <div class="name">delete-two</div>
+                <div class="code-name">#icon-delete-two</div>
             </li>
           
             <li class="dib">

+ 117 - 5
src/assets/iconfont/iconfont.css

@@ -1,9 +1,9 @@
 @font-face {
   font-family: "iconfont"; /* Project id 4723464 */
-  src: url('iconfont.woff2?t=1736217339971') format('woff2'),
-       url('iconfont.woff?t=1736217339971') format('woff'),
-       url('iconfont.ttf?t=1736217339971') format('truetype'),
-       url('iconfont.svg?t=1736217339971#iconfont') format('svg');
+  src: url('iconfont.woff2?t=1736823174249') format('woff2'),
+       url('iconfont.woff?t=1736823174249') format('woff'),
+       url('iconfont.ttf?t=1736823174249') format('truetype'),
+       url('iconfont.svg?t=1736823174249#iconfont') format('svg');
 }
 
 .iconfont {
@@ -14,6 +14,118 @@
   -moz-osx-font-smoothing: grayscale;
 }
 
+.icon-star1:before {
+  content: "\e7d6";
+}
+
+.icon-edit-1:before {
+  content: "\e7d7";
+}
+
+.icon-logout:before {
+  content: "\e7d8";
+}
+
+.icon-chat-message:before {
+  content: "\e7d9";
+}
+
+.icon-chevron-down:before {
+  content: "\e7da";
+}
+
+.icon-caret-down-small:before {
+  content: "\e7db";
+}
+
+.icon-cart:before {
+  content: "\e7dc";
+}
+
+.icon-delete-three:before {
+  content: "\e7d5";
+}
+
+.icon-copy:before {
+  content: "\e7d3";
+}
+
+.icon-quote:before {
+  content: "\e7d4";
+}
+
+.icon-send:before {
+  content: "\e7d2";
+}
+
+.icon-comment-two:before {
+  content: "\e7cd";
+}
+
+.icon-eit:before {
+  content: "\e7ce";
+}
+
+.icon-comments:before {
+  content: "\e7cf";
+}
+
+.icon-comment-one:before {
+  content: "\e7d0";
+}
+
+.icon-like:before {
+  content: "\e7d1";
+}
+
+.icon-set-top:before {
+  content: "\e7c7";
+}
+
+.icon-pic-two:before {
+  content: "\e7c6";
+}
+
+.icon-close-remind:before {
+  content: "\e7c8";
+}
+
+.icon-log:before {
+  content: "\e7c9";
+}
+
+.icon-jubaoguanli:before {
+  content: "\e7ca";
+}
+
+.icon-setting-one:before {
+  content: "\e7cb";
+}
+
+.icon-delete-one:before {
+  content: "\e7cc";
+}
+
+.icon-close-one:before {
+  content: "\e7c0";
+}
+
+.icon-slightly-smiling-face:before {
+  content: "\e7c1";
+}
+
+.icon-pic:before {
+  content: "\e7c2";
+}
+
+.icon-voice-one:before {
+  content: "\e7c4";
+}
+
+.icon-peoples-two:before {
+  content: "\e7c5";
+}
+
 .icon-caret-up:before {
   content: "\e8ed";
 }
@@ -58,7 +170,7 @@
   content: "\e791";
 }
 
-.icon-delete:before {
+.icon-delete-two:before {
   content: "\e7c3";
 }
 

File diff suppressed because it is too large
+ 0 - 0
src/assets/iconfont/iconfont.js


+ 198 - 2
src/assets/iconfont/iconfont.json

@@ -6,6 +6,202 @@
   "description": "",
   "glyphs": [
     {
+      "icon_id": "43103288",
+      "name": "star",
+      "font_class": "star1",
+      "unicode": "e7d6",
+      "unicode_decimal": 59350
+    },
+    {
+      "icon_id": "43103287",
+      "name": "edit-1",
+      "font_class": "edit-1",
+      "unicode": "e7d7",
+      "unicode_decimal": 59351
+    },
+    {
+      "icon_id": "43103286",
+      "name": "logout",
+      "font_class": "logout",
+      "unicode": "e7d8",
+      "unicode_decimal": 59352
+    },
+    {
+      "icon_id": "43103284",
+      "name": "chat-message",
+      "font_class": "chat-message",
+      "unicode": "e7d9",
+      "unicode_decimal": 59353
+    },
+    {
+      "icon_id": "43103285",
+      "name": "chevron-down",
+      "font_class": "chevron-down",
+      "unicode": "e7da",
+      "unicode_decimal": 59354
+    },
+    {
+      "icon_id": "43103282",
+      "name": "caret-down-small",
+      "font_class": "caret-down-small",
+      "unicode": "e7db",
+      "unicode_decimal": 59355
+    },
+    {
+      "icon_id": "43103283",
+      "name": "cart",
+      "font_class": "cart",
+      "unicode": "e7dc",
+      "unicode_decimal": 59356
+    },
+    {
+      "icon_id": "43078637",
+      "name": "delete-three",
+      "font_class": "delete-three",
+      "unicode": "e7d5",
+      "unicode_decimal": 59349
+    },
+    {
+      "icon_id": "43078629",
+      "name": "copy",
+      "font_class": "copy",
+      "unicode": "e7d3",
+      "unicode_decimal": 59347
+    },
+    {
+      "icon_id": "43078628",
+      "name": "quote",
+      "font_class": "quote",
+      "unicode": "e7d4",
+      "unicode_decimal": 59348
+    },
+    {
+      "icon_id": "43073198",
+      "name": "send",
+      "font_class": "send",
+      "unicode": "e7d2",
+      "unicode_decimal": 59346
+    },
+    {
+      "icon_id": "43073203",
+      "name": "comment-two",
+      "font_class": "comment-two",
+      "unicode": "e7cd",
+      "unicode_decimal": 59341
+    },
+    {
+      "icon_id": "43073202",
+      "name": "eit",
+      "font_class": "eit",
+      "unicode": "e7ce",
+      "unicode_decimal": 59342
+    },
+    {
+      "icon_id": "43073200",
+      "name": "comments",
+      "font_class": "comments",
+      "unicode": "e7cf",
+      "unicode_decimal": 59343
+    },
+    {
+      "icon_id": "43073199",
+      "name": "comment-one",
+      "font_class": "comment-one",
+      "unicode": "e7d0",
+      "unicode_decimal": 59344
+    },
+    {
+      "icon_id": "43073201",
+      "name": "like",
+      "font_class": "like",
+      "unicode": "e7d1",
+      "unicode_decimal": 59345
+    },
+    {
+      "icon_id": "43063382",
+      "name": "set-top",
+      "font_class": "set-top",
+      "unicode": "e7c7",
+      "unicode_decimal": 59335
+    },
+    {
+      "icon_id": "43063327",
+      "name": "pic-two",
+      "font_class": "pic-two",
+      "unicode": "e7c6",
+      "unicode_decimal": 59334
+    },
+    {
+      "icon_id": "43063326",
+      "name": "close-remind",
+      "font_class": "close-remind",
+      "unicode": "e7c8",
+      "unicode_decimal": 59336
+    },
+    {
+      "icon_id": "43063324",
+      "name": "log",
+      "font_class": "log",
+      "unicode": "e7c9",
+      "unicode_decimal": 59337
+    },
+    {
+      "icon_id": "43063325",
+      "name": "jubaoguanli",
+      "font_class": "jubaoguanli",
+      "unicode": "e7ca",
+      "unicode_decimal": 59338
+    },
+    {
+      "icon_id": "43063323",
+      "name": "setting",
+      "font_class": "setting-one",
+      "unicode": "e7cb",
+      "unicode_decimal": 59339
+    },
+    {
+      "icon_id": "43063322",
+      "name": "delete-one",
+      "font_class": "delete-one",
+      "unicode": "e7cc",
+      "unicode_decimal": 59340
+    },
+    {
+      "icon_id": "43062458",
+      "name": "close-one",
+      "font_class": "close-one",
+      "unicode": "e7c0",
+      "unicode_decimal": 59328
+    },
+    {
+      "icon_id": "43062459",
+      "name": "slightly-smiling-face",
+      "font_class": "slightly-smiling-face",
+      "unicode": "e7c1",
+      "unicode_decimal": 59329
+    },
+    {
+      "icon_id": "43062460",
+      "name": "pic",
+      "font_class": "pic",
+      "unicode": "e7c2",
+      "unicode_decimal": 59330
+    },
+    {
+      "icon_id": "43062461",
+      "name": "voice-one",
+      "font_class": "voice-one",
+      "unicode": "e7c4",
+      "unicode_decimal": 59332
+    },
+    {
+      "icon_id": "43062462",
+      "name": "peoples-two",
+      "font_class": "peoples-two",
+      "unicode": "e7c5",
+      "unicode_decimal": 59333
+    },
+    {
       "icon_id": "6598341",
       "name": "caret-up",
       "font_class": "caret-up",
@@ -84,8 +280,8 @@
     },
     {
       "icon_id": "4766676",
-      "name": "delete",
-      "font_class": "delete",
+      "name": "delete-two",
+      "font_class": "delete-two",
       "unicode": "e7c3",
       "unicode_decimal": 59331
     },

File diff suppressed because it is too large
+ 57 - 1
src/assets/iconfont/iconfont.svg


二進制
src/assets/iconfont/iconfont.ttf


二進制
src/assets/iconfont/iconfont.woff


二進制
src/assets/iconfont/iconfont.woff2


二進制
src/assets/img/chat/add.png


二進制
src/assets/img/chat/close.png


二進制
src/assets/img/chat/downArrow.png


二進制
src/assets/img/chat/downArrowGray.png


二進制
src/assets/img/chat/downArrowWhite.png


二進制
src/assets/img/chat/emoji_icon.png


二進制
src/assets/img/chat/loading.gif


二進制
src/assets/img/chat/message-search.png


二進制
src/assets/img/chat/more.png


二進制
src/assets/img/chat/no-chat.png


二進制
src/assets/img/chat/pic_icon.png


二進制
src/assets/img/chat/qrCode.png


二進制
src/assets/img/chat/radio.png


二進制
src/assets/img/chat/radio2.png


二進制
src/assets/img/chat/radio_circle_gray.png


二進制
src/assets/img/chat/radio_circle_orange.png


二進制
src/assets/img/chat/reduce.png


二進制
src/assets/img/chat/type1.png


二進制
src/assets/img/chat/type2.png


二進制
src/assets/img/chat/type3.png


二進制
src/assets/img/chat/type4.png


二進制
src/assets/img/chat/type5.png


二進制
src/assets/img/chat/type6.png


二進制
src/assets/img/chat/type7.png


二進制
src/assets/img/chat/type8.png


二進制
src/assets/img/chat/type9.png


二進制
src/assets/img/profile/profile_my_bg.png


二進制
src/assets/img/profile/profile_my_boy.png


二進制
src/assets/img/profile/profile_my_girl.png


二進制
src/assets/img/search.png


+ 29 - 0
src/assets/style/common.scss

@@ -0,0 +1,29 @@
+.xao-yao-dialog {
+  padding: 0 !important;
+  border-radius: 12px;
+
+  .el-dialog__header {
+    padding: 20px;
+    box-sizing: border-box;
+    //background: #55a532;
+    position: relative;
+    border-bottom: 1px solid #D9D9D9;
+    .el-dialog__title {
+      color: #333333;
+      font-size: 16px;
+    }
+    .el-dialog__headerbtn {
+      height: 100%;
+      top: 50%;
+      transform: translateY(-50%);
+    }
+  }
+  &.xao-yao-dialog__body--padding .el-dialog__body {
+    padding: 20px;
+    box-sizing: border-box;
+  }
+  &.xao-yao-dialog__body--padding-h .el-dialog__body {
+    padding: 20px 0;
+    box-sizing: border-box;
+  }
+}

+ 8 - 1
src/assets/style/element.scss

@@ -1,5 +1,12 @@
+@forward 'element-plus/theme-chalk/src/common/var.scss' with (
+  $colors: (
+    "primary": ("base": rgba(253, 154, 0, 1)),
+     'info': ( 'base': #999999 ),
+  ),
+);
+
 :root {
   --el-color-primary: rgba(253, 154, 0, 1);
   --el-color-primary-light-3: rgba(253, 154, 0, 0.8);
   --el-color-primary-dark-2: rgba(253, 154, 0, 0.95);
-}
+}

+ 202 - 0
src/components/MultiHeader/index.vue

@@ -0,0 +1,202 @@
+<template>
+  <div class="round-comp">
+    <template v-if="list.length">
+      <div class="round" :class="`round--${length}`">
+        <div class="round--center">
+          <div v-for="(img, i) in list" :key="i" class="header">
+            <img
+                width="100%"
+                height="100%"
+                class="w-full h-full object-cover"
+                :src="img"
+                alt=""
+            />
+          </div>
+        </div>
+      </div>
+    </template>
+  </div>
+</template>
+<script setup>
+const props = defineProps({
+  size: {
+    type: Number,
+    default: 48
+  },
+  imgUrls: Array
+})
+
+const length = ref(0);
+const list = ref([]);
+
+const initStyle = () => {
+  const root = document.documentElement;
+  const size = props.size;
+  root.style.setProperty('--round-size', size + 'px');
+  root.style.setProperty('--round-size-header', size * 22/44 + 'px');
+  root.style.setProperty('--round-size-sm-header', size * 15/44 + 'px');
+}
+
+const initData = () => {
+  list.value = (props.imgUrls ?? []).slice(0, 9);
+  length.value = list.value.length;
+}
+onMounted(() => {
+  initStyle();
+  initData();
+});
+
+</script>
+<style scoped lang="scss">
+.round-comp {
+  width: var(--round-size);
+  height: var(--round-size);
+  background: #F7F8FA;
+  border-radius: 100%;
+}
+.round {
+  //margin: 10px auto;
+  width: var(--round-size);
+  height: var(--round-size);
+  background: #F7F8FA;
+  border-radius: 100%;
+  overflow: clip;
+  position: relative;
+
+  .round--center {
+    height: 100%;
+    position: absolute;
+    display: grid;
+    gap: 1px;
+    align-items: center;
+    justify-items: center;
+    align-content: center;
+    justify-content: center;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%, -50%);
+    .header {
+      text-align: center;
+      font-size: 12px;
+    }
+  }
+}
+
+.round--1 {
+  .round--center {
+    .header {
+      width: var(--round-size);
+      height: var(--round-size);
+    }
+  }
+
+}
+
+.round--2 {
+  .round--center {
+    grid-template-columns: auto auto;
+    grid-template-rows: auto;
+    .header {
+      width: var(--round-size-header);
+      height: var(--round-size-header);
+    }
+  }
+}
+
+.round--3 {
+  .round--center {
+    grid-template-columns: auto auto;
+    grid-template-rows: auto auto;
+  }
+
+  .header {
+    width: var(--round-size-header);
+    height: var(--round-size-header);
+
+    &:nth-child(1) {
+      grid-column: 1 / span 2;
+    }
+  }
+}
+
+.round--4 {
+  .round--center {
+    grid-template-columns: auto auto;
+    grid-template-rows: auto auto;
+    .header {
+      width: var(--round-size-header);
+      height: var(--round-size-header);
+    }
+  }
+}
+
+.round--5 {
+  .round--center {
+    grid-template-columns: auto auto auto auto auto auto;
+    grid-template-rows: auto auto;
+    .header {
+      width: var(--round-size-sm-header);
+      height: var(--round-size-sm-header);
+      grid-column: span 2;
+      &:nth-child(1) {
+        grid-column: 2 / span 2;
+      }
+      &:nth-child(2) {
+        grid-column: 4 / span 2;
+      }
+    }
+  }
+}
+
+.round--6 {
+  .round--center {
+    grid-template-columns: auto auto auto;
+    grid-template-rows: auto auto;
+    .header {
+      width: var(--round-size-sm-header);
+      height: var(--round-size-sm-header);
+    }
+  }
+}
+
+.round--7 {
+  .round--center {
+    grid-template-columns: auto auto auto;
+    grid-template-rows: auto auto;
+    .header {
+      width: var(--round-size-sm-header);
+      height: var(--round-size-sm-header);
+      &:nth-child(1) {
+        grid-column: 1 / span 3;
+      }
+    }
+  }
+}
+.round--8 {
+  .round--center {
+    grid-template-columns: auto auto auto auto auto auto;
+    grid-template-rows: auto auto;
+    .header {
+      width: var(--round-size-sm-header);
+      height: var(--round-size-sm-header);
+      grid-column: span 2;
+      &:nth-child(1) {
+        grid-column: 2 / span 2;
+      }
+      &:nth-child(2) {
+        grid-column: 4 / span 2;
+      }
+    }
+  }
+}
+.round--9 {
+  .round--center {
+    grid-template-columns: auto auto auto;
+    grid-template-rows: auto auto;
+    .header {
+      width: var(--round-size-sm-header);
+      height: var(--round-size-sm-header);
+    }
+  }
+}
+</style>

+ 16 - 2
src/components/NavBar/Login.client.vue

@@ -35,15 +35,18 @@
           mode="vertical"
           unique-opened
           class="el-menu-vertical-demo"
+          active-text-color="#FD9A00"
           style="
             --el-menu-item-height: 30px;
             --el-menu-sub-item-height: 30px;
             --el-menu-item-font-size: 12px;
             --el-menu-text-color: #333;
+            --el-menu-active-color: #FD9A00
           "
         >
           <el-sub-menu index="1">
             <template #title>
+              <span class="iconfont icon-cart mr-6"></span>
               <span>我的订单</span>
             </template>
             <el-menu-item index="1-1">
@@ -55,20 +58,27 @@
             <el-menu-item index="1-3">
               <NuxtLink to="/profile/visa-orders">签证订单</NuxtLink>
             </el-menu-item>
-            <el-menu-item index="1-4">
+            <!--            <el-menu-item index="1-4">
               <NuxtLink to="/profile/my-comment">我的评论</NuxtLink>
-            </el-menu-item>
+            </el-menu-item>-->
             <!-- <el-menu-item index="1-4">
               <NuxtLink to="/profile/labour-orders">劳务投递</NuxtLink>
             </el-menu-item> -->
           </el-sub-menu>
           <el-menu-item>
+            <span class="iconfont icon-edit-1 mr-6"></span>
             <NuxtLink to="/profile/notes">我的游记</NuxtLink>
           </el-menu-item>
           <el-menu-item>
+            <span class="iconfont icon-star1 mr-6"></span>
             <NuxtLink to="/profile/collection">我的收藏</NuxtLink>
           </el-menu-item>
           <el-menu-item>
+            <span class="iconfont icon-chat-message mr-6"></span>
+            <NuxtLink to="/chat">我的消息</NuxtLink>
+          </el-menu-item>
+          <el-menu-item>
+            <span class="iconfont icon-logout mr-6"></span>
             <span @click="handleLogout">退出登录</span>
           </el-menu-item>
         </el-menu>
@@ -136,4 +146,8 @@ async function handleLogout() {
 :deep(.el-menu) {
   border-right: none;
 }
+:deep(.el-menu-item:hover),
+:deep(.el-sub-menu__title:hover){
+  color: #FD9A00;
+}
 </style>

+ 15 - 5
src/components/Profile/Home/Tabs.vue

@@ -54,26 +54,36 @@ const tabs = [
   //   to: '/profile/labour-orders'
   // },
 
-  {
-    label: '我的收藏',
-    icon: profile_tab_collect,
-    to: '/profile/collection'
-  },
+
   {
     label: '我的游记',
     icon: profile_tab_travel_note,
     to: '/profile/notes'
   },
   {
+    label: '我的收藏',
+    icon: profile_tab_collect,
+    to: '/profile/collection'
+  },
+  {
+    label: '我的消息',
+    icon: profile_my_comment,
+    to: '/chat'
+  }
+  /*{
     label: '我的评论',
     icon: profile_my_comment,
     to: '/profile/my-comment'
+<<<<<<< HEAD
   },
   {
     label: '我的返利',
     icon: profile_my_wallet,
     to: '/profile/wallet'
   }
+=======
+  }*/
+>>>>>>> 52e3ab6424458d9e83b37ba685a37d2736409c5d
 ]
 
 const route = useRoute()

+ 67 - 0
src/components/XDialog/index.vue

@@ -0,0 +1,67 @@
+<template>
+    <teleport to="body">
+        <transition name="fade">
+            <div v-if="show" @click="handleClose" class="flex items-center justify-center x-dialog-box" ref="dialogBoxRef">
+                <div @click.stop="" class="x-dialog-content bg-[#fff] rounded-[8px] scrollbar relative" :style="{ width: `${width || '300px'}` }">
+                    <div class="flex items-center justify-between sticky top-0 left-0 p-[10px] h-[44px] border-b-[#D9D9D9] border-b-[1px] bg-[#fff] z-[100]">
+                        <div class="text-[#333] text-[16px]">
+                            {{ title }}
+                        </div>
+                        <div @click.stop="handleClose" class="cursor-pointer" >
+                            <span class="iconfont icon-close text-[#666]"></span>
+                        </div>
+                    </div>
+                    <div class="w-full relative">
+                        <slot></slot>
+                    </div>
+                </div>
+            </div>
+        </transition>
+    </teleport>
+
+</template>
+<script setup>
+const emit = defineEmits(['close'])
+const props = defineProps({
+    title: String,
+    width: String,
+    show: Boolean,
+})
+const showModal = ref(false)
+watchEffect(() => props.show, (newValue) => {
+    showModal.value = props.show?true:false
+})
+const dialogBoxRef = ref(null)
+function handleClose() {
+    emit('close')
+}
+onMounted(() => {
+    nextTick(() => {
+        showModal.value && (document.body.style.overflow = 'hidden');
+    })
+})
+</script>
+<style lang="scss" scoped>
+.x-dialog-box {
+    width: 100vw;
+    height: 100vh;
+    background: rgba(0, 0, 0, 0.5);
+    position: fixed;
+    top: 0;
+    left: 0;
+    z-index: 1000;
+
+}
+
+.x-dialog-content {
+    max-height: 90vh;
+    overflow-y: auto;
+}
+
+.scrollbar::-webkit-scrollbar {
+    width: 0px;
+    height: 0px;
+    background-color: #ccc;
+}
+
+</style>

+ 8 - 0
src/middleware/01.intercept-components.global.js

@@ -0,0 +1,8 @@
+export default defineNuxtRouteMiddleware((to, from) => {
+    // 拦截路由
+    const interceptRouteRegex = [/components/, /component/];
+
+    if (interceptRouteRegex.find(o => o.test(to.fullPath))) {
+        return navigateTo('/404', {replace: true,})
+    }
+})

+ 78 - 0
src/pages/chat/components/ActiveMessage.vue

@@ -0,0 +1,78 @@
+<template>
+    <div class="w-full h-full overflow-y-auto relative">
+        <div
+            class="w-full sticky top-0 bg-[#fff] h-[50px] border-b-[1px] border-[#dedede] flex items-center justify-between shrink-0">
+            <div class=" mx-[15px]">
+                <el-popover trigger="hover" :popper-style="{ padding: 0 }">
+                    <template #reference>
+                        <div  class="flex items-center rounded h-[28px] text-[#FD9A00] text-[16px] cursor-pointer">
+                            {{ msgTypeList[msgType].text }}
+                            
+                            <span class="icon iconfont text-[#FD9A00] ml-3 mt-2" style="transform: rotate(90deg);">&#xe7eb;</span>
+                        </div>
+                    </template>
+                    <div class="border cursor-pointer">
+
+                        <div @click="msgType=0" class="hover:bg-[#fd9a00] hover:text-[#fff] w-full h-30 flex items-center justify-center">
+                            全部消息
+                        </div>
+
+
+                        <div @click="msgType=5" class="hover:bg-[#fd9a00] hover:text-[#fff] w-full h-30 flex items-center justify-center">
+                            赞与收藏
+                        </div>
+
+
+                        <div @click="msgType=6" class="hover:bg-[#fd9a00] hover:text-[#fff] w-full h-30 flex items-center justify-center">
+                            发出的评论
+                        </div>
+
+                        <div @click="msgType=7" class="hover:bg-[#fd9a00] hover:text-[#fff] w-full h-30 flex items-center justify-center">
+                            收到的评论
+                        </div>
+                    </div>
+                </el-popover>
+            </div>
+        </div>
+        <div class="px-12 pb-30">
+            <div class="flex items-center justify-between mt-24 cursor-pointer">
+                <div class="flex items-center">
+                    <div class="w-[44px] h-[44px] rounded-full overflow-hidden shrink-0">
+                        <img class="w-full h-full object-cover"
+                            src="https://game.gtimg.cn/images/yxzj/img201606/skin/hero-info/511/511-bigskin-5.jpg"
+                            alt="" />
+                    </div>
+                    <div class="flex flex-col justify-between ml-15">
+                        <div class="text-[14px] text-[#333]">
+                            阿姆斯特丹(田园环岛)
+                        </div>
+                        <div class="text-[12px] text-[#666]">
+                            关注了你 14:20
+                        </div>
+                    </div>
+                </div>
+                <div
+                    class="text-[#fff] bg-[#FD9A00] w-[72px] h-[32px] rounded text-[14px] flex items-center justify-center">
+                    私信
+                </div>
+            </div>
+        </div>
+    </div>
+</template>
+<script setup>
+//  5赞与收藏 6提及  7收到的评论 8发出的评论
+const msgTypeList = {
+    '0': { id: 0, text: '全部消息' },
+    '5': { id: 5, text: '赞与收藏' }, 
+    '6': { id: 6, text: '提及' },
+    '7': { id: 7, text: '收到的评论' },
+    '8': { id: 8, text: '发出的评论' },
+}
+const msgType=ref('0')
+
+</script>
+<style scoped>
+:deep(.el-tooltip__trigger:focus-visible) {
+    outline: unset;
+}
+</style>

+ 141 - 0
src/pages/chat/components/ApplyJoinGroup.vue

@@ -0,0 +1,141 @@
+<template>
+    <XDialog :title="title || ''" :width="width" :show="show" @close="handleClose">
+        <div class="w-[75%] bg-[#fff]" style="margin:0 auto;">
+            <div class="flex justify-center mt-30">
+                <div class="">
+                    <MultiHeader :size="80" :imgUrls="createGroupAvatar() || []"></MultiHeader>
+                </div>
+            </div>
+            <div class="flex items-center justify-center mt-20">
+                <div v-if="groupInfo?.groupName" class="text-[#333] text-[14px]">
+                    {{ groupInfo?.groupName }}({{ groupInfo?.memberCount }})
+                </div>
+                <div v-if="groupInfo?.belongTypeIdDictMap?.name"
+                    class="bg-[#FF9300]/20 rounded text-[#FF9300] text-[12px] flex items-center justify-center px-10 ml-10">
+                    {{ groupInfo?.belongTypeIdDictMap?.name }}
+                </div>
+            </div>
+            <div v-if="groupInfo?.description" class="text-center text-[#000]/60 text-[14px] mt-20">
+                {{ groupInfo?.description }}
+            </div>
+            <div class="flex items-center justify-center text-[12px] mt-20">
+                <div class="flex items-center">
+                    <div class="w-[24px] h-[24px] rounded-full bg-[#dedede] overflow-hidden">
+                        <img v-if="getLordAvatar()" :src="getLordAvatar()" class="w-full h-full object-cover" />
+                    </div>
+                    <div v-if="groupInfo?.leaderIdDictMap?.name" class="text-[#333] ml-10">
+                        {{ groupInfo?.leaderIdDictMap?.name }}(群主)
+                    </div>
+                </div>
+                <!-- 他人主页暂时不做 -->
+                <!-- <div class="text-[#666] flex items-center">
+                    <span>进入主页</span>
+                    <img src="~assets/img/chat/downArrowGray.png" class="w-[16px] mt-1" style="transform: rotate(-90deg);" />
+                </div> -->
+            </div>
+            <div v-if="status == 1" class="flex justify-center items-center mt-30 text-[#f40]">
+                您已加入该群
+            </div>
+            <div v-if="status == 2" class="flex justify-center items-center mt-30 text-[#f40]">
+                该群已封禁,无法加入
+            </div>
+            <div v-if="status == 3" class="flex justify-center items-center mt-30 text-[#f40]">
+                该群已解散,无法加入
+            </div>
+            <div v-if="status == 4" class="flex justify-center items-center mt-30 text-[#f40]">
+                您的加群申请已提交,等待群主审核
+            </div>
+            <div class="flex justify-center items-center my-30 cursor-pointer">
+                <div @click="handleClose"
+                    class="text-[#999] w-[80px] h-[32px] border rounded flex justify-center items-center">
+                    返回
+                </div>
+                <div v-if="status == 0" @click="handleJoin"
+                    class="text-[#fff] w-[80px] bg-[#FD9A00] ml-50 h-[32px] border rounded flex justify-center items-center">
+                    加入
+                </div>
+            </div>
+        </div>
+    </XDialog>
+</template>
+<script setup>
+import XDialog from '@/components/XDialog';
+const props = defineProps({
+    title: String,
+    width: String,
+    show: Boolean,
+    groupInfo: Object,
+    groupId: String,
+})
+
+const emit = defineEmits(['close'])
+function handleClose() {
+    emit('close')
+}
+const chatStore = useChatStore()
+const { curConversiton, chatList, ws, user } = storeToRefs(chatStore)
+const status = computed(() => props.groupInfo.codeShowStatus)
+// const status = ref(4)
+// 获取群主头像
+function getLordAvatar() {
+    const memberList = props.groupInfo.memberList
+    const leaderId = props.groupInfo.leaderId
+    let avatar = ''
+    if (Array.isArray(memberList)) {
+        for (let i = 0; i < memberList.length; i++) {
+            if (memberList[i].userId == leaderId) {
+                avatar = memberList[i].headImageUrl
+                break
+            }
+        }
+    }
+    return avatar
+}
+
+// 创建群头像
+function createGroupAvatar() {
+    const memberList = props.groupInfo.memberList
+    let avatarArr = []
+    if (Array.isArray(memberList)) {
+        for (let i = 0; i < memberList.length; i++) {
+            if(i<9){
+                avatarArr.push(memberList[i]?.headImageUrl)
+            }
+            if(i>=8){
+                break
+            }
+        }
+    }
+    return avatarArr
+}
+
+// 申请加群
+async function handleJoin() {
+    const data = {
+        groupId: props.groupId,
+        ids: [user.value.userId]
+    }
+    await request('/website/tourMember/invite', { method: 'post', body: data })
+    ElMessage.success('申请发送成功,等待群主审核')
+    handleClose()
+}
+onMounted(() => {
+
+    // codeShowStatus 状态 0未加入 1已加入 2已封禁 3已解散 4申请中"
+    const { codeShowStatus } = props.groupInfo || {}
+
+    if (codeShowStatus == 1) {
+        const groupId = props.groupId
+        const getUserId = ''
+        const sendUserId = user.value.userId
+        const noticeType = 2
+        const data = {
+            groupId,
+            getUserId,
+            sendUserId,
+            noticeType,
+        }
+        chatStore.createConversation(data)
+    }
+})
+</script>

+ 99 - 0
src/pages/chat/components/ApplyJoinList.vue

@@ -0,0 +1,99 @@
+<template>
+    <XDialog :title="title || ''" :width="width" :show="show" @close="handleClose">
+        <div class="p-20 bg-[#fff]" style="min-height: 200px;">
+            <div v-for="(item,index) in list" :key="index" class="flex items-center justify-between mb-20">
+                <div class="flex items-center">
+                    <div class="w-[44px] h-[44px] rounded-full overflow-hidden bg-[#dedede]">
+                        <img :src="item?.tourUser?.headImageUrl" class="w-full h-full object-cover" alt="" />
+                    </div>
+                    <div class="text-[14px] ml-10">
+                        <div class="text-[#333]">
+                            {{ item?.tourUser?.showName }}
+                        </div>
+                        <div class="text-[#666]">
+                            {{ item?.createTime }}
+                        </div>
+                    </div>
+                </div>
+                <!-- 1待确认 2已同意 3已拒绝 -->
+                <div v-if="item?.status == 1" class="flex items-center text-[14px] cursor-pointer">
+                    <div @click="handleAgreeAndDeny(item?.id,3)" class="w-[80px] h-[32px] flex items-center border justify-center rounded text-[#999] mr-20">
+                        拒绝
+                    </div>
+                    <div @click="handleAgreeAndDeny(item?.id,2)" class="w-[80px] h-[32px] bg-[#FD9A00] flex items-center justify-center rounded text-[#fff]">
+                        通过
+                    </div>
+                </div>
+                <div v-else class="text-[#999]">
+                    <div v-if="item?.status == 2">
+                        已同意
+                    </div>
+                    <div v-if="item?.status == 3">
+                        已拒绝
+                    </div>
+                </div>
+            </div>
+            <div class="mt-70 text-[#999] text-center">
+                没有收到申请
+            </div>
+        </div>
+    </XDialog>
+</template>
+<script setup>
+import XDialog from '@/components/XDialog';
+const props = defineProps({
+    title: String,
+    width: String,
+    show: Boolean,
+    groupId: String,
+})
+
+const emit = defineEmits(['close'])
+function handleClose() {
+    emit('close')
+}
+const chatStore = useChatStore()
+const { curConversiton, chatList, ws, user } = storeToRefs(chatStore)
+const status = computed(() => props.groupInfo.codeShowStatus)
+
+const list = ref([])
+async function getList(){
+
+    const query = {groupId:props.groupId}
+
+    const {data} =await request('/website/tourGroup/getApplicationsList',{query})
+    if(Array.isArray(data)){
+        list.value = data
+    }
+}
+
+// 同意与拒绝申请
+async function handleAgreeAndDeny(id,isPass){
+
+    if(!props.groupId) {
+        console.log('群id不能为空!')
+        return
+    }
+
+    if(isPass!=2 && isPass!=1) return
+
+    if(!id) {
+        ElMessage.error('用户不存在!')
+        return
+    }
+
+    const data={
+        groupId:props.groupId,
+        ids:[id],
+        isPass
+    }
+
+    await request('/website/tourGroup/applyIsPass',{method:'post',body:data})
+    ElMessage.success('操作成功')
+    getList()
+    
+}
+onMounted(() => {
+    getList()
+})
+</script>

+ 538 - 0
src/pages/chat/components/Chat.vue

@@ -0,0 +1,538 @@
+<template>
+    <div v-if="wsConnect == 0" class="w-full h-full flex items-center justify-center text-[#999] text-[16px]">
+        聊天网络连接中...
+    </div>
+    <div v-else-if="wsConnect == 1" class="w-full h-full flex items-center justify-center text-[#999] text-[16px]">
+        聊天正在连接中...
+    </div>
+    <div v-else-if="wsConnect == 3" class="w-full h-full flex items-center justify-center text-[#999] text-[16px]">
+        聊天连接已断开,请刷新页面重新连接,或稍后重试!
+    </div>
+    <template v-else-if="wsConnect == 2">
+        <div v-if="curConversiton?.groupId" class="w-full h-full">
+            <div class="w-full h-[50px] border-b-[1px] border-[#dedede] flex items-center justify-between shrink-0">
+                <div class="flex items-center">
+                    <div class="font-bold text-[18px] mx-[15px]">
+                        {{ curConversiton?.groupRemark || '' }}
+                    </div>
+                    <div v-if="curConversiton?.noticeType == 1" @click="handleFollow"
+                        class="rounded-full cursor-pointer bg-[#fa8446] flex items-center justify-center px-10 py-2 text-[#fff]">
+                        {{ FANS_STATUS[followStatus]?.text }}
+                    </div>
+                </div>
+                <div @click="showInfoTurnOnOff" class="mr-[15px]">
+                    <img class="w-[16px] h-[16px]" src="~assets/img/chat/more.png" alt="" />
+                </div>
+            </div>
+            <div class="w-full flex justify-between" style="height:calc(100% - 50px);">
+                <div class="flex-1 flex flex-col justify-between h-full border-[#ccc] border-r-[1px]">
+                    <div ref="messageBoxRef" class="flex-1 overflow-y-auto scrollbar mb-10">
+
+                        <template v-for="(item, index) in receiveGetter" :key="index">
+                            <!-- 右侧自己的消息 -->
+                            <template v-if="item.sendUserId == user.pass">
+                                <!-- 右侧消息 图片 -->
+                                <template v-if="item.messageType == 1">
+                                    <div class="text-center text-[#666] mt-30 text-[12px]">
+                                        {{ item.createTime }}
+                                    </div>
+                                    <div class="flex justify-end mt-30">
+                                        <div class="flex w-[80%] pr-10 justify-end">
+                                            <div class="mr-10">
+                                                <div class="text-[#666] text-right">{{ user?.showName }}</div>
+                                                <div class="p-5 rounded relative overflow-hidden" style="float:right;max-height:200px;">
+                                                    <el-image :preview-src-list="[convertImg(item.messageContent)]"
+                                                        :src="convertImg(item.messageContent)" fit="cover" />
+                                                    <div v-if="!item?.createTime" class="absolute top-[50%] left-[0px]"
+                                                        style="transform: translateY(-50%) translateX(-105%);">
+                                                        <MsgStatus></MsgStatus>
+                                                    </div>
+                                                </div>
+                                            </div>
+                                            <div class="w-40 h-40 rounded-full overflow-hidden shrink-0">
+                                                <img v-if="user?.headImageUrl" class="w-full h-full object-cover"
+                                                    :src="user?.headImageUrl" alt="">
+                                            </div>
+                                        </div>
+                                    </div>
+                                </template>
+                                <template v-if="item.messageType == 0 && item.messageContent">
+                                    <!-- 右侧消息 文字 -->
+                                    <div class="text-center text-[#666] mt-30 text-[12px]">
+                                        {{ item.createTime }}
+                                    </div>
+                                    <div class="flex justify-end mt-30">
+                                        <div class="flex w-[80%] pr-10 justify-end">
+                                            <div class="mr-10">
+                                                <div class="text-[#666] text-right">{{ user?.showName }}</div>
+                                                <div class="bg-[#FD9A00]/10 py-10 px-10 rounded text-[#333] text-wrap relative"
+                                                    style="float:right;">
+
+                                                    <div class=" rounded text-[#333] text-wrap relative"
+                                                        style="max-width: 500px;white-space:pre-wrap;word-wrap: break-word;">
+                                                        <TextMsg :msg="item.messageContent"></TextMsg>
+                                                    </div>
+                                                    <div v-if="!item?.createTime" class="absolute top-[50%] left-[0px]"
+                                                        style="transform: translateY(-50%) translateX(-100%);">
+                                                        <MsgStatus></MsgStatus>
+                                                    </div>
+                                                </div>
+                                            </div>
+                                            <div class="w-40 h-40 rounded-full overflow-hidden shrink-0 bg-[#dedede]">
+                                                <img v-if="user?.headImageUrl" class="w-full h-full object-cover"
+                                                    :src="user?.headImageUrl" alt="">
+                                            </div>
+                                        </div>
+                                    </div>
+                                </template>
+                            </template>
+                            <template v-else>
+                                <template v-if="item.messageType == 0 && item.messageContent">
+                                    <!-- 时间 -->
+                                    <div class="text-center text-[#666] mt-30 text-[12px]">
+                                        {{ item.createTime }}
+                                    </div>
+                                    <!-- 左侧消息 文本 -->
+                                    <div class="flex w-[80%] pl-10 mt-30">
+                                        <div class="w-40 h-40 rounded-full overflow-hidden shrink-0 bg-[#dedede]">
+                                            <img v-if="item?.headImageUrl" class="w-full h-full object-cover"
+                                                :src="item?.headImageUrl" alt="" />
+                                        </div>
+                                        <div class="ml-10">
+                                            <div class="text-[#666]">
+                                                {{ item?.showName }}
+                                            </div>
+                                            <div class="bg-[#eee] py-5 px-10 rounded text-[#333] text-wrap"
+                                                style="float:left;max-width: 500px;white-space:pre-wrap;word-wrap: break-word;">
+                                                <!-- {{ convertImg(item.messageContent) }} -->
+                                                <TextMsg :msg="item.messageContent"></TextMsg>
+                                            </div>
+                                        </div>
+                                    </div>
+                                </template>
+                                <template v-if="item.messageType == 1">
+                                    <div class="text-center text-[#666] mt-30 text-[12px]">
+                                        {{ item.createTime }}
+                                    </div>
+                                    <!-- 左侧消息 图片 -->
+                                    <div class="flex w-[80%] pl-10 mt-30">
+                                        <div class="w-40 h-40 rounded-full overflow-hidden shrink-0">
+                                            <img class="w-full h-full object-cover" :src="item?.headImageUrl" alt="" />
+                                        </div>
+                                        <div class="ml-10">
+                                            <div class="text-[#666]">
+                                                {{ item?.showName }}
+                                            </div>
+                                            <div class="p-5 rounded overflow-hidden" style="max-height:200px">
+                                                <el-image :preview-src-list="[convertImg(item.messageContent)]"
+                                                    :src="convertImg(item.messageContent)" fit="fill" />
+                                            </div>
+                                        </div>
+                                    </div>
+                                </template>
+                            </template>
+                        </template>
+                        <div ref="msgBottomRef" class="h-50"></div>
+                    </div>
+                    <div class="w-full h-[160px] border-t-[1px] border-[#ccc] flex flex-col justify-between">
+                        <div class="flex items-center h-[36px] pl-10 cursor-pointer">
+
+                            <el-popover placement="top-start" :width="420" trigger="hover">
+                                <template #reference>
+                                    <img src="~assets/img/chat/emoji_icon.png" class="w-[20px] h-[20px]" alt="" />
+                                </template>
+                                <div class="flex flex-wrap w-full border justify-between">
+                                    <div @click="emojiClick(item.emoji)" v-for="(item, index) in emojiArr" :key="index"
+                                        class="w-[32px] h-[32px] text-[20px] shrink-0 cursor-pointer hover:bg-[#eee] flex items-center justify-center">
+                                        {{ item.emoji }}
+                                    </div>
+                                </div>
+                            </el-popover>
+                            <div class="relative w-[20px] h-[20px] ml-20 cursor-pointer" style="position: relative;">
+                                <img src="~assets/img/chat/pic_icon.png" class="w-[20px] h-[20px]" alt="" />
+                                <input type="file" @change="selectImg" class="absolute w-[20px] h-[20px]"
+                                    style="position:absolute;top:0;left:0;z-index:10;opacity: 0;" />
+                            </div>
+
+                            <img @click="showChatHistoryModal = true" src="~assets/img/chat/message-search.png"
+                                class="w-[20px] h-[20px] ml-20" alt="" />
+                        </div>
+                        <div class="flex justify-between flex-1">
+                            <div class="flex-1 flex flex-col justify-end">
+                                <textarea lang="en" ref="inputBoxRef" v-model="messageContent" class="pl-10"
+                                    style="width:100%;height:95%;resize:none;border:none;outline: none;"
+                                    placeholder="请输入消息,按Enter键或点击发送按钮发送"></textarea>
+                            </div>
+                            <div class="h-full shrink-0 flex flex-col justify-between mr-5 items-end">
+                                <div class="text-[#999]">{{ messageContent?.length }}/5000</div>
+                                <div @click="sendMessage"
+                                    class="rounded-full cursor-pointer bg-[#fa8446] text-[#fff] flex items-center justify-center mb-8 w-[80px] h-[30px]">
+                                    发送
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+                <div v-if="showInfo" class="w-[220px] shrink-0 h-full h-full overflow-y-auto scrollbar relative">
+                    <FriendInfo v-if="noticeType == 1" @clearMsg="onClearMsg" :followStatus="followStatus"></FriendInfo>
+                    <template v-if="noticeType == 2">
+                        <GroupSetting v-if="showGroupSetting" @clearMsg="onClearMsg"></GroupSetting>
+                        <GroupInfo v-else></GroupInfo>
+                    </template>
+                </div>
+            </div>
+            <!-- 查看聊天记录弹窗 -->
+            <ChatHistory v-if="showChatHistoryModal" :groupId="groupId" :show="showChatHistoryModal" width="560px"
+                title="查看聊天记录" @close="showChatHistoryModal = false" />
+        </div>
+        <div v-else class="w-full h-full flex items-center justify-center text-[#999] text-[16px]">
+            您尚未选择联系人
+        </div>
+    </template>
+
+
+</template>
+<script setup>
+import ChatHistory from './ChatHistory.vue';
+import FriendInfo from './FriendInfo.vue';
+import GroupInfo from './GroupInfo.vue';
+import GroupSetting from './GroupSetting.vue';
+import MsgStatus from './MsgStatus.vue'
+import TextMsg from './TextMsg.vue';
+import FANS_STATUS from '../followSta'
+import emojiArr from '../emoji.js'
+
+const uploadUrl = `${import.meta.env.VITE_APP_BASE_URL}/website/tourMessage/upload`
+const chatStore = useChatStore()
+const { ws, curConversiton, receive, receiveGetter, connectSta, onNewMessage, conversations } = storeToRefs(chatStore)
+const user = computed(() => chatStore.user)
+
+
+// 消息接收者的用户id
+const getUserId = computed(() => curConversiton.value.toUserId)
+
+// 消息发送者:当前登录用户的加密id
+const sendUserId = computed(() => user.value.pass)
+
+// 会话id
+const groupId = computed(() => curConversiton.value.groupId)
+
+// 用户在群聊中艾特的人
+const specialUserId = ref('')
+
+// 用户输入的文本消息
+const messageContent = ref('')
+
+// 聊天类型 1单聊 2群聊 3系统消息 4关注信息
+const noticeType = computed(() => curConversiton.value.noticeType)
+
+// 显示群聊设置
+const showGroupSetting = ref(false)
+
+const messageBoxRef = ref(null)
+const msgBottomRef = ref(null)
+const inputBoxRef = ref(null)
+
+// 当前websocket连接状态 0: 未连接 1: 连接中 2: 已连接 3: 已断开
+const wsConnect = computed(() => connectSta.value)
+
+// 显示聊天框右侧信息(主要是好友信息,群信息,群设置)
+const showInfo = ref(false)
+
+
+
+// 聊天记录的弹窗开关
+const showChatHistoryModal = ref(false)
+
+// 本地生成一个唯一消息id
+function getLocalId() {
+    const random = Math.floor(Math.random() * 10000)
+    return Date.now() + '-' + random
+}
+// 发送文本消息
+function sendMessage() {
+    if (!messageContent.value.trim()) return
+    const msgJSONString = JSON.stringify({
+        messageContent: messageContent.value
+    })
+    const msg = {
+        getUserId: getUserId.value,
+        sendUserId: sendUserId.value,
+        specialUserId: specialUserId.value,
+        groupId: groupId.value,
+        messageContent: messageContent.value,
+        messageType: 0,
+        noticeType: noticeType.value,
+        object: {
+            id: getLocalId()
+        }
+    }
+    receive.value.push(msg)
+    messageContent.value = ''
+    nextTick(() => {
+        setTimeout(() => {
+            messageBoxRef.value && (messageBoxRef.value.scrollTop = messageBoxRef.value.scrollHeight + 1000)
+        }, 100)
+    })
+    ws.value.send(JSON.stringify(msg))
+    
+}
+
+const pageSize = ref(10)
+
+const messageCount = ref(0)
+
+const timer = ref(0)
+// 获取聊天记录
+async function getChatHistory(messageId = '') {
+    clearTimeout(timer.value)
+    timer.value = setTimeout(async () => {
+        if (curConversiton.value.isLocal) return
+
+        if (!groupId.value) return
+
+        if (receive.value.length >= messageCount.value && receive.value.length > 0) return
+
+        if (receive.value.length && !messageId) return
+
+        const query = {
+            pageSize: pageSize.value,
+            groupId: groupId.value,
+            messageId
+        }
+
+        const { data: { data = [], count = [] } } = await request('/website/tourMessage/getMessageByGroupId', { query })
+
+        messageCount.value = count || 0
+
+
+        if (Array.isArray(data)) {
+
+            receive.value = [...data, ...receive.value]
+        }
+
+        // 获取到数据后,滚动到底部
+        if (!messageId) {
+            nextTick(() => {
+                setTimeout(() => {
+                    messageBoxRef.value && (messageBoxRef.value.scrollTop = messageBoxRef.value.scrollHeight + 100000)
+                }, 100)
+            })
+        }
+    }, 500)
+
+}
+function convertImg(msgJSONString) {
+    try {
+        const msg = JSON.parse(msgJSONString)
+        if (msg.messageContent == undefined) {
+            return msg
+        } else {
+
+            return msg.messageContent
+        }
+    } catch (e) {
+        return msgJSONString
+    }
+}
+
+function showInfoTurnOnOff() {
+    if (noticeType.value == 1) {
+        showInfo.value = !showInfo.value
+    }
+
+    if (noticeType.value == 2) {
+        showInfo.value = true
+        showGroupSetting.value = !showGroupSetting.value
+    }
+}
+
+
+
+
+// 清除聊天记录
+function onClearMsg() {
+    receive.value = []
+    getChatHistory()
+}
+// 监听输入框的enter事件
+function addEventListenerTextarea() {
+    nextTick(() => {
+        if (inputBoxRef.value) {
+            inputBoxRef.value.removeEventListener('keydown', () => { })
+            inputBoxRef.value.addEventListener('keydown', function (event) {
+                if (event.key === 'Enter') {
+                    event.preventDefault();
+                    sendMessage()
+                }
+            })
+        }
+    })
+}
+
+// 监听消息列表的滚动事件
+function addEventListenerMessage() {
+    nextTick(() => {
+        if (messageBoxRef.value) {
+
+            messageBoxRef.value.removeEventListener('scroll', () => { })
+
+            messageBoxRef.value.addEventListener('scroll', (e) => {
+
+                if (messageBoxRef.value.scrollTop == 0 && receive.value[0]?.id) {
+                    console.log('滚动到顶部了')
+                    console.log(receive.value[0].id)
+                    getChatHistory(receive.value[0].id)
+                }
+            })
+        }
+    })
+}
+
+// 选择发送图片
+function selectImg(evt) {
+
+    const file = evt.target.files[0]
+
+    const { size, type, name } = file
+    const IMIETypes = ['image/jpeg', 'image/png', 'image/gif']
+
+    if (!IMIETypes.includes(type)) {
+        ElMessage.error('请上传图片')
+        return
+    }
+
+    const maxSize = 1024 * 1024 * 20
+
+    if (size > maxSize) {
+        ElMessage.error('图片大小不能超过20M')
+        return
+    }
+
+    const formData = new FormData()
+
+    formData.append('uploadFile', file)
+    formData.append('fieldName', 'messageContent')
+    formData.append('asImage', true)
+
+    request(uploadUrl, { method: 'post', body: formData, }).then((res) => {
+        const { data: { fileUrl } } = res;
+        if (fileUrl) {
+            const msg = {
+                getUserId: getUserId.value,
+                sendUserId: sendUserId.value,
+                specialUserId: '',
+                groupId: groupId.value,
+                messageContent: fileUrl,
+                messageType: 1,
+                noticeType: noticeType.value,
+                object: {
+                    id: getLocalId()
+                }
+            }
+            ws.value.send(JSON.stringify(msg))
+        }
+    })
+}
+
+// 选择表情
+function emojiClick(emoji = '') {
+
+    // 获取光标位置
+    const cursorPos = inputBoxRef.value.selectionStart
+
+    const textBefore = messageContent.value.substring(0, cursorPos);
+    const textAfter = messageContent.value.substring(cursorPos);
+
+    // 插入表情
+    messageContent.value = textBefore + emoji + textAfter;
+
+    nextTick(() => {
+
+        inputBoxRef.value.focus();
+        // 设置光标位置
+        inputBoxRef.value.setSelectionRange(cursorPos + emoji.length, cursorPos + emoji.length)
+    })
+
+}
+const followStatus = ref(0)
+// 获取我与对方的关注情况
+async function isFollow() {
+
+    if (noticeType.value !== 1) return //只有单聊中才需要获取关注情况
+
+    const query = {
+        userId: curConversiton.value.toUserId
+    }
+
+    const { data: status = 0 } = await request('/website/tourGroup/isFollow', { query })
+
+    followStatus.value = status
+}
+// 关注、取关
+async function handleFollow() {
+    if (followStatus.value == 3) return
+    await chatStore.handleFollow(curConversiton.value.toUserId, followStatus.value)
+    isFollow()
+}
+//修改当前消息为已读
+async function updateRead() {
+    const noticeType = curConversiton.value.noticeType
+    if (noticeType == 1 || noticeType == 2) {
+        const query = {groupId:groupId.value}
+        await request('/website/tourMessage/updateRead', { query })
+    }
+
+}
+
+// 监听当前会话的切换
+watch(groupId, async () => {
+    // 消息置空
+    receive.value = []
+
+    // 获取前会话用户的聊天记录
+    getChatHistory()
+
+    //隐藏右侧信息
+    if (noticeType.value == 2) {
+        showInfo.value = true
+        showGroupSetting.value = false
+    } else {
+        showInfo.value = false
+    }
+
+    // 监听消息输入框键盘enter事件
+    addEventListenerTextarea()
+
+    // 监听聊天框消息滚动事件
+    addEventListenerMessage()
+
+    // 获取与当前会话用户的关注状态
+    isFollow()
+
+    updateRead()
+
+})
+watch(onNewMessage, () => {
+    messageBoxRef.value && (messageBoxRef.value.scrollTop = messageBoxRef.value.scrollHeight + 100000)
+})
+onMounted(() => {
+    getChatHistory()
+    addEventListenerTextarea()
+    addEventListenerMessage()
+    isFollow()
+})
+
+</script>
+<style scoped lang="scss">
+.scrollbar::-webkit-scrollbar {
+    width: 0px;
+    height: 0px;
+    background-color: #ccc;
+}
+
+.scrollbar::-webkit-scrollbar-thumb {
+    border-radius: 10px;
+    background-color: #d2d2d2;
+}
+</style>

+ 156 - 0
src/pages/chat/components/ChatHistory.vue

@@ -0,0 +1,156 @@
+<template>
+    <XDialog :title="title || ''" :width="width" :show="show" @close="handleClose">
+        <div ref="msgBoxRef" class="w-full h-[520px] bg-[#fff] overflow-y-auto relative text-wrap">
+            <div class="flex justify-center py-20 sticky top-0 bg-[#fff]" style="z-index:100;">
+                <div class="rounded-full w-[80%] h-[40px] border flex items-center">
+                    <img src="~assets/img/search.png" class="w-[16px] h-[16px] mx-10" alt="">
+                    <input v-model="searchMessage" @input="search" type="text" placeholder="搜索聊天关键字" maxlength="20"
+                        class="outline-none w-[80%]" />
+                </div>
+            </div>
+            <template v-for="(item, index) in list" :key="index">
+                <div v-if="item.messageContent.trim()" class="flex px-20 pb-30 w-full">
+                    <div class="w-[44px] h-[44px] rounded-full overflow-hidden shrink-0">
+                        <img :src="item.headImageUrl" class="w-full h-full object-cover" alt="" />
+                    </div>
+                    <div class="flex-1 pl-15  border-b border-[#eee] pb-30 w-full">
+                        <div class="flex justify-between items-center text-[#666] text-[12px]">
+                            <div>{{ item.showName }}</div>
+                            <div>{{ item.createTime }}</div>
+                        </div>
+                        <template v-if="item.messageType == 0">
+                            <div class="text-wrap text-[14px] text-[#333] mt-20 text-justify w-full">
+                                <!-- {{ item.messageContent }} -->
+                                <span v-html="textHilight(convertMsg(item.messageContent))"></span>
+                            </div>
+                        </template>
+                        <template v-if="item.messageType == 1">
+                            <div class="w-[50%]">
+                                <el-image :preview-src-list="[item.messageContent]" :src="item.messageContent" fit="cover" />
+                            </div>
+                        </template>
+                    </div>
+                </div>
+            </template>
+
+            <div v-if="list.length == 0 && !loading" class="flex flex-col justify-center items-center h-300">
+                <img src="~assets/img/chat/no-chat.png" class="w-[160px] h-[160px]" alt="" />
+                <div class="text-[#737B80] text-[14px] mt-20">暂无聊天记录</div>
+            </div>
+        </div>
+    </XDialog>
+</template>
+<script setup>
+import XDialog from '@/components/XDialog';
+const props = defineProps({
+    title: String,
+    width: String,
+    show: Boolean,
+    groupId: String,
+})
+const chatStore = useChatStore()
+const { curConversiton, user } = storeToRefs(chatStore)
+const msgBoxRef = ref(null)
+const emit = defineEmits(['close'])
+function handleClose() {
+    emit('close')
+}
+const list = ref([])
+const total = ref(0)
+const pageNum = ref(1)
+const pageSize = ref(10)
+const searchMessage = ref('')
+const loading = ref(false)
+async function getList() {
+
+    if (!props.groupId) return
+
+    if (list.value.length >= total.value && list.value.length > 0) {
+        pageNum.value -= 1
+        return
+    }
+
+    const query = {
+        pageNum: pageNum.value,
+        pageSize: pageSize.value,
+        groupId: props.groupId,
+        searchMessage: searchMessage.value
+    }
+
+    if(searchMessage.value){
+        query.messageType = 0
+    }
+
+    loading.value = true
+    const { data: { dataList, totalCount } } = await request('/website/tourMessage/getMessageByGroupIdPage', { query }).finally(() => loading.value = false)
+    total.value = totalCount || 0
+    if (Array.isArray(dataList)) {
+        if (pageNum.value == 1) {
+            list.value = dataList
+        } else {
+            list.value = [...list.value, ...dataList]
+        }
+    }
+
+}
+// 搜索
+const timer = ref(0)
+async function search() {
+    clearTimeout(timer.value)
+    timer.value = setTimeout(async () => {
+        loading.value = true
+        list.value = []
+        pageNum.value = 1
+        total.value = 0
+        getList()
+    }, 500)
+}
+
+// 搜索文字高亮
+function textHilight(str=''){
+    if(searchMessage.value){
+        return str.replace(searchMessage.value,'<span style="color:#FD9A00;font-weight:600;">'+searchMessage.value+'</span>')
+    }
+    return str
+}
+
+function convertMsg(msgJSONString) {
+    try {
+        const msg = JSON.parse(msgJSONString)
+        if (msg.messageContent == undefined) {
+            return msg
+        } else {
+            return msg.messageContent
+        }
+    } catch (e) {
+
+        return msgJSONString
+    }
+}
+// 监听消息滚动触底事件
+function addEventListenermsgBoxRef() {
+    nextTick(() => {
+        if (msgBoxRef.value) {
+            msgBoxRef.value.removeEventListener('scroll', () => { })
+            msgBoxRef.value.addEventListener('scroll', (e) => {
+
+                if (msgBoxRef.value.scrollHeight - (msgBoxRef.value.scrollTop + msgBoxRef.value.clientHeight) <= 2) {
+                    
+                    pageNum.value += 1
+                    getList()
+
+                }
+            })
+        }
+    })
+}
+watch(() => props.groupId, () => {
+    pageNum.value = 1
+    getList()
+    addEventListenermsgBoxRef()
+})
+onMounted(() => {
+    getList()
+    addEventListenermsgBoxRef()
+})
+</script>

+ 168 - 0
src/pages/chat/components/ChatList.vue

@@ -0,0 +1,168 @@
+<template>
+    <div @contextmenu.prevent.stop="handleFriendRightClick($event, item)" @click="handleConversationClick(item)"
+        v-for="(item, index) in conversations" :key="index"
+        class="flex items-center relative justify-between h-[60px] cursor-pointer hover:bg-[#eee] hover:rounded-[8px]"
+        :class="[item.id == curConversiton.id ? 'bg-[#eee]' : '', item.isTop == 1 ? 'bg-[#eee]' : '']">
+        <div v-if="item.noticeType == 1 || item.noticeType == 3"
+            class="w-[44px] h-[44px] bg-[#dedede] rounded-full overflow-hidden shrink-0 mx-[10px] ">
+            <template v-if="item.noticeType == 1 || item.noticeType == 3">
+                <img v-if="item?.headImage" :src="item?.headImage" alt="" class="w-full h-full object-cover">
+            </template>
+
+
+        </div>
+        <template v-if="item.noticeType == 2">
+            <div class="shrink-0 mx-[10px] ">
+                <MultiHeader :size="44" :imgUrls="item.dfGroupImage || []"></MultiHeader>
+            </div>
+
+        </template>
+        <div class="flex flex-col h-[40px] flex-1 justify-between">
+            <div class="flex justify-between items-center">
+                <div class="text-[14px] font-bold truncate w-[150px]">
+                    {{ item?.groupRemark }}
+                </div>
+                <div class="text-[12px] text-[#999] shrink-0 mr-[10px]">{{
+                    formatTimestamp(item?.lastMessage?.createTime) }}
+                </div>
+            </div>
+            <div class="flex justify-between items-center">
+                <div class="text-[12px] truncate w-[150px] text-[#999]">
+                    <template v-if="item?.lastMessage?.messageType == 0">
+                        {{ convertMsg(item?.lastMessage?.messageContent) }}
+                    </template>
+                    <template v-if="item?.lastMessage?.messageType == 1">
+                        [图片]
+                    </template>
+                </div>
+                <div v-if="item?.unreadMessageCount"
+                    class="text-[12px] text-[#fff] shrink-0 mr-[10px] bg-[#f40]  rounded-full px-[5px]">
+                    {{ item?.unreadMessageCount }}
+                </div>
+            </div>
+        </div>
+    </div>
+    <!-- 右键菜单 -->
+    <ContextMenu :show="showContextMenu" :conversation="conversationToAction" :x="contextmenuX" :y="contextmenuY"
+        @close="showContextMenu = false">
+    </ContextMenu>
+</template>
+<script setup>
+import ContextMenu from './ContextMenu.vue';
+
+
+const chatStore = useChatStore()
+const { messages, conversations, curConversiton, connectSta } = storeToRefs(chatStore)
+
+const wsConnect = computed(() => connectSta.value)
+// 会话id
+const groupId = computed(() => curConversiton.value.groupId)
+
+const showContextMenu = ref(false)
+
+const contextmenuX = ref(0)
+const contextmenuY = ref(0)
+
+// 右键菜单要操作的会话
+const conversationToAction = ref({})
+function handleFriendRightClick(evt, conver) {
+    const { groupId } = conver
+
+    // 关注消息、互动消息、系统消息 不显示右键菜单
+    if (groupId < 0) return
+
+    const { pageX, pageY } = evt
+    contextmenuX.value = pageX
+    contextmenuY.value = pageY
+    showContextMenu.value = true
+    conversationToAction.value = conver
+    evt.preventDefault()
+}
+function convertMsg(msgJSONString) {
+    try {
+        const msg = JSON.parse(msgJSONString)
+        return msg.messageContent
+    } catch (e) {
+        return msgJSONString
+    }
+}
+// 选择会话
+function handleConversationClick(item) {
+
+    // receive.value = []
+    curConversiton.value = item
+}
+function formatTimestamp(timestamp) {
+    const now = new Date();
+    const targetDate = new Date(timestamp);
+
+    // 时间差
+    const timeDifference = now - targetDate;
+
+    if (isNaN(timeDifference)) {
+        return ''
+    }
+
+    // 年月日时分
+    const minutes = Math.floor(timeDifference / 1000 / 60);
+    const hours = targetDate.getHours();
+    const day = targetDate.getDate();
+    const month = targetDate.getMonth() + 1; // 月份从0开始,所以加1
+    const year = targetDate.getFullYear();
+
+    // 获取当前日期的年月日
+    const currentYear = now.getFullYear();
+    const currentMonth = now.getMonth() + 1;
+    const currentDay = now.getDate();
+
+    // 补零函数
+    const padZero = (num) => num.toString().padStart(2, '0');
+
+    // 小于1分钟
+    if (minutes < 1) {
+        return '刚刚';
+    }
+
+    // 同一天的不同时间段
+    if (year === currentYear && targetDate.getDate() === currentDay) {
+        const formattedHour = padZero(hours);
+        const formattedMinute = padZero(targetDate.getMinutes());
+
+        if (hours < 6) {
+            return `凌晨 ${formattedHour}:${formattedMinute}`;
+        } else if (hours < 12) {
+            return `上午 ${formattedHour}:${formattedMinute}`;
+        } else if (hours < 18) {
+            return `下午 ${formattedHour - 12}:${formattedMinute}`;
+        } else {
+            return `晚上 ${formattedHour - 12}:${formattedMinute}`;
+        }
+    }
+
+    // 昨天
+    const yesterday = new Date(now);
+    yesterday.setDate(now.getDate() - 1);
+    if (year === currentYear && targetDate.getDate() === yesterday.getDate()) {
+        return '昨天';
+    }
+
+    // 同年
+    if (year === currentYear) {
+        return `${month}月${day}日`;
+    }
+
+    // 大于一年
+    return `${year}年${padZero(month)}月${padZero(day)}日`;
+}
+watch(groupId, () => {
+    // 清除未读消息
+    if (wsConnect.value == 2) {
+        for (let i = 0; i < conversations.value.length; i++) {
+            if (conversations.value[i].groupId == groupId.value) {
+                conversations.value[i].unreadMessageCount = 0
+                break;
+            }
+        }
+    }
+})
+</script>

+ 202 - 0
src/pages/chat/components/Complain.vue

@@ -0,0 +1,202 @@
+<template>
+    <XDialog :title="title || ''" :width="width" :show="show" @close="handleClose">
+        <div class="p-20">
+            <div class="text-[#333] text-[14px]">举报类型<span class="text-[#f40]">*</span></div>
+            <div class="w-full ">
+                <el-row v-for="(item, index) in complainList" :key="index" class="mt-20">
+                    <template v-for="(subItem, subIndex) in item" :span="8" :key="subIndex">
+                        <el-col :span="6">
+                            <div @click="handleComplainClick(subItem.id)" class="flex items-center cursor-pointer ">
+                                <div class="text-[#333] text-[14px] mx-8 ">
+                                    {{ subItem.typeName }}
+                                </div>
+                                <img v-if="subItem.isChecked" class="w-[21px] h-[21px]"
+                                    src="~assets/img/chat/radio_circle_orange.png" alt="">
+                                <img v-else class="w-[21px] h-[21px]" src="~assets/img/chat/radio_circle_gray.png"
+                                    alt="" />
+                            </div>
+                        </el-col>
+                    </template>
+                </el-row>
+                <div v-if="selectedComplainType.description"
+                    class="p-10 bg-[#FAFAFA] rounded mt-10 text-[#666] text-[12px]">
+                    {{ selectedComplainType?.description }}
+                </div>
+            </div>
+            <div class="text-[#333] text-[14px] mt-20">举报描述<span class="text-[#f40]">*</span></div>
+            <div class="p-10 bg-[#FAFAFA] rounded mt-10">
+                <textarea v-model="content" maxlength="200"
+                    class="w-full h-[100px] resize-none bg-[#fafafa] outline-none"
+                    placeholder="请尽可能的描述存在的问题,如:让您感到不适的面面、或其他未提及的违规内容"></textarea>
+                <div class="text-right text-[#999] text-[12px]">
+                    {{ content.length }}/200
+                </div>
+            </div>
+            <div class="text-[#333] text-[14px] mt-20">图片描述<span class="text-[#f40]">*</span></div>
+            <div class="flex items-center cursor-pointer flex-wrap mt-10">
+
+                <div v-for="(item, index) in files" :key="index"
+                    class="w-[80px] h-[80px] rounded mr-20 relative rounded-[8px] overflow-hidden">
+                    <img :src="item.localUrl" class="w-full h-full object-cover" />
+                    <div @click="delImg(index)"
+                        class="absolute top-0 right-0 w-[20px] h-[20px] text-[#fff] flex itemc-center justify-center"
+                        style="background:rgba(0,0,0,0.4);border-bottom-left-radius:8px;">
+                        x
+                    </div>
+                </div>
+                <div v-if="files.length < 3"
+                    class="text-[#929292] relative  w-[80px] bg-[#fafafa] h-[80px] rounded flex items-center justify-center">
+                    <span style="font-size:40px">+</span>
+                    <input @change="fileChange" type="file" class="absolute top-0 left-0 w-[80px] h-[80px]"
+                        style="opacity: 0;" />
+                </div>
+            </div>
+            <div class="flex items-center justify-end text-[14px] cursor-pointer mt-20">
+                <div
+                    class="w-[80px] mr-20 h-[32px] flex items-center justify-center border border-[#666] rounded text-[#666]">
+                    取消
+                </div>
+                <div @click="confirm"
+                    class="w-[80px] h-[32px] flex items-center justify-center bg-[#FD9A00] rounded text-[#fff]">
+                    提交
+                </div>
+            </div>
+        </div>
+    </XDialog>
+</template>
+<script setup>
+import XDialog from '@/components/XDialog';
+const props = defineProps({
+    title: String,
+    width: String,
+    show: Boolean,
+    groupId: String,
+})
+const emit = defineEmits(['close'])
+function handleClose() {
+    releaseUrl()
+    emit('close')
+}
+const content = ref('')
+const complainList = ref([])
+const selectedComplainType = ref({})
+
+// 获取投诉类型
+async function getTypeList() {
+    const { data } = await request('/website/tourComplaintType/getTypeList')
+
+    if (!Array.isArray(data)) return
+
+    const arr = []
+    const chunkSize = 4;
+    for (let i = 0; i < data.length; i += chunkSize) {
+        arr.push(data.slice(i, i + chunkSize))
+    }
+    complainList.value = arr
+}
+function handleComplainClick(id) {
+    for (let i = 0; i < complainList.value.length; i++) {
+        for (let j = 0; j < complainList.value[i].length; j++) {
+            if (complainList.value[i][j].id == id) {
+                complainList.value[i][j].isChecked = true
+                selectedComplainType.value = complainList.value[i][j]
+            } else {
+                complainList.value[i][j].isChecked = false
+            }
+        }
+    }
+}
+
+const files = ref([])
+// 选择图片
+function fileChange(evt) {
+    if (files.value.length >= 3) {
+        ElMessage.error('最多上传三张图片')
+        return
+    }
+
+    const file = evt.target.files[0]
+    const { size, type } = file
+    const IMIETypes = ['image/jpeg', 'image/png', 'image/gif']
+
+    if (!IMIETypes.includes(type)) {
+        ElMessage.error('请上传图片')
+        return
+    }
+
+    const maxSize = 1024 * 1024 * 5
+
+    if (size > maxSize) {
+        ElMessage.error('图片大小不能超过2M')
+        return
+    }
+    if (files.value.length < 3) {
+        files.value.push({ file, localUrl: URL.createObjectURL(file) })
+    } else {
+        ElMessage.error('最多上传三张图片')
+    }
+    console.log('imgs:', files.value)
+}
+
+
+function delImg(index) {
+    URL.revokeObjectURL(files.value[index].localUrl)
+    files.value.splice(index, 1)
+}
+const images = ref([])
+async function upload(file) {
+    
+        const formData = new FormData()
+        formData.append('uploadFile', file)
+        formData.append('fieldName', 'image')
+        formData.append('asImage', true)
+        const { data: { fileUrl } } =await request('/website/tourComplait/upload', { method: 'post', body: formData, })
+        return fileUrl
+}
+async function confirm() {
+    if (!props.groupId) return
+    const { id: typeId } = selectedComplainType.value
+    if (!typeId) {
+        ElMessage.error('请选择投诉类型')
+        return
+    }
+    if(!content.value){
+        ElMessage.error('请填写投诉内容')
+        return
+    }
+    if (!files.value.length) {
+        ElMessage.error('请上传图片')
+        return
+    }
+
+    const image = await Promise.all([...files.value.map(async item=>await upload(item.file))])
+    console.log(image)
+    const data = {
+        typeId,
+        description: content.value,
+        objectType: 2,
+        image:image,
+        groupId: props.groupId
+    }
+    ElLoading.service({
+        lock: true,
+        text: '正在提交...',
+        background: 'rgba(0, 0, 0, 0.1)'
+    })
+    await request('/website/tourComplait/add', { method: 'post', body: data }).finally(()=>ElLoading.service().close())
+    ElMessage.success('投诉成功!')
+    emit('close')
+}
+// 释放内存
+function releaseUrl() {
+    files.value.forEach(file => {
+        URL.revokeObjectURL(file.localUrl)
+    })
+}
+onMounted(() => {
+    getTypeList()
+})
+onUnmounted(() => {
+    releaseUrl()
+})
+</script>

+ 47 - 0
src/pages/chat/components/ContextMenu.vue

@@ -0,0 +1,47 @@
+<template>
+    <div v-if="show" @click="handleClose" @contextmenu.prevent.stop="handleRightClick($event)" class="w-[100vw] h-[100vh] bg-[#000]/0 z-[999] fixed top-0 left-0">
+        <div @click.stop="" :style="{left: `${x}px`, top: `${y}px`,boxShadow: '0px 4px 4px 0px rgba(0,0,0,0.25)'}" class="w-[100px] absolute py-10 bg-[#fff] border cursor-pointer rounded-[8px]">
+            <div v-if="conversation?.isTop == 0" @click="chatStore.setConversation({isTop:1,groupId:conversation?.groupId}),handleClose()" class="h-[35px] flex items-center justify-center text-[12px] text-[#333] hover:bg-[#FD9A00]/10 hover:text-[#FD9A00]">
+                置顶聊天
+            </div>
+            <div v-if="conversation?.isTop == 1" @click="chatStore.setConversation({isTop:0,groupId:conversation?.groupId}),handleClose()" class="h-[35px] flex items-center justify-center text-[12px] text-[#333] hover:bg-[#FD9A00]/10 hover:text-[#FD9A00]">
+                取消置顶
+            </div>
+            <!-- <div class="h-[35px] flex items-center justify-center text-[12px] text-[#333] hover:bg-[#FD9A00]/10 hover:text-[#FD9A00]">
+                不显示聊天
+            </div> -->
+            <div @click="chatStore.setConversation({isShow:0,groupId:conversation?.groupId}),handleClose()" class="h-[35px] flex items-center justify-center text-[12px] text-[#333] hover:bg-[#FD9A00]/10 hover:text-[#FD9A00]">
+                删除聊天
+            </div>
+        </div>
+    </div>
+</template>
+<script setup>
+const chatStore = useChatStore()
+const { ws, curConversiton, receive, receiveGetter, connectSta, onNewMessage } = storeToRefs(chatStore)
+const user = computed(() => chatStore.user)
+
+const props = defineProps({
+    show: Boolean,
+    x: {
+        type: Number,
+        default: 100
+    },
+    y: {
+        type: Number,
+        default: 0
+    },
+    conversation:Object
+})
+const emit = defineEmits(['close'])
+function handleClose() {
+    emit('close')
+}
+function handleRightClick(evt) {
+    handleClose()
+    evt.preventDefault()
+}
+onMounted(() => {
+    console.log(props.conversation)
+})
+</script>

+ 388 - 0
src/pages/chat/components/CreateGroup.vue

@@ -0,0 +1,388 @@
+<template>
+    <XDialog :title="title || ''" :width="width" :show="show" @close="handleClose">
+        <div class="w-full bg-[#fff]">
+            <div v-if="showConfig" class="px-20 w-full">
+                <div class="text-[#666] text-[14px] mt-20">群聊名称<span class="text-[#f40]">*</span></div>
+                <div class="w-full border border-[#E7E7E7] rounded h-[40px] mt-8 overflow-hidden">
+                    <input v-model="groupName" class="w-full h-full outline-none px-10" type="text" maxlength="30" placeholder="请输入群聊名称" />
+                </div>
+                <div class="text-[#666] text-[14px] mt-20">群介绍<span class="text-[#f40]">*</span></div>
+                <div class="w-full h-[100px] text-[#333] rounded mt-8 overflow-hidden">
+                    <textarea v-model="description" class="w-full h-full outline-none bg-[#fafafa] p-10 resize-none text-[12px]"
+                        maxlength="200" placeholder="请输入群介绍"></textarea>
+                </div>
+                <div class="text-[#666] text-[14px] mt-20">群类型</div>
+                <div class="w-full ">
+                    <template v-for="(item, index) in groupTypeList" :key="index">
+                        <el-row class="mt-20">
+                            <template v-for="(subItem, subIndex) in item" :span="8" :key="subIndex">
+                                <el-col :span="8">
+                                    <div @click="handleTypeClick(subItem.id)" class="flex items-center cursor-pointer">
+                                        <img :src="subItem.typeIcon" class="w-[16px] h-[16px]" alt="" />
+                                        <span class="text-[#333] text-[14px] mx-8">{{ subItem.typeName }}</span>
+                                        <img v-if="subItem.isChecked" class="w-[21px] h-[21px]"
+                                            src="~assets/img/chat/radio_circle_orange.png" alt="">
+                                        <img v-else class="w-[21px] h-[21px]"
+                                            src="~assets/img/chat/radio_circle_gray.png" alt="" />
+                                    </div>
+                                </el-col>
+                            </template>
+                        </el-row>
+                        <template v-for="(subItem, subIndex) in item" :key="subIndex">
+                            <div v-if="subTypeList.children && subTypeList.children.length && subItem.id == subTypeList.id"
+                                class="flex items-center flex-wrap bg-[#fafafa] pb-10 mt-10 rounded-[4px]">
+                                <div @click="handleSubTypeClick(tag.id)" v-for="(tag, tagIndex) in subTypeList.children"
+                                    :key="tagIndex"
+                                    class="px-10 py-4 border rounded text-[#333] text-[14px] mx-10 mt-10 cursor-pointer"
+                                    :class="[tag.isChecked ? 'bg-[#FD9A00]/[.06] text-[#FD9A00]' : '']">
+                                    {{ tag.typeName }}
+                                </div>
+                            </div>
+                        </template>
+                    </template>
+                </div>
+                <div class="flex justify-between items-center mt-20">
+                    <div >
+                        <div class="text-[#333] text-[14px]">
+                            个人主页展示
+                        </div>
+                        <div class="text-[#666] text-[10px]">
+                            开启后,在群聊广场和个人主页展示
+                        </div>
+                    </div>
+                    <el-switch v-model="isPublic" />
+                </div>
+                <div class="h-[70px] items-center justify-end w-full flex  mt-10 cursor-pointer">
+                    <div @click="handleClose"
+                        class="w-[80px] h-[32px] flex mr-20 items-center justify-center border border-[#666] rounded text-[#666]">
+                        取消
+                    </div>
+                    <div @click="createNullGroup" class="w-[80px] h-[32px] flex items-center justify-center rounded text-[#fff] bg-[#FD9A00]">
+                        创建
+                    </div>
+                </div>
+            </div>
+            <div v-else class="mt-20 border-t-[1px] border-[#eee] flex justify-between">
+                <div ref="frendsBoxRef"
+                    class="w-[280px] h-[300px] border-r-[1px] border-[#eee] relative overflow-y-auto scrollbar">
+                    <div class="px-20 sticky top-0 bg-[#fff]">
+                        <div class="h-[15px]"></div>
+                        <div class="flex items-center border rounded-full h-[35px]">
+                            <img src="~assets/img/search.png" class="w-[16px] h-[16px] mx-10" alt="">
+                            <input v-model="keyword" @input="search" class="outline-none" type="text" placeholder="搜索互关好友">
+                        </div>
+                    </div>
+                    <div class="mt-5 pb-20">
+                        <div v-if="curConversiton?.noticeType == 1"
+                            class="flex items-center flex-warp text-wrap hover:bg-[#eee] px-20 py-10 cursor-pointer">
+                            <img class="w-[21px] h-[21px]" src="~assets/img/chat/radio_circle_orange.png" alt="" />
+                            <div class="w-[24px] h-[24px] rounded-full overflow-hidden mx-10">
+                                <img :src="curConversiton?.headImage" class="w-full h-full object-cover" alt="" />
+                            </div>
+                            <span>{{ curConversiton?.groupRemark }}</span>
+                        </div>
+                        <template v-for="(item, index) in searchResult" :key="item.id">
+                            <div v-if="item?.attentionIdDictMap?.userId != curConversiton.toUserId"
+                                @click="friendClick(item, index)"
+                                class="flex items-center flex-warp text-wrap hover:bg-[#eee] px-20 py-10 cursor-pointer">
+                                <img v-if="item.isChecked" class="w-[21px] h-[21px]"
+                                    src="~assets/img/chat/radio_circle_orange.png" alt="">
+                                <img v-else class="w-[21px] h-[21px]" src="~assets/img/chat/radio_circle_gray.png"
+                                    alt="" />
+                                <div class="w-[24px] h-[24px] rounded-full overflow-hidden mx-10">
+                                    <img :src="item?.attentionIdDictMap?.headImageUrl" class="w-full h-full object-cover" alt="" />
+                                </div>
+                                <span>{{ item?.attentionIdDictMap?.showName }}</span>
+                            </div>
+                        </template>
+
+                        <div v-if="friends.length >= totalCount" class="text-[#999] text-[12px] text-center">
+                            没有更多了~
+                        </div>
+                    </div>
+                </div>
+                <div class="w-[280px] h-[300px]">
+                    <div class="h-[40px] flex items-center justify-between px-10">
+                        <div class="text-[#000] text-[14px]">选择好友</div>
+                        <div v-if="Array.isArray(friends)" class="text-[#999] text-[12px]">
+                            已选择{{ friends.filter(item => item.isChecked).length +1 }}个好友</div>
+                    </div>
+                    <div class="h-[180px] overflow-y-auto scrollbar px-10">
+                        <div class="flex flex-wrap w-full">
+
+                            <div v-if="curConversiton?.noticeType == 1" class="w-[25%] mt-10">
+                                <div class="w-[50%] aspect-[1/1] rounded-full mx-auto relative cursor-pointer">
+                                    <img :src="curConversiton?.headImage"
+                                        class="w-full h-full object-cover rounded-full" alt="" />
+                                </div>
+                                <div class="truncate w-full text-[12px] text-[#333] text-center">
+                                    {{ curConversiton?.groupRemark }}
+                                </div>
+                            </div>
+                            <template v-for="item in selectedFriends" :key="item.id">
+                                <div v-if="item.isChecked" class="w-[25%] mt-10">
+                                    <div class="w-[50%] aspect-[1/1] rounded-full mx-auto relative cursor-pointer">
+                                        <img :src="item?.attentionIdDictMap?.headImageUrl"
+                                            class="w-full h-full object-cover rounded-full" alt="" />
+                                        <div @click="item.isChecked = false"
+                                            class="absolute top-[-5px] right-[-5px] w-[15px] h-[15px] bg-[#999] flex items-center justify-center text-[#fff] text-[10px] rounded-full">
+                                            x
+                                        </div>
+                                    </div>
+                                    <div class="truncate w-full text-[12px] text-[#333] text-center">
+                                        {{ item?.attentionIdDictMap?.showName }}
+                                    </div>
+                                </div>
+                            </template>
+                        </div>
+                    </div>
+                    <div class="h-[70px] cursor-pointer items-center justify-around w-full flex border-t-[1px] border-[#eee] mt-10">
+                        <div @click="handleClose"
+                            class="w-[80px] h-[32px] flex items-center justify-center border border-[#666] rounded text-[#666]">
+                            取消
+                        </div>
+                        <div @click="createGroup"
+                            class="w-[80px] h-[32px] flex items-center justify-center rounded text-[#fff] bg-[#FD9A00]">
+                            创建
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </XDialog>
+</template>
+<script setup>
+import XDialog from '@/components/XDialog';
+
+const props = defineProps({
+    title: String,
+    width: String,
+    show: Boolean,
+    showConfig: {
+        type: Boolean,
+        default: false
+    }
+})
+const emit = defineEmits(['close'])
+const frendsBoxRef = ref(null)
+const chatStore = useChatStore()
+const { messages, reqChatList, ws, curConversiton, receive, receiveGetter, isConnect } = storeToRefs(chatStore)
+function handleClose() {
+    emit('close')
+}
+const isPublic = ref(false)
+const description = ref('')
+const groupName = ref('')
+const groupTypeList = ref([])
+
+const subTypeList = ref({})
+function handleTypeClick(typeId) {
+    for (let i = 0; i < groupTypeList.value.length; i++) {
+        for (let x = 0; x < groupTypeList.value[i].length; x++) {
+            if (groupTypeList.value[i][x].id == typeId) {
+                console.log('sid:', subTypeList.id)
+                if (typeId == subTypeList.value.id) {
+                    subTypeList.value = {}
+                    groupTypeList.value[i][x].isChecked = false
+                } else {
+                    groupTypeList.value[i][x].isChecked = true
+                    subTypeList.value = groupTypeList.value[i][x]
+                    console.log('subTypeList:', subTypeList.value)
+                }
+
+            } else {
+                groupTypeList.value[i][x].isChecked = false
+            }
+        }
+    }
+}
+function handleSubTypeClick(typeId) {
+    if (subTypeList.value.children && subTypeList.value.children.length) {
+        const list = subTypeList.value.children.map(item => {
+            if (item.id == typeId) {
+                item.isChecked = true
+                return item
+            } else {
+                item.isChecked = false
+                return item
+            }
+        })
+        subTypeList.value.children = list;
+        console.log('subTypeList.value:', subTypeList.value)
+    }
+}
+
+// 获取群类型(树形数据)
+async function treeType() {
+    const { data } = await request('/website/tourGroupType/treeType')
+
+    if (!Array.isArray(data)) return
+
+    const arr = []
+    const chunkSize = 3;
+    for (let i = 0; i < data.length; i += chunkSize) {
+        arr.push(data.slice(i, i + chunkSize))
+    }
+    groupTypeList.value = arr
+}
+
+const friends = ref([])
+const selectedFriends = ref([])
+// 获取互关列表
+const loading = ref(false)
+const totalCount = ref(0)
+const pageNum = ref(1)
+const pageSize = ref(10)
+const noMore = ref(false)
+const searchResult = ref([])
+const keyword = ref('')
+async function getFansList() {
+    if (friends.value.length >= totalCount.value && totalCount.value > 0) return
+
+    loading.value = true
+
+    const { data } = await request('/website/tourism/fans/getFriends', { query: { flagPage:1 } }).finally(() => loading.value = false)
+
+    const { dataList, totalCount: total } = data
+
+    if (!Array.isArray(dataList)) return
+
+    totalCount.value = total
+    friends.value = [...dataList]
+    searchResult.value = [...dataList]
+    noMore.value = true
+    // if (pageNum.value == 1) {
+    //     friends.value = [...dataList]
+    // } else {
+    //     friends.value = [...friends.value, ...dataList]
+    // }
+
+    // if (friends.value.length >= totalCount.value) {
+    //     noMore.value = true
+    //     frendsBoxRef.value.removeEventListener('scroll', () => { })
+    // }
+    
+}
+async function search(){
+    if(!keyword.value) {
+        searchResult.value = [...friends.value]
+        return
+    }
+    let result = []
+    friends.value.map(item=>{
+        if(item?.attentionIdDictMap?.showName.indexOf(keyword.value) != -1){
+            result.push(item)
+        }
+    })
+    searchResult.value = result
+}
+function friendClick(friend, index) {
+
+    const { attentionIdDictMap } = friend || {}
+    const { userId } = attentionIdDictMap || {}
+    if (curConversiton.value?.noticeType == 1 && userId == curConversiton.value?.toUserId) {
+        friend.isChecked = true
+    } else {
+        friend.isChecked = friend.isChecked ? false : true
+    }
+    friends.value[index] = friend
+
+    const arr = friends.value.filter(item => item.isChecked)
+    selectedFriends.value = arr
+}
+async function createGroup() {
+
+    const ids = []
+
+    selectedFriends.value.map(item => ids.push(item?.attentionIdDictMap?.userId))
+
+    if (curConversiton.value?.noticeType == 1) {
+        ids.push(curConversiton.value.toUserId)
+    }
+
+    if (!ids.length) return
+
+    const data = {
+        createType: 2,
+        ids,
+    }
+    
+    await request('/website/tourGroup/createGroup', { method: 'post', body: data })
+    ElMessage.success('创建成功')
+    chatStore.reqChatList()
+    handleClose()
+}
+async function createNullGroup(){
+
+    console.log('subTypeList.value:', subTypeList.value)
+    if(!groupName.value.trim()){
+        ElMessage.error('请输入群名称')
+        return
+    }
+    
+    if(!subTypeList.value.id) {
+        ElMessage.error('请选择群类型')
+        return
+    }
+    
+    const data = {
+        createType: 1,
+        isPublic:isPublic.value?1:0,
+        belongTypeId:'',
+        groupName:groupName.value,
+        description:description.value
+    }
+
+    if(subTypeList.value?.children?.length){
+        data.belongTypeId = subTypeList.value.children.filter(item=>item.isChecked)[0]?.id
+    }else{
+        data.belongTypeId = subTypeList.value.id
+    }
+
+    if(!data.belongTypeId) {
+        ElMessage.error('请选择群类型')
+        return
+    }
+
+    await request('/website/tourGroup/createGroup', { method: 'post', body: data })
+
+    ElMessage.success('创建成功')
+    chatStore.reqChatList()
+    handleClose()
+}
+// 监听互关好友列表的滚动事件
+function addEventListenerFrendsBoxRef() {
+    nextTick(() => {
+        if (frendsBoxRef.value) {
+            frendsBoxRef.value.removeEventListener('scroll', () => { })
+            frendsBoxRef.value.addEventListener('scroll', (e) => {
+
+                if (frendsBoxRef.value.scrollHeight - (frendsBoxRef.value.scrollTop + frendsBoxRef.value.clientHeight) <= 2) {
+
+                    pageNum.value += 1
+                    getFansList()
+                }
+            })
+        }
+    })
+}
+onMounted(() => {
+    treeType()
+    getFansList()
+    addEventListenerFrendsBoxRef()
+
+})
+</script>
+<style scoped lang="scss">
+.scrollbar::-webkit-scrollbar {
+    width: 1px;
+    height: 1px;
+    background-color: #eee;
+}
+
+.scrollbar::-webkit-scrollbar-thumb {
+    border-radius: 10px;
+    //   -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
+    background-color: #FD9A00;
+}
+</style>

+ 309 - 0
src/pages/chat/components/FansList.vue

@@ -0,0 +1,309 @@
+<template>
+    <div ref="fansRef" class="w-full h-full overflow-y-auto scrollbar rounded relative">
+
+        <template v-if="type == constant.ADD_FOLLOW">
+            <div class="flex items-center justify-center w-full p-[10px] sticky top-0 bg-[#fff]" style="z-index:100">
+                <div class="h-[40px] flex items-center justify-between border rounded-full w-full">
+                    <img src="~assets/img/search.png" class="w-[16px] h-[16px] shrink-0 mx-10" alt="" />
+                    <input @input="search" v-model="showName" class="flex-1 h-full outline-none" type="text" />
+                    <img @click="showName='',search()" src="~assets/img/chat/close.png" class="w-[16px] h-[16px] shrink-0 mx-10" alt="" />
+                </div>
+            </div>
+            <div @click="searchClick(item)" v-for="(item, index) in searchList" :key="index"
+                class="flex items-center relative justify-between h-[60px] cursor-pointer hover:bg-[#eee] hover:rounded-[8px]">
+                <div class="w-[44px] h-[44px] bg-[#dedede] rounded-full overflow-hidden shrink-0 mx-[10px]">
+                    <img :src="item?.tourUserVo?.headImageUrl" alt="" class="w-full h-full object-cover">
+                </div>
+                <div class="flex flex-col h-[40px] flex-1 justify-between">
+                    <div class="flex justify-between items-center">
+                        <div class="text-[14px] font-bold truncate w-[150px]">{{ item?.tourUserVo?.showName }}
+                        </div>
+                    </div>
+                    <div class="flex justify-between items-center">
+                        <div class="text-[12px] truncate w-[150px] text-[#999]">{{ transNum(item?.fansCount) }}粉丝</div>
+
+                    </div>
+                </div>
+                <div class="mr-[10px]">
+                    <div @click.stop="follow(item?.tourUserVo.userId, item?.fansStatus)"
+                        class="w-[72px] h-[32px] rounded text-[#fff] text-[14px] flex items-center justify-center"
+                        :class="['bg-[' + FANS_STATUS[item.fansStatus]?.bg + ']']">
+                        {{ FANS_STATUS[item.fansStatus]?.text }}
+                    </div>
+                </div>
+            </div>
+        </template>
+        <template v-if="type == constant.FOLLOW_LIST || type == constant.FANS_EACH_OTHER">
+            <div @click="followClick(fans)" v-for="(fans, index) in fansList" :key="index" class="flex items-center relative justify-between h-[60px] cursor-pointer hover:bg-[#eee] hover:rounded-[8px]">
+                <div class="w-[44px] h-[44px] bg-[#dedede] rounded-full overflow-hidden shrink-0 mx-[10px]">
+                    <img :src="fans?.attentionIdDictMap?.avatar || fans?.attentionIdDictMap?.headImageUrl" alt="" class="w-full h-full object-cover">
+                </div>
+                <div class="flex flex-col h-[40px] flex-1 justify-between">
+                    <div class="flex justify-between items-center">
+                        <div class="text-[14px] font-bold truncate w-[150px]">{{ fans?.attentionIdDictMap?.name || fans?.attentionIdDictMap?.showName }}
+                        </div>
+                    </div>
+                    <div class="flex justify-between items-center">
+                        <div class="text-[12px] truncate w-[150px] text-[#999]">{{ transNum(fans?.fansNum) }}粉丝</div>
+
+                    </div>
+                </div>
+                <div class="mr-[10px]">
+                    <div @click.stop="follow(fans?.attentionId)"
+                        class="w-[72px] h-[32px] rounded text-[#fff] text-[14px] flex items-center justify-center"
+                        :class="['bg-[' + FANS_STATUS[fans.fansStatus]?.bg + ']']">
+                        {{ FANS_STATUS[fans.fansStatus]?.text }}
+                    </div>
+                </div>
+            </div>
+        </template>
+        <!-- 我的粉丝 -->
+        <template v-if="type == constant.FANS_LIST">
+            <div @click="fansClick(fans)" v-for="(fans, index) in fansList" :key="index"
+                class="flex items-center relative justify-between h-[60px] cursor-pointer hover:bg-[#eee] hover:rounded-[8px]">
+                <div class="w-[44px] h-[44px] bg-[#dedede] rounded-full overflow-hidden shrink-0 mx-[10px]">
+                    <img :src="fans?.createUserIdDictMap?.avatar" alt="" class="w-full h-full object-cover">
+                </div>
+                <div class="flex flex-col h-[40px] flex-1 justify-between">
+                    <div class="flex justify-between items-center">
+                        <div class="text-[14px] font-bold truncate w-[150px]">{{ fans?.createUserIdDictMap?.name }}
+                        </div>
+                    </div>
+                    <div class="flex justify-between items-center">
+                        <div class="text-[12px] truncate w-[150px] text-[#999]">{{ transNum(fans?.fansNum) }}粉丝</div>
+
+                    </div>
+                </div>
+                <div class="mr-[10px]">
+                    <div @click.stop="follow(fans?.createUserIdDictMap?.id)"
+                        class="w-[72px] h-[32px] rounded text-[#fff] text-[14px] flex items-center justify-center"
+                        :class="['bg-[' + FANS_STATUS[fans.fansStatus]?.bg + ']']">
+                        {{ FANS_STATUS[fans.fansStatus]?.text }}
+                    </div>
+                </div>
+            </div>
+        </template>
+        <div v-if="loading" class="text-center text-[#999] text-[14px] pb-30">
+            加载中...
+        </div>
+        <div v-if="noMore && !loading" class="text-center text-[#999] text-[14px] pb-20">
+            <div v-if="!searchList.length && type == constant.ADD_FOLLOW" class="mt-100">
+                没有数据
+            </div>
+            <div v-else-if="!fansList.length && type !== constant.ADD_FOLLOW" class="mt-100">
+                没有数据
+            </div>
+        </div>
+    </div>
+</template>
+<script setup>
+
+import constant from '../const.js';
+
+const chatStore = useChatStore()
+const { curConversiton, chatList, ws, user } = storeToRefs(chatStore)
+const dayjs = useDayjs()
+const emit = defineEmits(['chat'])
+const FANS_STATUS = {
+    0: { status: 0, text: '关注', describe: '未关注', bg: '#FD9A00' },
+    1: { status: 1, text: '已关注', describe: '已关注', bg: '#999' },
+    2: { status: 2, text: '相互关注', describe: '相互关注', bg: '#999' },
+    3: { status: 2, text: '自己', describe: '自己', bg: '#999' },
+    4: { status: 4, text: '回关', describe: '被关注', bg: '#FD9A00' }
+}
+const props = defineProps({
+    type: { type: String, default: constant.FANS_LIST },
+})
+
+// 搜索用户关键字
+const showName = ref('')
+
+const listUrl = computed(() => {
+    switch (props.type) {
+        case constant.FANS_LIST:
+            return '/website/tourism/fans/getMyFans'
+        case constant.FOLLOW_LIST:
+            return '/website/tourism/fans/getMyConCern'
+        case constant.FANS_EACH_OTHER:
+            return '/website/tourism/fans/getFriends'
+        default:
+            return '/website/tourism/fans/getMyFans'
+    }
+})
+const fansRef = ref(null)
+const loading = ref(false)
+const noMore = ref(false)
+const fansList = ref([])
+const pageNum = ref(1)
+const pageSize = ref(10)
+const totalCount = ref(0)
+
+watch(() => props.type, () => {
+    pageNum.value = 1
+    fansList.value = []
+    totalCount.value = 0
+    getData()
+})
+
+// 获取 粉丝/关注/互关 列表
+async function getFansList() {
+    if (fansList.value.length >= totalCount.value && totalCount.value > 0) return
+
+    loading.value = true
+
+    const { data } = await request(listUrl.value, { query: { pageNum: pageNum.value, pageSize: pageSize.value } }).finally(() => loading.value = false)
+
+    const { dataList, totalCount: total } = data
+
+    if (!Array.isArray(dataList)) return
+
+    totalCount.value = total
+
+    if (pageNum.value == 1) {
+        fansList.value = [...dataList]
+    } else {
+        fansList.value = [...fansList.value, ...dataList]
+    }
+
+    if (fansList.value.length >= totalCount.value) {
+        noMore.value = true
+        fansRef.value.removeEventListener('scroll', () => {})
+    }
+}
+// 关注/取关
+const followLoadding = ref(false)
+async function follow(id, fansStatus) {
+
+    if (!id) return
+
+    // 不能关注或取关自己
+    if (fansStatus == 3) return
+
+    // 确保每次操作请求结束后 再执行下一次操作
+    if (followLoadding.value) return
+
+    followLoadding.value = true
+
+    await request('/website/tourism/fans/saveConcern', { method: 'post', body: { attentionId: id } }).finally(() => followLoadding.value = false)
+
+    ElMessage.success('操作成功')
+    pageNum.value = 1
+    fansList.value = []
+    getData()
+}
+
+// 搜索
+const searchList = ref([])
+const timer = ref(0)
+async function search() {
+
+    clearTimeout(timer.value)
+    timer.value = setTimeout(async () => {
+        pageNum.value = 1
+        totalCount.value = 0
+        const query = {
+            showName: showName.value,
+            pageNum: pageNum.value,
+            pageSize: pageSize.value
+        }
+        loading.value = true
+        const { data } = await request('/website/tourism/fans/getUserListByNickname', { query }).finally(() => loading.value = false)
+        const { dataList, totalCount: total } = data || []
+
+        if (!Array.isArray(dataList)) {
+            searchList.value = []
+            return
+        }
+
+        totalCount.value = total
+
+        if (pageNum.value == 1) {
+            searchList.value = [...dataList]
+        } else {
+            searchList.value = [...searchList.value, ...dataList]
+        }
+
+        if (searchList.value.length >= totalCount.value) {
+            noMore.value = true
+            fansRef.value.removeEventListener('scroll', () => { })
+        }
+    }, 500)
+}
+function transNum(num) {
+    if (isNaN(num) || !num) return 0
+    if (num < 10000) return num
+    return (num / 10000).toFixed(1) + '万'
+}
+function getData() {
+    if (props.type == constant.ADD_FOLLOW) {
+        search()
+    } else {
+        getFansList()
+    }
+}
+function fansClick(fans) {
+    
+    const groupId = Date.now() + '' + Math.floor(Math.random() * 100000)
+    const getUserId = fans?.createUserIdDictMap?.id
+    const sendUserId = user.value.userId
+    const noticeType = 1
+    const data = {
+        groupId,
+        getUserId,
+        sendUserId,
+        noticeType,
+    }
+    chatStore.createConversation(data)
+    emit('chat')
+}
+function followClick(fans) {
+    const groupId = Date.now() + '' + Math.floor(Math.random() * 100000)
+    const getUserId = fans?.attentionIdDictMap?.userId || fans?.attentionIdDictMap?.id
+    const sendUserId = user.value.userId
+    const noticeType = 1
+    const data = {
+        groupId,
+        getUserId,
+        sendUserId,
+        noticeType,
+    }
+    chatStore.createConversation(data)
+    emit('chat')
+    
+}
+// 搜索结果点击
+function searchClick(fans){
+    const groupId = Date.now() + '' + Math.floor(Math.random() * 100000)
+    const getUserId = fans?.tourUserVo?.userId
+    const sendUserId = user.value.userId
+    const noticeType = 1
+    const data = {
+        groupId,
+        getUserId,
+        sendUserId,
+        noticeType,
+    }
+    chatStore.createConversation(data)
+    emit('chat')
+    
+}
+onMounted(() => {
+    if (props.type !== constant.ADD_FOLLOW) {
+        getFansList()
+    }
+
+    nextTick(() => {
+        fansRef.value.addEventListener('scroll', () => {
+
+            if (noMore.value) return
+
+            const { scrollTop, scrollHeight, clientHeight } = fansRef.value
+
+            if (scrollTop + clientHeight >= scrollHeight) {
+                pageNum.value++
+                getData()
+            }
+        })
+    })
+})
+</script>

+ 105 - 0
src/pages/chat/components/FriendInfo.vue

@@ -0,0 +1,105 @@
+<template>
+    <div class="">
+        <div class="flex items-center border-b-[1px] border-[#eee] p-10">
+            <div class="text-[12px] flex flex-col items-center">
+                <div class="w-[44px] h-[44px] rounded-full overflow-hidden">
+                    <img class="w-full h-full object-cover" :src="curConversiton.headImage" alt="" />
+                </div>
+                <div class="w-[60px] truncate text-center">
+                    {{ curConversiton.groupRemark || '' }}
+                </div>
+            </div>
+            <div v-if="followStatus == 2" @click="showCreateGroupModal = true" class="text-[12px] flex flex-col items-center cursor-pointer">
+                <div class="w-[44px] h-[44px] rounded-full overflow-hidden">
+                    <img class="w-full h-full object-cover"  src="~assets/img/chat/add.png" alt="" />
+                </div>
+                <div class="w-[60px] truncate text-center">
+                    多人群聊
+                </div>
+            </div>
+        </div>
+        <div class="p-10 text-[#333] text-[14px] cursor-pointer">
+            <div class="flex items-center justify-between mt-24">
+                <div>设置备注名</div>
+                <div class="w-[40%]">
+                    <input v-model="remark" @blur="changeRemark" maxlength="18" type="text" class="w-full text-right outline-none text-[#999] text-[12px]" placeholder="输入备注" />
+                </div>
+            </div>
+            <div @click="showChatHistoryModal = true" class="flex items-center justify-between mt-24">
+                <div>查看聊天记录</div>
+                <div class="text-[#999]">
+                    <img class="w-[20px] h-[20px]" src="~assets/img/chat/downArrowGray.png" alt="" style="transform:rotate(-90deg)" />
+                </div>
+            </div>
+            <div class="flex items-center justify-between mt-24">
+                <div>消息免打扰</div>
+                <div class="text-[#999]">
+                    <el-switch @change="updateSingleTourMember({isNotDisturb:isNotDisturb?1:0})" v-model="isNotDisturb" />
+                </div>
+            </div>
+            <div class="flex items-center justify-between mt-24">
+                <div>置顶聊天</div>
+                <div class="text-[#999]">
+                    <el-switch @change="updateSingleTourMember({isTop:isTop?1:0})" v-model="isTop" />
+                </div>
+            </div>
+            <div class="flex items-center justify-center mt-50 text-[#FF476A] text-[16px]">
+                <el-popconfirm @confirm="clearMsg" title="删除后不可恢复,确定删除?" confirm-button-text="确认" cancel-button-text="取消">
+                    <template #reference>
+                        <div>清空聊天记录</div>
+                    </template>
+                </el-popconfirm>
+            </div>
+        </div>
+        <!-- 查看聊天记录弹窗 -->
+        <ChatHistory v-if="showChatHistoryModal" :groupId="curConversiton.groupId" :show="showChatHistoryModal" width="560px" title="查看聊天记录" @close="showChatHistoryModal = false" />
+        <CreateGroup v-if="showCreateGroupModal" :show="showCreateGroupModal" width="560px" title="邀请好友群聊" @close="showCreateGroupModal = false"></CreateGroup>
+    </div>
+</template>
+<script setup>
+import ChatHistory from './ChatHistory.vue';
+import CreateGroup from './CreateGroup.vue';
+const props=defineProps({
+    followStatus: String | Number,
+})
+const chatStore = useChatStore()
+const { messages, reqChatList, ws, curConversiton, receive, receiveGetter, isConnect } = storeToRefs(chatStore)
+const user = computed(() => chatStore.user)
+const remark = ref('')
+const showChatHistoryModal = ref(false)
+const showCreateGroupModal = ref(false)
+const isNotDisturb = ref(curConversiton.value.isNotDisturb==1?true:false)
+const isTop = ref(curConversiton.value.isTop==1?true:false)
+const emit = defineEmits(['clearMsg'])
+// 修改备注
+async function changeRemark(){
+    await request('/website/tourMember/updateTourMember',{
+        method:'post',
+        body:{
+            remark:remark.value,
+            groupId:curConversiton.value.groupId
+        }
+    })
+}
+// 会话是否免打扰,是否置顶
+async function updateSingleTourMember(data){
+    data.groupId = curConversiton.value.groupId
+    await request('/website/tourMember/updateSingleTourMember',{
+        method:'post',
+        body:data
+    })
+    chatStore.reqChatList()
+}
+// 清除聊天记录
+async function clearMsg(){
+    const query = {
+        groupId:curConversiton.value.groupId
+    }
+    await request('/website/tourMessage/clearGroupMessage',{query})
+    ElMessage.success('清除成功')
+    emit('clearMsg')
+}
+onMounted(() => {
+    remark.value = curConversiton.value?.groupRemark
+})
+</script>

+ 77 - 0
src/pages/chat/components/GroupInfo.vue

@@ -0,0 +1,77 @@
+<template>
+    <div class="pb-20">
+        <div class="sticky top-0 text-[#333] bg-[#fff]">
+            <template v-if="groupNotice">
+                <div class="h-[48px] pl-12 text-[16px] border-b-[1px] border-[#eee] flex items-center">
+                    群公告
+                </div>
+                <div class="px-12 text-[14px] mt-12 text-justify tips">
+                    {{ groupNotice }}
+                </div>
+            </template>
+
+            <div class="h-[48px] pl-12 text-[16px] border-b-[1px] border-[#eee] flex items-center">
+                群成员
+            </div>
+        </div>
+        <div class="px-12 w-full">
+            <div v-for="item in list" :key="item.id" class="flex items-center justify-between w-full mt-15">
+
+                <div class="w-[24px] h-[24px] rounded-full overflow-hidden shrink-0">
+                    <img v-if="item?.headImageUrl" class="w-full h-full object-cover" :src="item?.headImageUrl" alt="">
+                </div>
+                <div class="text-[12px] truncate flex-1 ml-5">
+                    {{ item?.groupNickname }}
+                </div>
+
+                <div v-if="item?.groupRole == 1"
+                    class="text-[10px] px-4 text-[#FF476A] bg-[#FF476A]/20 rounded shrink-0 flex items-center justify-center">
+                    群主
+                </div>
+            </div>
+        </div>
+    </div>
+</template>
+<script setup>
+const chatStore = useChatStore()
+const { curConversiton } = storeToRefs(chatStore)
+const user = computed(() => chatStore.user)
+
+const list = ref([])
+const groupNotice = ref('')
+const groupId = computed(() => curConversiton.value?.groupId)
+// 获取群公告与成员
+async function getGroupInfo() {
+
+    if(!groupId.value) return
+
+    const query = {
+        groupId: groupId.value
+    }
+    const { data } = await request('/website/tourGroup/getGroupInfoAndMemberByGroupId', { query })
+    const { memberList } = data || {}
+
+    const { groupNotice: notice } = data
+    groupNotice.value = notice?.messageContent || ''
+
+    if (Array.isArray(memberList)) {
+        list.value = memberList
+    }
+}
+watch(groupId,()=>{
+    getGroupInfo()
+})
+onMounted(() => {
+    getGroupInfo()
+})
+</script>
+<style scoped>
+.tips {
+    overflow: hidden;
+    text-overflow: ellipsis;
+    display: -webkit-box;
+    -webkit-line-clamp: 2;
+    overflow: hidden;
+    -webkit-box-orient: vertical;
+}
+</style>

+ 52 - 0
src/pages/chat/components/GroupIntro.vue

@@ -0,0 +1,52 @@
+<template>
+    <XDialog :title="title || ''" :width="width" :show="show" @close="handleClose">
+        <div class="p-20 bg-[#fff]">
+            <textarea v-if="editable" v-model="intro" class="w-full h-[300px] p-20 outline-none resize-none bg-[#fafafa]" placeholder="请输入群介绍" :maxlength="200"></textarea>
+            <div v-else class="text-[12px] text-[#333]" style="min-height:200px;">
+                {{ groupInfo?.description || '暂无群介绍' }}
+            </div>
+            <div v-if="editable" class="flex justify-end items-center text-[14px] mt-20 cursor-pointer">
+                <div @click="handleClose" class="w-[80px] h-[32px] mr-20 flex items-center justify-center border rounded text-[#999]">
+                    取消
+                </div>
+                <div @click="confirm" class="w-[80px] h-[32px] bg-[#FD9A00] flex items-center justify-center rounded text-[#fff]">
+                    保存
+                </div>
+            </div>
+        </div>
+    </XDialog>
+</template>
+<script setup>
+import XDialog from '@/components/XDialog';
+
+const props = defineProps({
+    title: String,
+    width: String,
+    show: Boolean,
+    groupId: String | Number,
+    editable: Boolean,
+    groupInfo: Object,
+})
+
+const emit = defineEmits(['close','confirm'])
+function handleClose() {
+    emit('close')
+}
+const intro = ref('')
+async function confirm(){
+    if(!props.groupId) return
+    if(!props.groupInfo?.description) return
+    if(intro.value.length>200){
+        ElMessage.error('群介绍不能超过200个字!')
+        return
+    }
+    const data = {
+        groupId: props.groupId,
+        description:intro.value
+    }
+    await request('/website/tourGroup/updateGroup',{method:'post',body:data})
+    ElMessage.success('保存成功!')
+    emit('confirm')
+    handleClose()
+}
+</script>

+ 72 - 0
src/pages/chat/components/GroupQrCode.vue

@@ -0,0 +1,72 @@
+<template>
+    <XDialog :title="title || ''" :width="width" :show="show" @close="handleClose">
+        <div class="w-full bg-[#fff]">
+            <div class="flex justify-center items-center mt-40">
+                <div>
+                    <MultiHeader :imgUrls="createGroupAvatar() || []" :size="60"></MultiHeader>
+                </div>
+                
+            </div>
+            <div class="flex justify-center items-center text-[#000] text-[14px] mt-12">
+                群聊:{{ groupInfo?.groupName }}
+            </div>
+            <div class="flex justify-center items-center mt-20">
+                <div class="flex justify-center items-center w-[350px] aspect-[1/1] border">
+                    <img :src="qrcodeUrl" style="width:100%;" alt="">
+                </div>
+            </div>
+
+            <!-- <div class="flex justify-center items-center text-[#666] text-[12px] mt-12">
+                该二维码7天内(01月06日前)有效
+            </div> -->
+            <div class="flex justify-center items-center text-[14px] my-20 cursor-pointer ">
+                <div @click="saveQrCode" class="w-[170px] h-[32px] flex items-center justify-center border rounded text-[#333]">
+                    保存到电脑
+                </div>
+            </div>
+        </div>
+
+    </XDialog>
+</template>
+<script setup>
+
+import XDialog from '@/components/XDialog';
+const apiUrl = `${import.meta.env.VITE_APP_BASE_URL}/website/tourGroup/getGroupQR`
+const props = defineProps({
+    title: String,
+    width: String,
+    show: Boolean,
+    groupId: String | Number,
+    groupInfo: Object,
+})
+const qrcodeUrl = computed(()=>apiUrl+'?groupId='+props.groupId+'&systemOs=0')
+
+
+const emit = defineEmits(['close'])
+function handleClose() {
+    emit('close')
+}
+// 创建群头像
+function createGroupAvatar() {
+    const memberList = props.groupInfo.memberList
+    let avatarArr = []
+    if (Array.isArray(memberList)) {
+        for (let i = 0; i < memberList.length; i++) {
+            if(i<9){
+                avatarArr.push(memberList[i]?.headImageUrl)
+            }
+            if(i>=8){
+                break
+            }
+        }
+    }
+    return avatarArr
+}
+function saveQrCode(){
+    
+    window.open(qrcodeUrl.value,'_blank')
+}
+onMounted(()=>{
+    console.log('props.info:',props.groupInfo)
+})
+</script>

+ 365 - 0
src/pages/chat/components/GroupSetting.vue

@@ -0,0 +1,365 @@
+<template>
+    <div>
+        <div class="flex flex-wrap items-center justify-between text-[#333] px-12">
+            <template v-for="(item, index) in list" :key="index">
+                <div v-if="index < 5" class="w-[25%] mt-15 flex flex-col justify-center items-center">
+                    <div class="w-[60%] aspect-[1/1] rounded-full overflow-hidden bg-[#f5f5f5]">
+                        <img v-if="item?.headImageUrl" class="w-full h-full object-cover" :src="item?.headImageUrl" alt="" />
+                    </div>
+                    <div class="w-full truncate text-[12px] text-center">
+                        {{ item?.groupNickname }}
+                    </div>
+                </div>
+            </template>
+
+            <div @click="showInviteModal = true" class="w-[25%] mt-15 flex flex-col justify-center items-center cursor-pointer">
+                <div class="w-[60%] aspect-[1/1] rounded-full overflow-hidden">
+                    <img class="w-full h-full object-cover" src="~assets/img/chat/add.png" alt="" />
+                </div>
+                <div class="truncate text-[12px]">
+                    添加
+                </div>
+            </div>
+            <div v-if="isLord" @click="showRemoveMemberModal = true" class="w-[25%] mt-15 flex flex-col justify-center items-center cursor-pointer">
+                <div class="w-[60%] aspect-[1/1] rounded-full overflow-hidden">
+                    <img class="w-full h-full object-cover" src="~assets/img/chat/reduce.png" alt="" />
+                </div>
+                <div class="truncate text-[12px]">
+                    删除
+                </div>
+            </div>
+        </div>
+        <div v-if="list.length>6" class="h-[50px] cursor-pointer border-b-[1px] border-[#eee] flex items-center justify-center text-[14px] text-[#666]">
+            查看全部
+            <img class="w-[15px] h-[15px]" src="~assets/img/chat/downArrowGray.png" alt=""
+                style="transform:rotate(-90deg)" />
+        </div>
+        <div class="px-12 cursor-pointer text-[14px] text-[#333] border-b-[1px] border-[#eee] pb-24">
+            <div v-if="isLord" class="flex items-center justify-between mt-24">
+                <div>群聊名称</div>
+                <div class="w-[40%]">
+                    <input v-model="groupInfo.groupName" @blur="updateGroup({groupName:groupInfo?.groupName})"  maxlength="18" type="text" class="w-full text-right outline-none text-[#999] text-[12px]" placeholder="设置群聊名称" />
+                </div>
+            </div>
+            <div @click="showSetGroupQrCodeModal = true" class="flex items-center justify-between mt-24">
+                <div>群二维码</div>
+                <div class="flex items-center">
+                    <img class="w-[15px] h-[15px]" src="~assets/img/chat/qrCode.png" alt="" />
+                    <img class="w-[15px] h-[15px]" src="~assets/img/chat/downArrowGray.png" alt=""
+                        style="transform:rotate(-90deg)" />
+                </div>
+            </div>
+            <div @click="showSetGroupTipsModal = true" class="mt-24" >
+                <div>群公告</div>
+                <div class="flex items-center justify-between">
+                    <div class="text-[#999] text-[12px] tips3">
+                        {{ groupInfo?.groupNotice?.messageContent || '暂无公告' }}
+                    </div>
+                    <img class="w-[15px] h-[15px] shrink-0" src="~assets/img/chat/downArrowGray.png" alt=""
+                        style="transform:rotate(-90deg)" />
+                </div>
+            </div>
+            <div @click="showSetGroupIntroModal = true" class="mt-24">
+                <div>群介绍</div>
+                <div class="flex items-center justify-between">
+                    <div class="text-[#999] text-[12px] tips3">
+                        {{ groupInfo?.description || '暂无介绍' }}
+                    </div>
+                    <img class="w-[15px] h-[15px] shrink-0" src="~assets/img/chat/downArrowGray.png" alt=""
+                        style="transform:rotate(-90deg)" />
+                </div>
+            </div>
+            <div @click="showSetGroupTypeModal = true" v-if="isLord" class="flex items-center justify-between mt-24">
+                <div>群聊类型</div>
+                <div class="flex items-center text-[12px] text-[#999]">
+                    {{ groupInfo?.belongTypeIdDictMap?.name }}
+                    <img class="w-[15px] h-[15px]" src="~assets/img/chat/downArrowGray.png" alt=""
+                        style="transform:rotate(-90deg)" />
+                </div>
+            </div>
+            <div class="flex items-center justify-between mt-24">
+                <div>群备注</div>
+                <div class="w-[40%]">
+                    <input v-model="remark" @blur="updateTourMember({remark})" maxlength="18" type="text" class="w-full text-right outline-none text-[#999] text-[12px]"
+                        placeholder="设置群聊备注" />
+                </div>
+            </div>
+        </div>
+        <div v-if="isLord" class=" px-12 cursor-pointer text-[14px] text-[#333] border-b-[1px] border-[#eee] pb-24">
+            <div  class="mt-24">
+                <div class="flex items-center justify-between">
+                    <div>个人主页展示</div>
+                    <div class="text-[#999]">
+                        <el-switch v-model="isPublic" @change="updateGroup({isPublic:isPublic?1:0})" />
+                    </div>
+                </div>
+                <div class="text-[10px] text-[#666] ">
+                    开启后,在群聊广场和个人主页展示
+                </div>
+            </div>
+            <div class="mt-24">
+                <div class="flex items-center justify-between">
+                    <div>群聊邀请确认</div>
+                    <div class="text-[#999]">
+                        <el-switch v-model="isNeedConfirm" @change="updateGroup({isNeedConfirm:isNeedConfirm?1:0})" />
+                    </div>
+                </div>
+                <div class="text-[10px] text-[#666] ">
+                    启用后,群成员需群主确认才能邀请朋友进群。扫 描二维码进群将同时停用。
+                </div>
+            </div>
+            <div @click="showApplyListModal = true" class="flex items-center justify-between mt-24">
+                <div>收到的进群申请</div>
+                <div class="">
+                    <img class="w-[15px] h-[15px]" src="~assets/img/chat/downArrowGray.png" alt="" style="transform:rotate(-90deg)" />
+                </div>
+            </div>
+        </div>
+        <div class=" px-12 cursor-pointer text-[14px] text-[#333] border-b-[1px] border-[#eee] pb-24">
+            <div class="flex items-center justify-between mt-24">
+                <div>我在群里的昵称</div>
+                <div class="w-[40%]">
+                    <input v-model="groupNickname" @blur="updateTourMember({groupNickname})" maxlength="18" type="text" class="w-full text-right outline-none text-[#999] text-[12px]" placeholder="输入昵称" />
+                </div>
+            </div>
+            <div @click="showChatHistoryModal = true" class="flex items-center justify-between mt-24">
+                <div>查看聊天记录</div>
+                <div class="">
+                    <img class="w-[15px] h-[15px]" src="~assets/img/chat/downArrowGray.png" alt=""
+                        style="transform:rotate(-90deg)" />
+                </div>
+            </div>
+            <div class="flex items-center justify-between mt-24">
+                <div>消息免打扰</div>
+                <div class="text-[#999]">
+                    <el-switch v-model="isNotDisturb" @change="updateSingleTourMember({isNotDisturb:isNotDisturb?1:0})" />
+                </div>
+            </div>
+            <div class="flex items-center justify-between mt-24">
+                <div>置顶聊天</div>
+                <div class="text-[#999]">
+                    <el-switch v-model="isTop" @change="updateSingleTourMember({isTop:isTop?1:0})" />
+                </div>
+            </div>
+            <div @click="showComplainModal = true" class="flex items-center justify-between mt-24">
+                <div>举报</div>
+                <div class="">
+                    <img class="w-[15px] h-[15px]" src="~assets/img/chat/downArrowGray.png" alt=""
+                        style="transform:rotate(-90deg)" />
+                </div>
+            </div>
+        </div>
+        <div class="flex cursor-pointer items-center justify-center mt-24 text-[#FF476A] text-[16px]">
+            <el-popconfirm @confirm="clearMsg" title="删除后不可恢复,确定删除?" confirm-button-text="确认" cancel-button-text="取消">
+                <template #reference>
+                    <div>清空聊天记录</div>
+                </template>
+            </el-popconfirm>
+        </div>
+        <div v-if="isLord" class="flex cursor-pointer items-center justify-center my-24 text-[#FF476A] text-[16px]">
+            <el-popconfirm @confirm="exitGroup" title="解散群聊后不可恢复,确定解散?" confirm-button-text="确认" cancel-button-text="取消">
+                <template #reference>
+                    <div>解散群聊</div>
+                </template>
+            </el-popconfirm>
+        </div>
+    </div>
+    <!-- 群类型设置 -->
+    <SetGroupType v-if="showSetGroupTypeModal" :groupId="curConversiton.groupId" :belongTypeId="groupInfo?.belongTypeId" :show="showSetGroupTypeModal" width="560px"
+    title="设置群类型" @close="showSetGroupTypeModal = false,getGroupInfo()"></SetGroupType>
+    
+    <!-- 群公告查看与设置 -->
+    <GroupTips v-if="showSetGroupTipsModal" :groupId="curConversiton.groupId" :groupInfo="groupInfo" :editable="isLord" :show="showSetGroupTipsModal" width="560px" title="群公告" @close="showSetGroupTipsModal = false,getGroupInfo()"></GroupTips>
+    <!-- 群介绍查看与设置 -->
+    <GroupIntro v-if="showSetGroupIntroModal" :groupId="curConversiton.groupId" :groupInfo="groupInfo" :editable="isLord" :show="showSetGroupIntroModal" width="560px" title="群介绍" @close="showSetGroupIntroModal = false" @confirm="getGroupInfo"></GroupIntro>
+    <!-- 群二维码 -->
+    <GroupQrCode v-if="showSetGroupQrCodeModal" :groupId="curConversiton.groupId" :groupInfo="groupInfo" :show="showSetGroupQrCodeModal" width="560px" title="群二维码分享" @close="showSetGroupQrCodeModal = false"></GroupQrCode>
+    <!-- 查看聊天记录弹窗 -->
+    <ChatHistory v-if="showChatHistoryModal" :groupId="curConversiton.groupId" :show="showChatHistoryModal" width="560px"
+                title="查看聊天记录" @close="showChatHistoryModal = false" />
+    <!-- 举报投诉弹窗 -->
+    <Complain v-if="showComplainModal" :groupId="curConversiton.groupId" :show="showComplainModal" width="560px" title="举报投诉" @close="showComplainModal = false"></Complain>
+    <!-- 收到的进群申请 -->
+    <ApplyJoinList v-if="showApplyListModal" :groupId="curConversiton.groupId" :show="showApplyListModal" width="560px" title="收到的进群申请" @close="showApplyListModal = false"></ApplyJoinList>
+    <!-- 踢人出群 -->
+    <RemoveMember v-if="showRemoveMemberModal" @removeMember="getGroupInfo" :groupId="curConversiton.groupId" :show="showRemoveMemberModal" width="560px" title="移除群聊" @close="showRemoveMemberModal = false"></RemoveMember>
+    
+    <!-- 拉人进群 -->
+    <InviteGroup v-if="showInviteModal" @invite="getGroupInfo" :groupId="curConversiton.groupId" :show="showInviteModal" width="560px" title="邀请加入群聊" @close="showInviteModal = false"></InviteGroup>
+</template>
+<script setup>
+import SetGroupType from './SetGroupType.vue'
+import GroupTips from './GroupTips.vue'
+import GroupQrCode from './GroupQrCode.vue'
+import ChatHistory from './ChatHistory.vue'
+import Complain from './Complain.vue'
+import ApplyJoinList from './ApplyJoinList.vue'
+import RemoveMember from './RemoveMember.vue'
+import InviteGroup from './InviteGroup.vue'
+import GroupIntro from './GroupIntro.vue'
+const chatStore = useChatStore()
+const { messages, reqChatList, ws, curConversiton, receive, receiveGetter, isConnect } = storeToRefs(chatStore)
+const user = computed(() => chatStore.user)
+const show = ref(false)
+const bather = ref(false)
+const showSetGroupTypeModal = ref(false)
+const showSetGroupTipsModal = ref(false)
+const showSetGroupQrCodeModal = ref(false)
+const showChatHistoryModal = ref(false)
+const showComplainModal = ref(false)
+const showApplyListModal = ref(false)
+const showRemoveMemberModal = ref(false)
+const showInviteModal = ref(false)
+const showSetGroupIntroModal = ref(false)
+const isNotDisturb = ref(curConversiton.value.isNotDisturb==1?true:false)
+const isTop = ref(curConversiton.value.isTop==1?true:false)
+
+// 对群的备注
+const remark = ref('')
+
+// 我在群里面的昵称
+const groupNickname = ref('')
+
+const list = ref([])
+const groupNotice = ref('')
+const groupInfo = ref({})
+
+// 是否是群主
+const isLord = ref(false)
+// 是否开启进群验证
+const isNeedConfirm = ref(false)
+const isPublic = ref(false)
+// 获取群信息
+async function getGroupInfo() {
+
+    if (!curConversiton.value.groupId) return
+
+    const query = {
+        groupId: curConversiton.value.groupId
+    }
+    const { data } = await request('/website/tourGroup/getGroupInfoAndMemberByGroupId', { query })
+    const { memberList } = data || {}
+
+    groupInfo.value = data
+    const { groupNotice: notice,isNeedConfirm:is_needConfirm,isPublic:is_public } = data
+    groupNotice.value = notice
+
+    isNeedConfirm.value = is_needConfirm==1?true:false
+    isPublic.value = is_public==1?true:false
+
+    if (Array.isArray(memberList)) {
+        list.value = memberList
+        for(let i = 0 ;i< memberList.length;i++){
+            if(memberList[i].userId == user?.value.userId){
+                remark.value = memberList[i].groupRemark
+                groupNickname.value = memberList[i].groupNickname 
+                
+            }
+            if(memberList[i].groupRole == 1 && memberList[i].userId == user?.value.userId){
+                isLord.value = true
+            }
+        }
+    }
+}
+
+// 修改我在群里的昵称、对群名的备注
+async function updateTourMember(obj={}){
+
+    if(!obj.remark && !obj.groupNickname) return
+
+    if(!curConversiton.value.groupId) return
+
+    const data={
+        groupId:curConversiton.value.groupId,
+        ...obj
+    }
+
+    await request('/website/tourMember/updateTourMember',{method:'post',body:data})
+    getGroupInfo()
+    chatStore.reqChatList()
+}
+
+/**
+ * 编辑群 只有群主才可以调这个函数
+ * @param {Object} obj
+ * belongTypeId 群类型 
+ * groupName 群名称 
+ * groupAvatar 群头像 
+ * isNeedConfirm 是否开启群验证0否1是
+ * isPublic 是否公开群0否1是
+ * */  
+async function updateGroup(obj={}){
+
+    if(!curConversiton.value.groupId) return
+
+    const {belongTypeId,groupName,groupAvatar,isNeedConfirm,isPublic} = obj
+
+    if(!belongTypeId && !groupName && !groupAvatar && (isNeedConfirm!=0 && isNeedConfirm!=1) && (isPublic!=0&&isPublic!=1)) return
+    
+    const data={
+        groupId:curConversiton.value.groupId,
+        ...obj
+    }
+    await request('/website/tourGroup/updateGroup',{method:'post',body:data})
+    getGroupInfo()
+    chatStore.reqChatList()
+}
+// 会话是否免打扰,是否置顶
+async function updateSingleTourMember(data){
+    data.groupId = curConversiton.value.groupId
+    await request('/website/tourMember/updateSingleTourMember',{
+        method:'post',
+        body:data
+    })
+    chatStore.reqChatList()
+    getGroupInfo()
+}
+
+// 获取申请加群的列表
+async function getApplicationsList(){
+    const query = {groupId:curConversiton.value.groupId}
+    const res = await request('/website/tourGroup/getApplicationsList',{query})
+    console.log('群申请:',res)
+}
+// 清除聊天记录
+async function clearMsg(){
+    const query = {
+        groupId:curConversiton.value.groupId
+    }
+    await request('/website/tourMessage/clearGroupMessage',{query})
+    ElMessage.success('清除成功')
+    emit('clearMsg')
+}
+// 解散/退出群聊
+async function exitGroup(){
+    const query = {
+        groupId:curConversiton.value.groupId
+    }
+    ElLoading.service({
+        lock: true,
+        text: '正在退出...',
+        background: 'rgba(0, 0, 0, 0.2)'
+    })
+
+    await request('/website/tourGroup/exitGroup',{query}).finally(() => ElLoading.service().close())
+
+    ElMessage.success('操作成功')
+    curConversiton.value = {}
+    chatStore.reqChatList()
+
+}
+onMounted(() => {
+    getGroupInfo()
+    getApplicationsList()
+})
+</script>
+<style scoped lang="scss">
+.tips3 {
+    overflow: hidden;
+    text-overflow: ellipsis;
+    display: -webkit-box;
+    -webkit-line-clamp: 3;
+    overflow: hidden;
+    -webkit-box-orient: vertical;
+}
+</style>

+ 210 - 0
src/pages/chat/components/GroupSquare.vue

@@ -0,0 +1,210 @@
+<template>
+    <XDialog :title="title || ''" :width="width" :show="show" @close="handleClose">
+        <div ref="containerRef" class="w-full relative h-[600px] relative overflow-y-auto">
+            <div class="w-full sticky top-0">
+                <div class="px-20  bg-[#fff]">
+                    <div class="h-[15px]"></div>
+                    <div class="flex items-center border rounded-full h-[35px]">
+                        <img src="~assets/img/search.png" class="w-[16px] h-[16px] mx-10" alt="">
+                        <input v-model="groupName" @input="search" class="outline-none" type="text" placeholder="搜索">
+                    </div>
+                </div>
+                <div ref="typeBoxRef" class="overflow-x-auto pt-20 pb-10 mx-20 scrollbar">
+                    <div v-if="typeList.length" class="flex items-center text-[14px] cursor-pointer">
+                        <div @click="groupTypeClick({}, 'typeAll')" id="typeAll"
+                            class="w-[88px] h-[32px] flex rounded items-center justify-center shrink-0 mr-20 border"
+                            :class="[selectedType.id ? '' : 'bg-[#FD9A00] text-[#fff]']">
+                            推荐
+                        </div>
+                        <div @click="groupTypeClick(item, 'type' + index)" :id="'type' + index"
+                            v-for="(item, index) in typeList" :key="index"
+                            class="w-[88px] h-[32px] flex items-center rounded justify-center shrink-0 mr-20 border"
+                            :class="[selectedType.id == item.id ? 'bg-[#FD9A00] text-[#fff]' : '']">
+                            {{ item.typeName }}
+                        </div>
+                    </div>
+                </div>
+            </div>
+            <div class="px-20 ">
+                <div v-for="(item, index) in groupList" :key="index" class="flex items-center justify-between mt-20">
+                    <div class="flex items-center">
+                        <div class="w-[44px] h-[44px] rounded-full overflow-hidden bg-[#dedede]">
+
+                        </div>
+                        <div class="ml-10">
+                            <div class="flex items-center text-[14px] text-[#333]">
+                                <div class="truncate" style="max-width:280px;">
+                                    {{ item?.groupName }}
+                                </div>
+                                <div v-if="item?.belongTypeIdDictMap?.name" class="text-[12px] text-[#FF9300] bg-[#FEF4E6] rounded px-5 ml-5">
+                                    {{ item?.belongTypeIdDictMap?.name }}
+                                </div>
+                            </div>
+                            <div class="text-[#000]/60 text-[14px] truncate" style="max-width:280px;">
+                                {{ item?.description }}
+                            </div>
+                        </div>
+                    </div>
+                    
+                    <div @click="joinGroup(item.id)" v-if="!item.codeShowStatus" class="w-[72px] h-[32px] bg-[#FD9A00] flex items-center justify-center rounded cursor-pointer text-[#fff]">
+                        加入
+                    </div>
+
+                    <div @click="toChat(item.id)"  v-if="item.codeShowStatus == 1" class="w-[72px] h-[32px] bg-[#FD9A00]/10 flex items-center justify-center rounded cursor-pointer text-[#FD9A00]">
+                        去聊天
+                    </div>
+                    <div v-if="item.codeShowStatus == 2" class="w-[72px] h-[32px] bg-[#FAFAFA] flex items-center justify-center rounded text-[#333]">
+                        已封禁
+                    </div>
+                    <div v-if="item.codeShowStatus == 3" class="w-[72px] h-[32px] bg-[#FAFAFA] flex items-center justify-center rounded text-[#333]">
+                        已解散
+                    </div>
+                    <div v-if="item.codeShowStatus == 4" class="w-[72px] h-[32px] bg-[#FAFAFA] flex items-center justify-center rounded text-[#333]">
+                        申请中
+                    </div>
+                </div>
+            </div>
+            <div v-if="!groupList.length" class="text-center text-[#999] text-[14px] mt-100">
+                没有数据
+            </div>
+        </div>
+    </XDialog>
+</template>
+<script setup>
+import XDialog from '@/components/XDialog';
+const props = defineProps({
+    title: String,
+    width: String,
+    show: Boolean,
+})
+const emit = defineEmits(['close'])
+const chatStore = useChatStore()
+
+const user = computed(() => chatStore.user)
+function handleClose() {
+    emit('close')
+}
+const typeBoxRef = ref(null)
+const typeList = ref([])
+const selectedType = ref({})
+async function getTypeList() {
+
+    const { data } = await request('/website/tourGroup/getGroupTypeFirst')
+
+    if (!Array.isArray(data)) return
+
+    typeList.value = data
+
+}
+
+const groupList = ref([])
+const groupName = ref('')
+const pageNum = ref(1)
+const pageSize = ref(10)
+const timer = ref(0)
+const totalCount = ref(0)
+async function searchGroup() {
+    clearTimeout(timer.value)
+    timer.value = setTimeout(async () => {
+        const query = {
+            pageNum: pageNum.value,
+            pageSize: pageSize.value,
+        }
+
+        if (groupName.value) {
+            query.groupName = groupName.value
+        }
+
+        if (selectedType.value?.id) {
+            query.groupTypeId = selectedType.value.id
+        }
+
+        const { data } = await request('/website/tourGroup/list', { query })
+
+        const { dataList, totalCount: count } = data || {}
+
+        totalCount.value = count || 0
+
+        if (!Array.isArray(dataList)) return
+
+        if (pageNum.value == 1) {
+            groupList.value = dataList
+        } else {
+            groupList.value = [...groupList.value, ...dataList]
+        }
+
+        console.log('群列表:', res)
+    }, 500)
+
+}
+function groupTypeClick(item, domId) {
+    selectedType.value = item
+    typeBoxRef.value.scrollTo({
+        left: document.getElementById(domId).offsetLeft - 20,
+        behavior: 'smooth'
+    });
+    search()
+}
+function search() {
+    pageNum.value = 1
+    searchGroup()
+}
+// 申请加群
+async function joinGroup(groupId){
+    const data = {
+        groupId,
+        ids: [user.value.userId]
+    }
+    ElLoading.service()
+    await request('/website/tourMember/invite',{method:'post',body:data}).finally(()=>ElLoading.service().close())
+    ElMessage.success('申请成功')
+    search()
+}
+async function toChat(groupId){
+    
+    const sendUserId = user.value.userId
+    const noticeType = 2
+    const data = {
+        groupId,
+        getUserId:'',
+        sendUserId,
+        noticeType,
+    }
+    chatStore.createConversation(data)
+    handleClose()
+}
+// 监听消息滚动触底事件
+function addEventListenerContainerRef() {
+    nextTick(() => {
+        if (containerRef.value) {
+            containerRef.value.removeEventListener('scroll', () => { })
+            containerRef.value.addEventListener('scroll', (e) => {
+
+                if (containerRef.value.scrollHeight - (containerRef.value.scrollTop + containerRef.value.clientHeight) <= 2) {
+
+                    pageNum.value += 1
+                    searchGroup()
+
+                }
+            })
+        }
+    })
+}
+onMounted(() => {
+    getTypeList()
+    addEventListenerContainerRef()
+    searchGroup()
+})
+</script>
+<style lang="scss" scoped>
+.scrollbar::-webkit-scrollbar {
+    width: 5px;
+    height: 5px;
+    //   background-color: #ccc;
+}
+
+.scrollbar::-webkit-scrollbar-thumb {
+    border-radius: 10px;
+    background-color: #d2d2d2;
+}
+</style>

+ 82 - 0
src/pages/chat/components/GroupTips.vue

@@ -0,0 +1,82 @@
+<template>
+    <XDialog :title="title || ''" :width="width" :show="show" @close="handleClose">
+        <div class="p-20 bg-[#fff]">
+            <div class="flex items-center border-b border-[#eee] pb-20 mb-20">
+                <div class=" ">
+                    <MultiHeader :imgUrls="headImages"></MultiHeader>
+                </div>
+                <div class="text-[14px] ml-15">
+                    <div class="text-[#333] ">
+                        {{ groupInfo?.groupName }}
+                    </div>
+                    <div class="text-[#666]">
+                        {{ groupInfo?.groupNotice?.createTime }}
+                    </div>
+                </div>
+            </div>
+            <textarea v-if="editable" v-model="groupInfo.groupNotice.messageContent" class="w-full h-[300px] p-20 outline-none resize-none bg-[#fafafa]" placeholder="请输入群公告"></textarea>
+            <div v-else class="text-[12px] text-[#333]" style="min-height:200px;">
+                {{ groupInfo?.groupNotice?.messageContent || '暂无群公告' }}
+            </div>
+            <div v-if="editable" class="flex justify-end items-center text-[14px] mt-20 cursor-pointer">
+                <div @click="handleClose" class="w-[80px] h-[32px] mr-20 flex items-center justify-center border rounded text-[#999]">
+                    取消
+                </div>
+                <div @click="confirm" class="w-[80px] h-[32px] bg-[#FD9A00] flex items-center justify-center rounded text-[#fff]">
+                    保存
+                </div>
+            </div>
+        </div>
+    </XDialog>
+</template>
+<script setup>
+import XDialog from '@/components/XDialog';
+
+const props = defineProps({
+    title: String,
+    width: String,
+    show: Boolean,
+    groupId: String | Number,
+    editable: Boolean,
+    groupInfo: Object,
+})
+
+const emit = defineEmits(['close'])
+function handleClose() {
+    emit('close')
+}
+async function confirm(){
+    if(!props.groupId) return
+    if(!props.groupInfo?.groupNotice) return
+
+    const data = {
+        groupId: props.groupId,
+        messageContent:props.groupInfo.groupNotice.messageContent
+    }
+    await request('/website/tourMessage/updateAnnouncement',{method:'post',body:data})
+    ElMessage.success('公告发布成功!')
+    handleClose()
+}
+
+const headImages = ref([])
+// 获取头像
+function getAvatar(){
+    const arr = []
+    const {memberList} = props.groupInfo
+    console.log('memberList:',memberList);
+    if(Array.isArray(memberList)){
+        for (let i = 0; i < memberList.length; i++) {
+            if(i<9){
+                arr.push(memberList[i].headImageUrl)
+            }
+        }
+    }
+    headImages.value = arr
+    console.log('arr:',headImages.value)
+}
+onMounted(()=>{
+   nextTick(()=>{
+        getAvatar()
+   })
+})
+</script>

+ 156 - 0
src/pages/chat/components/InviteGroup.vue

@@ -0,0 +1,156 @@
+<template>
+    <XDialog :title="title || ''" :width="width" :show="show" @close="handleClose">
+        <div class="w-full bg-[#fff]">
+            <div class="flex justify-between">
+                <div ref="frendsBoxRef" class="w-[280px] h-[300px] border-r-[1px] border-[#eee] relative overflow-y-auto scrollbar">
+                    <div class="px-20 sticky top-0 bg-[#fff]">
+                        <div class="h-[15px]"></div>
+                        <div class="flex items-center border rounded-full h-[35px]">
+                            <img src="~assets/img/search.png" class="w-[16px] h-[16px] mx-10" alt="">
+                            <input v-model="keyword" @input="search" class="outline-none" type="text" placeholder="搜索群友">
+                        </div>
+                    </div>
+                    <div class="mt-5 pb-20">
+                        <template v-for="(item, index) in searchResult" :key="item.id">
+                            <div @click="item.isChecked = item.isChecked ? false:true" class="flex items-center flex-warp text-wrap hover:bg-[#eee] px-20 py-10 cursor-pointer">
+                                <img v-if="item.isChecked" class="w-[21px] h-[21px]" src="~assets/img/chat/radio_circle_orange.png" alt="">
+                                <img v-else class="w-[21px] h-[21px]" src="~assets/img/chat/radio_circle_gray.png" alt="" />
+                                <div class="w-[24px] h-[24px] rounded-full overflow-hidden mx-10">
+                                    <img :src="item?.headImageUrl" class="w-full h-full object-cover" alt="" />
+                                </div>
+                                <span>{{ item?.showName }}</span>
+                            </div>
+                        </template>
+                    </div>
+                </div>
+                <div class="w-[280px] h-[300px]">
+                    <div class="h-[40px] flex items-center justify-between px-10">
+                        <div class="text-[#000] text-[14px]">选择好友</div>
+                        <div v-if="Array.isArray(searchResult)" class="text-[#999] text-[12px]">
+                            已选择{{ searchResult.filter(item => item.isChecked).length}}个好友</div>
+                    </div>
+                    <div class="h-[180px] overflow-y-auto scrollbar px-10">
+                        <div class="flex flex-wrap w-full">
+                            <template v-for="item in searchResult" :key="item.id">
+                                <div v-if="item.isChecked" class="w-[25%] mt-10">
+                                    <div class="w-[50%] aspect-[1/1] rounded-full mx-auto relative cursor-pointer">
+                                        <img :src="item?.headImageUrl" class="w-full h-full object-cover rounded-full" alt="" />
+                                        <div @click="item.isChecked = false" class="absolute top-[-5px] right-[-5px] w-[15px] h-[15px] bg-[#999] flex items-center justify-center text-[#fff] text-[10px] rounded-full">
+                                            x
+                                        </div>
+                                    </div>
+                                    <div class="truncate w-full text-[12px] text-[#333] text-center">
+                                        {{ item?.showName }}
+                                    </div>
+                                </div>
+                            </template>
+                        </div>
+                    </div>
+                    <div class="h-[70px] cursor-pointer items-center justify-around w-full flex border-t-[1px] border-[#eee] mt-10">
+                        <div @click="handleClose"
+                            class="w-[80px] h-[32px] flex items-center justify-center border border-[#666] rounded text-[#666]">
+                            取消
+                        </div>
+                        <div @click="comfirm" class="w-[80px] h-[32px] flex items-center justify-center rounded text-[#fff] bg-[#FD9A00]">
+                            确认
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </XDialog>
+</template>
+<script setup>
+import XDialog from '@/components/XDialog';
+
+const props = defineProps({
+    title: String,
+    width: String,
+    show: Boolean,
+    groupId:String
+})
+const emit = defineEmits(['close','invite'])
+const frendsBoxRef = ref(null)
+const chatStore = useChatStore()
+const { messages, reqChatList, ws, curConversiton, receive, receiveGetter, isConnect } = storeToRefs(chatStore)
+function handleClose() {
+    emit('close')
+}
+
+
+
+const friends = ref([])
+const selectedFriends = ref([])
+const memberList = ref([])
+
+const searchResult = ref([])
+const keyword = ref('')
+async function search(){
+    if(!keyword.value) {
+        searchResult.value = [...memberList.value]
+        return
+    }
+    let result = []
+    memberList.value.map(item=>{
+        if(item?.showName.indexOf(keyword.value) != -1){
+            result.push(item)
+        }
+    })
+    searchResult.value = result
+}
+
+// 确认
+async function comfirm(){
+    const arr = searchResult.value.filter(item=>item.isChecked)
+    if(!arr.length){
+        ElMessage.warning('请选择要邀请的好友')
+        return
+    }
+    const ids = arr.map(item=>item.userId)
+    const data = {
+        groupId:props.groupId,
+        ids
+    }
+    ElLoading.service({
+        lock:true,
+        text:'正在邀请中...',
+        background:'rgba(0,0,0,0.2)'
+    })
+    await request('/website/tourMember/invite',{method:'post',body:data}).finally(()=>ElLoading.service().close())
+    
+    emit('invite')
+    ElMessage.success('邀请成功')
+    handleClose()
+}
+
+// 获取互关好友列表,没有在群里的
+async function getFriends(){
+    const query = {
+        groupId:props.groupId
+    }
+    const {data} =await request('/website/tourMember/memberLit',{query})
+
+    if(!Array.isArray(data)) return 
+    searchResult.value = data
+    memberList.value = data
+    console.log('互关好友:',res)
+
+}
+
+onMounted(() => {
+    getFriends()
+})
+</script>
+<style scoped lang="scss">
+.scrollbar::-webkit-scrollbar {
+    width: 1px;
+    height: 1px;
+    background-color: #eee;
+}
+
+.scrollbar::-webkit-scrollbar-thumb {
+    border-radius: 10px;
+    //   -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
+    background-color: #FD9A00;
+}
+</style>

+ 16 - 0
src/pages/chat/components/MsgStatus.vue

@@ -0,0 +1,16 @@
+<template>
+    
+    <img v-if="status == 'loading'" src="~assets/img/chat/loading.gif" class="w-[20px]" alt="" />
+    <!-- <div v-else class="text-[#999] text-[10px] w-[50px]">发送失败</div> -->
+    <div v-else></div>
+</template>
+<script setup>
+const chatStore = useChatStore()
+
+const status = ref('loading')
+onMounted(()=>{
+    setTimeout(()=>{
+        status.value = 'fail'
+    },10000)
+})
+</script>

+ 119 - 0
src/pages/chat/components/NewFans.vue

@@ -0,0 +1,119 @@
+<template>
+    <div ref="containerRef" class="w-full h-full overflow-y-auto relative">
+        <div
+            class="w-full sticky top-0 bg-[#fff] h-[50px] border-b-[1px] border-[#dedede] flex items-center justify-between shrink-0">
+            <div class="font-bold text-[18px] mx-[15px]">
+                新增粉丝
+            </div>
+        </div>
+
+        <div v-for="(item, index) in list" :key="index" class="px-12 pb-30">
+            <div class="flex items-center justify-between mt-24 cursor-pointer">
+                <div class="flex items-center">
+                    <div class="w-[44px] h-[44px] rounded-full overflow-hidden shrink-0">
+                        <img :src="item?.messageContent?.tourUserVo?.headImageUrl" class="w-full h-full object-cover"
+                            alt="" />
+                    </div>
+                    <div class="flex flex-col justify-between ml-15">
+                        <div class="text-[14px] text-[#333]">
+                            {{ item?.messageContent?.tourUserVo?.showName }}
+                        </div>
+                        <div class="text-[12px] text-[#666]">
+                            关注了你 {{ item?.createTime }}
+                        </div>
+                    </div>
+                </div>
+
+                <!-- 未关注 -->
+                <div v-if="item.focusStatus == 0"
+                    class="text-[#fff] bg-[#FD9A00] w-[72px] h-[32px] rounded text-[14px] flex items-center justify-center">
+                    关注
+                </div>
+                <!-- 已关注 -->
+                <div v-if="item.focusStatus == 1"
+                    class="text-[#fff] bg-[#FD9A00] w-[72px] h-[32px] rounded text-[14px] flex items-center justify-center">
+                    取消关注
+                </div>
+                <!-- 相互关注 -->
+                <div v-if="item.focusStatus == 2"
+                    class="text-[#fff] bg-[#FD9A00] w-[72px] h-[32px] rounded text-[14px] flex items-center justify-center">
+                    私聊
+                </div>
+                <!-- 被关注 -->
+                <div v-if="item.focusStatus == 4"
+                    class="text-[#fff] bg-[#FD9A00] w-[72px] h-[32px] rounded text-[14px] flex items-center justify-center">
+                    回关
+                </div>
+            </div>
+        </div>
+
+    </div>
+</template>
+<script setup>
+import followSta from '../followSta'
+const chatStore = useChatStore()
+
+const containerRef = ref(null)
+const list = ref([])
+const pageNum = ref(1)
+const pageSize = ref(10)
+const totalCount = ref(0)
+async function getList() {
+    const data = {
+        pageNum: pageNum.value,
+        pageSize: pageSize.value,
+        noticeType: 4
+    }
+
+    const { data: Data } = await chatStore.getListSystemAndFocusMessages(data)
+
+    const { dataList, totalCount: count } = Data || {}
+    if (!Array.isArray(dataList)) return
+
+    totalCount.value = count
+
+    const arr = dataList.map((item) => {
+        try {
+            item.messageContent = JSON.parse(item.messageContent)
+        } catch (error) {
+
+        }
+        return item
+    })
+    if (pageNum == 1) {
+        list.value = arr
+    } else {
+        list.value = [...list.value, ...arr]
+    }
+
+}
+
+function addEventListenerContainerRef() {
+    if (containerRef.value) {
+        containerRef.value.removeEventListener('scroll', () => { })
+        containerRef.value.addEventListener('scroll', () => {
+            const { scrollTop, scrollHeight, clientHeight } = containerRef.value
+            if (scrollTop + clientHeight >= scrollHeight) {
+                if (list.value.length >= totalCount.value) return
+                pageNum += 1
+                getList()
+            }
+        })
+    }
+}
+
+onMounted(() => {
+    getList()
+    addEventListenerContainerRef()
+})
+</script>
+<style scoped lang="scss">
+.line3 {
+    overflow: hidden;
+    text-overflow: ellipsis;
+    display: -webkit-box;
+    -webkit-line-clamp: 3;
+    overflow: hidden;
+    -webkit-box-orient: vertical;
+}
+</style>

+ 154 - 0
src/pages/chat/components/RemoveMember.vue

@@ -0,0 +1,154 @@
+<template>
+    <XDialog :title="title || ''" :width="width" :show="show" @close="handleClose">
+        <div class="w-full bg-[#fff]">
+            <div class="mt-20 border-t-[1px] border-[#eee] flex justify-between">
+                <div ref="frendsBoxRef" class="w-[280px] h-[300px] border-r-[1px] border-[#eee] relative overflow-y-auto scrollbar">
+                    <div class="px-20 sticky top-0 bg-[#fff]">
+                        <div class="h-[15px]"></div>
+                        <div class="flex items-center border rounded-full h-[35px]">
+                            <img src="~assets/img/search.png" class="w-[16px] h-[16px] mx-10" alt="">
+                            <input v-model="keyword" @input="search" class="outline-none" type="text" placeholder="搜索群友">
+                        </div>
+                    </div>
+                    <div class="mt-5 pb-20">
+                        <template v-for="(item, index) in searchResult" :key="item.id">
+                            <div @click="item.isChecked = item.isChecked ? false:true" class="flex items-center flex-warp text-wrap hover:bg-[#eee] px-20 py-10 cursor-pointer">
+                                <img v-if="item.isChecked" class="w-[21px] h-[21px]" src="~assets/img/chat/radio_circle_orange.png" alt="">
+                                <img v-else class="w-[21px] h-[21px]" src="~assets/img/chat/radio_circle_gray.png" alt="" />
+                                <div class="w-[24px] h-[24px] rounded-full overflow-hidden mx-10">
+                                    <img :src="item?.headImageUrl" class="w-full h-full object-cover" alt="" />
+                                </div>
+                                <span>{{ item?.groupNickname }}</span>
+                            </div>
+                        </template>
+                    </div>
+                </div>
+                <div class="w-[280px] h-[300px]">
+                    <div class="h-[40px] flex items-center justify-between px-10">
+                        <div class="text-[#000] text-[14px]">已选群友</div>
+                        <div v-if="Array.isArray(searchResult)" class="text-[#999] text-[12px]">
+                            已选择{{ searchResult.filter(item => item.isChecked).length}}个群友</div>
+                    </div>
+                    <div class="h-[180px] overflow-y-auto scrollbar px-10">
+                        <div class="flex flex-wrap w-full">
+                            <template v-for="item in searchResult" :key="item.id">
+                                <div v-if="item.isChecked" class="w-[25%] mt-10">
+                                    <div class="w-[50%] aspect-[1/1] rounded-full mx-auto relative cursor-pointer">
+                                        <img :src="item?.headImageUrl" class="w-full h-full object-cover rounded-full" alt="" />
+                                        <div @click="item.isChecked = false" class="absolute top-[-5px] right-[-5px] w-[15px] h-[15px] bg-[#999] flex items-center justify-center text-[#fff] text-[10px] rounded-full">
+                                            x
+                                        </div>
+                                    </div>
+                                    <div class="truncate w-full text-[12px] text-[#333] text-center">
+                                        {{ item?.groupNickname }}
+                                    </div>
+                                </div>
+                            </template>
+                        </div>
+                    </div>
+                    <div class="h-[70px] cursor-pointer items-center justify-around w-full flex border-t-[1px] border-[#eee] mt-10">
+                        <div @click="handleClose"
+                            class="w-[80px] h-[32px] flex items-center justify-center border border-[#666] rounded text-[#666]">
+                            取消
+                        </div>
+                        <div @click="comfirm" class="w-[80px] h-[32px] flex items-center justify-center rounded text-[#fff] bg-[#FD9A00]">
+                            确认
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </XDialog>
+</template>
+<script setup>
+import XDialog from '@/components/XDialog';
+
+const props = defineProps({
+    title: String,
+    width: String,
+    show: Boolean,
+    groupId:String
+})
+const emit = defineEmits(['close','removeMember'])
+const frendsBoxRef = ref(null)
+const chatStore = useChatStore()
+const { messages, reqChatList, ws, curConversiton, receive, receiveGetter, isConnect } = storeToRefs(chatStore)
+function handleClose() {
+    emit('close')
+}
+
+const memberList = ref([])
+
+const searchResult = ref([])
+const keyword = ref('')
+async function search(){
+    if(!keyword.value) {
+        searchResult.value = [...memberList.value]
+        return
+    }
+    let result = []
+    memberList.value.map(item=>{
+        if(item?.groupNickname.indexOf(keyword.value) != -1){
+            result.push(item)
+        }
+    })
+    searchResult.value = result
+}
+
+// 确认
+async function comfirm(){
+    const arr = searchResult.value.filter(item=>item.isChecked)
+    if(!arr.length){
+        ElMessage.warning('请选择要移除的群成员')
+        return
+    }
+    const ids = arr.map(item=>item.userId)
+    const data = {
+        groupId:props.groupId,
+        ids
+    }
+    ElLoading.service({
+        lock:true,
+        text:'正在移除中...',
+        background:'rgba(0,0,0,0.2)'
+    })
+    await request('/website/tourMember/removeMember',{method:'post',body:data}).finally(()=>ElLoading.service().close())
+    ElMessage.success('移除成功')
+    emit('removeMember')
+    handleClose()
+}
+
+async function getGroupInfo(){
+    const query = {
+        groupId: props.groupId
+    }
+    const { data } = await request('/website/tourGroup/getGroupInfoAndMemberByGroupId', { query })
+    const { memberList:list,leaderId } = data || {}
+
+    if(Array.isArray(list)){
+        for(let i = 0;i<list.length;i++){
+            if(list[i].userId == leaderId){
+                list.splice(i,1)
+            }
+        }
+        memberList.value = list
+        searchResult.value = list
+    }
+}
+onMounted(() => {
+    getGroupInfo()
+})
+</script>
+<style scoped lang="scss">
+.scrollbar::-webkit-scrollbar {
+    width: 1px;
+    height: 1px;
+    background-color: #eee;
+}
+
+.scrollbar::-webkit-scrollbar-thumb {
+    border-radius: 10px;
+    //   -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
+    background-color: #FD9A00;
+}
+</style>

+ 174 - 0
src/pages/chat/components/SetGroupType.vue

@@ -0,0 +1,174 @@
+<template>
+    <XDialog :title="title || ''" :width="width" :show="show" @close="handleClose">
+        <div class="w-full bg-[#fff]">
+            <div class="px-20 w-full">
+
+                <div class="w-full mb-[20px]">
+                    <template v-for="(item, index) in groupTypeList" :key="index">
+                        <el-row class="mt-20">
+                            <template v-for="(subItem, subIndex) in item" :span="8" :key="subIndex">
+                                <el-col :span="8">
+                                    <div @click="handleTypeClick(subItem.id)" class="flex items-center cursor-pointer">
+                                        <div class="w-[16px] h-[16px]">
+                                            <img v-if="subItem?.typeIcon" :src="subItem?.typeIcon" alt="" />
+                                        </div>
+
+                                        <span class="text-[#333] text-[14px] mx-8">{{ subItem.typeName }}</span>
+                                        <img v-if="subItem.isChecked" class="w-[21px] h-[21px]"
+                                            src="~assets/img/chat/radio_circle_orange.png" alt="">
+                                        <img v-else class="w-[21px] h-[21px]"
+                                            src="~assets/img/chat/radio_circle_gray.png" alt="" />
+                                    </div>
+                                </el-col>
+                            </template>
+                        </el-row>
+                        <template v-for="(subItem, subIndex) in item" :key="subIndex">
+                            <div v-if="subTypeList.children && subTypeList.children.length && subItem.id == subTypeList.id"
+                                class="flex items-center flex-wrap bg-[#fafafa] pb-10 mt-10 rounded-[4px]">
+                                <div @click="handleSubTypeClick(tag.id)" v-for="(tag, tagIndex) in subTypeList.children"
+                                    :key="tagIndex"
+                                    class="px-10 py-4 border rounded text-[#333] text-[14px] mx-10 mt-10 cursor-pointer"
+                                    :class="[tag.isChecked ? 'bg-[#FD9A00]/[.06] text-[#FD9A00]' : '']">
+                                    {{ tag.typeName }}
+                                </div>
+                            </div>
+                        </template>
+                    </template>
+                </div>
+                <div class="flex justify-end items-center text-[14px] mb-20 cursor-pointer">
+                    <div @click="handleClose" class="w-[80px] h-[32px] mr-20 flex items-center justify-center border rounded text-[#999]">
+                        取消
+                    </div>
+                    <div @click="confirm" class="w-[80px] h-[32px] bg-[#FD9A00] flex items-center justify-center rounded text-[#fff]">
+                        确认选择
+                    </div>
+                </div>
+            </div>
+        </div>
+    </XDialog>
+</template>
+<script setup>
+import XDialog from '@/components/XDialog';
+import typeList from "../groupTypeList"
+import friends_mock from '../friends_mock';
+
+const props = defineProps({
+    title: String,
+    width: String,
+    show: Boolean,
+    belongTypeId: String,
+    groupId: String | Number,
+})
+
+const curTypeId = computed(() => props.belongTypeId)
+
+const emit = defineEmits(['close'])
+const frendsBoxRef = ref(null)
+const chatStore = useChatStore()
+const { messages, reqChatList, ws, curConversiton, receive, receiveGetter, isConnect } = storeToRefs(chatStore)
+function handleClose() {
+    emit('close')
+}
+const groupTypeList = ref([])
+function chunkTypeList() {
+    const arr = []
+    const chunkSize = 3;
+    for (let i = 0; i < typeList.length; i += chunkSize) {
+        arr.push(typeList.slice(i, i + chunkSize))
+    }
+    groupTypeList.value = arr;
+    console.log(groupTypeList.value)
+}
+const subTypeList = ref({})
+function handleTypeClick(typeId) {
+    for (let i = 0; i < groupTypeList.value.length; i++) {
+        for (let x = 0; x < groupTypeList.value[i].length; x++) {
+            if (groupTypeList.value[i][x].id == typeId) {
+                console.log('sid:', subTypeList.id)
+                if (typeId == subTypeList.value.id) {
+                    subTypeList.value = {}
+                    groupTypeList.value[i][x].isChecked = false
+                } else {
+                    groupTypeList.value[i][x].isChecked = true
+                    subTypeList.value = groupTypeList.value[i][x]
+                    console.log('subTypeList:', subTypeList.value)
+                }
+
+            } else {
+                groupTypeList.value[i][x].isChecked = false
+            }
+        }
+    }
+}
+function handleSubTypeClick(typeId) {
+    if (subTypeList.value.children && subTypeList.value.children.length) {
+        const list = subTypeList.value.children.map(item => {
+            if (item.id == typeId) {
+                item.isChecked = true
+                return item
+            } else {
+                item.isChecked = false
+                return item
+            }
+        })
+        subTypeList.value.children = list;
+        console.log('subTypeList.value:', subTypeList.value)
+    }
+}
+
+// 获取群类型(树形数据)
+async function treeType() {
+    const { data } = await request('/website/tourGroupType/treeType')
+
+    if (!Array.isArray(data)) return
+    
+    for (let i = 0; i < data.length; i++) {
+        if (data[i].id == curTypeId.value) {
+            data[i].isChecked = true
+            subTypeList.value = data[i]
+            break
+        }
+    }
+    if (!subTypeList.value.id) {
+        for (let i = 0; i < data.length; i++) {
+            for(let x = 0;x<data[i].children.length;x++){
+                if(data[i].children[x].id == curTypeId.value){
+                    data[i].isChecked = true
+                    data[i].children[x].isChecked = true
+                    subTypeList.value = data[i]
+                    break
+                }
+            }
+        }
+    }
+
+    const arr = []
+    const chunkSize = 3;
+    for (let i = 0; i < data.length; i += chunkSize) {
+        arr.push(data.slice(i, i + chunkSize))
+    }
+    groupTypeList.value = arr
+}
+async function confirm(){
+    console.log(subTypeList.value, 'subTypeList')
+    let id = null
+    if(subTypeList.value.children.length){
+        id = subTypeList.value.children.filter(item=>item.isChecked)[0].id
+    }else{
+        id = subTypeList.value.id
+    }
+    if(!id) return
+    const data = {
+        belongTypeId:id,
+        groupId:props.groupId
+    }
+    await request('/website/tourGroup/updateGroup',{method:'post',body:data})
+    ElMessage.success('选择成功')
+    handleClose()
+}
+onMounted(() => {
+    // chunkTypeList()
+    treeType()
+
+})
+</script>

+ 60 - 0
src/pages/chat/components/SystemMessage.vue

@@ -0,0 +1,60 @@
+<template>
+    <div class="w-full h-full overflow-y-auto relative">
+        <div class="w-full sticky top-0 bg-[#fff] h-[50px] border-b-[1px] border-[#dedede] flex items-center justify-between shrink-0">
+            <div class="font-bold text-[18px] mx-[15px]">
+                系统消息
+            </div>
+        </div>
+        <div class="px-12 pb-30">
+            <div @click="show = true" v-for="item in 100" class="flex mt-24 cursor-pointer">
+                <div class="w-[44px] h-[44px] rounded-full overflow-hidden shrink-0">
+                    <img class="w-full h-full object-cover"
+                        src="https://game.gtimg.cn/images/yxzj/img201606/skin/hero-info/511/511-bigskin-5.jpg" alt="" />
+                </div>
+                <div class="w-full ml-10">
+                    <div class="text-[12px] text-[#333] flex items-center">
+                        系统消息 02:39
+                    </div>
+                    <div class="bg-[#fafafa] rounded p-10 mt-8">
+                        <div class="text-[#000] text-[16px]">
+                            众行致远丨习主席复信里的中外情谊
+                        </div>
+                        <div class="mt-10 text-[#999] text-[14px] text-justify line3">
+                            今年以来,习近平主席多次给国外友人回信,鼓励中外人民加强交流、增进理解、扩大合作,为推动构今年以来,习近平主席多次给国外友人回信,鼓励中外人民加强交流、增进理解、扩大合作,为推动构今年今年以来,习近平主席多次给国外友人回信,鼓励中外人民加强交流、增进理解、扩大合作,为推动构今年以来,习近平年以来今年以来,习近平主席多次给国外友人回信,鼓励中外人民加强交流、增进理解、扩大合作,为推动构今年以来,习近平主席多次给国外友人回信,鼓励中外人民加强交流、增进理解、扩大合作,为推动构今年今年以来,习近平主席多次给国外友人回信,鼓励中外人民加强交流、增进理解、扩大合作,为推动构今年以来,习近平年以来
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+        <XDialog title="系统消息" width="560px" :show="show" @close="show = false">
+            <div class="p-20" style="min-height:300px">
+                <div class="text-[14px] text-[#000]">
+                    “最强县级市”女市长拟任新职
+                </div>
+                <div class="text-[#666] text-[12px] mt-8">
+                    2024-12-28 15:33:20
+                </div>
+                <div class="text-[12px] text-[#333] mt-10 text-justify" style="line-height:30px;">
+                    12月19日,江苏省委组织部发布一批干部任前公示。其中,现任昆山市委副书记、市长,昆山经济技术开发区管委。
+                </div>
+            </div>
+        </XDialog>
+    </div>
+</template>
+<script setup>
+import XDialog from '@/components/XDialog';
+const show = ref(false)
+function getList(){
+
+}
+</script>
+<style scoped lang="scss">
+    .line3{
+    overflow: hidden;
+    text-overflow: ellipsis;
+    display: -webkit-box;
+    -webkit-line-clamp: 3;
+    overflow:hidden;
+    -webkit-box-orient: vertical;
+}
+</style>

+ 89 - 0
src/pages/chat/components/TextMsg.vue

@@ -0,0 +1,89 @@
+<template>
+    <span v-if="!isLink">
+        {{ msgContent }}
+    </span>
+    <div v-else class="w-[260px] cursor-pointer">
+        <!-- <div>{{ msgContent }}</div> -->
+        <div @click="clickLink" class="w-[260px] bg-[#fff] rounded flex justify-between items-center px-20 py-10" style="min-height: 60px;">
+            <div class="w-[145px]">
+                <div class="text-[#999] text-[14px] msg3">
+                    {{ msgContent }}
+                </div>
+                <!-- <div class="text-[#666] text-[10px]">
+                   {{ msgContent }}
+                </div> -->
+            </div>
+            <div class="w-[50px] h-[50px] bg-[#eee] shrink-0 overflow-hidden rounded">
+                <img v-if="!isOutLink" class="w-full h-full object-cover" src="https://v.xiaoyaotravel.com/image/BannerInfo/imgUrl/45f3230bf29f4887b447d4c18ef47313.jpg" alt="">
+            </div>
+        </div>
+        <div v-if="isOutLink" class="text-[10px] text-[#f40]">
+            这是一个外部链接,安全性未知,请谨慎访问
+        </div>
+        <div v-else class="text-[10px] text-[green]">
+            逍遥游官方网站,可以放心访问!
+        </div>
+    </div>
+</template>
+<script setup>
+
+
+const props = defineProps({
+    msg: {
+        type: String,
+        default: '',
+    },
+})
+
+const isLink = ref(false)
+const isOutLink = ref(true)
+const msgContent = ref('')
+const urlOrigin = ref('')
+const hostname = ref('')
+function clickLink() {
+    window.open(msgContent.value,'_blank')
+}
+onMounted(() => {
+    let msg = ''
+
+    // 第一个trycatch把文本消息解析出来
+    try {
+        const content = JSON.parse(props.msg)
+        if (content.messageContent == undefined) {
+            msg = content
+        } else {
+            msg = content.messageContent
+        }
+    } catch (e) {
+        msg = props.msg
+    }
+
+    // 第二个trycatch检测文本消息是否是链接
+    try {
+        const url = new URL(msg)
+        urlOrigin.value = url.origin
+        hostname.value = url.hostname
+        console.log('hostname:',url.hostname)
+        if(url.hostname == 'www.xiaoyaotravel.com'){
+            isOutLink.value = false
+        }else{
+            isOutLink.value = true
+        }
+        isLink.value = true
+    } catch (e) {
+        
+    }
+    msgContent.value = msg
+})
+</script>
+
+<style lang="scss" scoped>
+.msg3 {
+    overflow: hidden;
+    text-overflow: ellipsis;
+    display: -webkit-box;
+    -webkit-line-clamp: 3;
+    overflow: hidden;
+    -webkit-box-orient: vertical;
+}
+</style>

+ 8 - 0
src/pages/chat/const.js

@@ -0,0 +1,8 @@
+export default {
+    CHAT_LIST:'chatList',//聊天列表
+    FANS_EACH_OTHER:'fansEachOther',//我的互关
+    FANS_LIST:'fansList',//我的粉丝
+    FOLLOW_LIST:'FollowList',//我的关注
+    GROUP_SQUARE:'groupSquare',//群聊广场
+    ADD_FOLLOW:'addFollow',//添加关注
+}

+ 108 - 0
src/pages/chat/emoji.js

@@ -0,0 +1,108 @@
+export default [
+  { name: '嘿嘿', emoji: '😀' },
+  { name: '哈哈', emoji: '😃' },
+  { name: '嘻嘻', emoji: '😁' },
+  { name: '苦笑', emoji: '😅' },
+  { name: '融化', emoji: '🫠' },
+  { name: '笑得满地打滚', emoji: '🤣' },
+  { name: '眨眼', emoji: '😉' },
+  { name: '大笑', emoji: '😄' },
+  { name: '笑哭了', emoji: '😂' },
+  { name: '羞涩微笑', emoji: '😊' },
+  { name: '呵呵', emoji: '🙂' },
+  { name: '微笑天使', emoji: '😇' },
+  { name: '斜眼笑', emoji: '😆' },
+  { name: '倒脸', emoji: '🙃' },
+  { name: '喜笑颜开', emoji: '🥰' },
+  { name: '微笑', emoji: '☺️' },
+  { name: '花痴', emoji: '😍' },
+  { name: '羞涩亲亲', emoji: '😚' },
+  { name: '好崇拜哦', emoji: '🤩' },
+  { name: '微笑亲亲', emoji: '😙' },
+  { name: '飞吻', emoji: '😘' },
+  { name: '含泪的笑脸', emoji: '🥲' },
+  { name: '亲亲', emoji: '😗' },
+  { name: '好吃', emoji: '😋' },
+  { name: '吐舌', emoji: '😛' },
+  { name: '单眼吐舌', emoji: '😜' },
+  { name: '滑稽', emoji: '🤪' },
+  { name: '眯眼吐舌', emoji: '😝' },
+  { name: '发财', emoji: '🤑' },
+  { name: '抱抱', emoji: '🤗' },
+  { name: '想一想', emoji: '🤔' },
+  { name: '不说', emoji: '🤭' },
+  { name: '致敬', emoji: '🫡' },
+  { name: '睁眼捂嘴', emoji: '🫢' },
+  { name: '偷看', emoji: '🫣' },
+  { name: '安静的脸', emoji: '🤫' },
+  { name: '闭嘴', emoji: '🤐' },
+  { name: '龇牙咧嘴', emoji: '😬' },
+  { name: '挑眉', emoji: '🤨' },
+  { name: '迷茫', emoji: '😶‍🌫️' },
+  { name: '呼气', emoji: '😮‍💨' },
+  { name: '冷漠', emoji: '😐' },
+  { name: '得意', emoji: '😏' },
+  { name: '说谎', emoji: '🤥' },
+  { name: '无语', emoji: '😑' },
+  { name: '不高兴', emoji: '😒' },
+  { name: '沉默', emoji: '😶' },
+  { name: '翻白眼', emoji: '🙄' },
+  { name: '松了口气', emoji: '😌' },
+  { name: '沉思', emoji: '😔' },
+  { name: '流口水', emoji: '🤤' },
+  { name: '睡着了', emoji: '😴' },
+  { name: '困', emoji: '😪' },
+  { name: '感冒', emoji: '😷' },
+  { name: '打喷嚏', emoji: '🤧' },
+  { name: '发烧脸发烧', emoji: '🥵' },
+  { name: '受伤', emoji: '🤕' },
+  { name: '冷脸', emoji: '🥶' },
+  { name: '恶心', emoji: '🤢' },
+  { name: '头昏眼花', emoji: '🥴' },
+  { name: '呕吐', emoji: '🤮' },
+  { name: '晕头转向', emoji: '😵' },
+  { name: '爆炸头', emoji: '🤯' },
+  { name: '晕', emoji: '😵‍💫' },
+  { name: '困扰', emoji: '😕' },
+  { name: '吃惊', emoji: '😮' },
+  { name: '忍住泪水', emoji: '🥹' },
+  { name: '失望但如释重负', emoji: '😥' },
+  { name: '痛苦', emoji: '😣' },
+  { name: '郁闷', emoji: '🫤' },
+  { name: '缄默', emoji: '😯' },
+  { name: '啊', emoji: '😦' },
+  { name: '哭', emoji: '😢' },
+  { name: '失望', emoji: '😞' },
+  { name: '担心', emoji: '😟' },
+  { name: '震惊', emoji: '😲' },
+  { name: '极度痛苦', emoji: '😧' },
+  { name: '放声大哭', emoji: '😭' },
+  { name: '汗', emoji: '😓' },
+  { name: '微微不满', emoji: '🙁' },
+  { name: '脸红', emoji: '😳' },
+  { name: '害怕', emoji: '😨' },
+  { name: '吓死了', emoji: '😱' },
+  { name: '累死了', emoji: '😩' },
+  { name: '不满', emoji: '☹️' },
+  { name: '恳求的脸', emoji: '🥺' },
+  { name: '冷汗', emoji: '😰' },
+  { name: '困惑', emoji: '😖' },
+  { name: '汗', emoji: '😓' },
+  { name: '打呵欠', emoji: '🥱' },
+  { name: '傲慢', emoji: '😤' },
+  { name: '生气的恶魔', emoji: '👿' },
+  { name: '怒火中烧', emoji: '😡' },
+  { name: '头骨', emoji: '💀' },
+  { name: '生气', emoji: '😠' },
+  { name: '骷髅', emoji: '☠️' },
+  { name: '嘴上有符号的脸', emoji: '🤬' },
+  { name: '恶魔微笑', emoji: '😈' },
+  { name: '大便', emoji: '💩' },
+  { name: '外星人', emoji: '👽' },
+  { name: '小丑脸', emoji: '🤡' },
+  { name: '外星怪物', emoji: '👾' },
+  { name: '食人魔', emoji: '👹' },
+  { name: '机器人', emoji: '🤖' },
+  { name: '小妖精', emoji: '👺' },
+  { name: '鬼', emoji: '👻' }
+]

+ 7 - 0
src/pages/chat/followSta.js

@@ -0,0 +1,7 @@
+export default {
+    0: { status: 0, text: '关注', describe: '未关注', bg: '#FD9A00' },
+    1: { status: 1, text: '已关注', describe: '已关注', bg: '#999' },
+    2: { status: 2, text: '相互关注', describe: '相互关注', bg: '#999' },
+    3: { status: 2, text: '自己', describe: '自己', bg: '#999' },
+    4: { status: 4, text: '回关', describe: '被关注', bg: '#FD9A00' }
+}

+ 152 - 0
src/pages/chat/friends_mock.js

@@ -0,0 +1,152 @@
+export default [
+    {
+      id: 1,
+      name: '张三',
+      avatar: 'https://game.gtimg.cn/images/yxzj/img201606/skin/hero-info/511/511-bigskin-5.jpg',
+      lastMessage: '晚上一起打游戏?',
+      time: '12:30',
+      signature: '生活就像一盒巧克力',
+      location: '北京',
+      age: 25
+    },
+    {
+      id: 2,
+      name: '李四',
+      avatar: 'https://game.gtimg.cn/images/yxzj/img201606/skin/hero-info/511/511-bigskin-5.jpg',
+      lastMessage: '项目进展得怎么样了?',
+      time: '09:15',
+      signature: '努力工作,好好生活',
+      location: '上海',
+      age: 28
+    },
+    {
+      id: 3,
+      name: '王五',
+      avatar: 'https://game.gtimg.cn/images/yxzj/img201606/skin/hero-info/511/511-bigskin-5.jpg',
+      lastMessage: '周末有空吗?',
+      time: '昨天',
+      signature: '追求进步',
+      location: '广州',
+      age: 24
+    },
+    {
+      id: 3,
+      name: '王五',
+      avatar: 'https://game.gtimg.cn/images/yxzj/img201606/skin/hero-info/511/511-bigskin-5.jpg',
+      lastMessage: '周末有空吗?',
+      time: '昨天',
+      signature: '追求进步',
+      location: '广州',
+      age: 24
+    },
+    {
+      id: 4,
+      name: '王五',
+      avatar: 'https://game.gtimg.cn/images/yxzj/img201606/skin/hero-info/511/511-bigskin-5.jpg',
+      lastMessage: '周末有空吗?',
+      time: '昨天',
+      signature: '追求进步',
+      location: '广州',
+      age: 24
+    },
+    {
+      id: 5,
+      name: '王五',
+      avatar: 'https://game.gtimg.cn/images/yxzj/img201606/skin/hero-info/511/511-bigskin-5.jpg',
+      lastMessage: '周末有空吗?',
+      time: '昨天',
+      signature: '追求进步',
+      location: '广州',
+      age: 24
+    },
+    {
+      id: 6,
+      name: '王五',
+      avatar: 'https://game.gtimg.cn/images/yxzj/img201606/skin/hero-info/511/511-bigskin-5.jpg',
+      lastMessage: '周末有空吗?',
+      time: '昨天',
+      signature: '追求进步',
+      location: '广州',
+      age: 24
+    },
+    {
+      id: 7,
+      name: '王五',
+      avatar: 'https://game.gtimg.cn/images/yxzj/img201606/skin/hero-info/511/511-bigskin-5.jpg',
+      lastMessage: '周末有空吗?',
+      time: '昨天',
+      signature: '追求进步',
+      location: '广州',
+      age: 24
+    },
+    {
+      id: 8,
+      name: '王五',
+      avatar: 'https://game.gtimg.cn/images/yxzj/img201606/skin/hero-info/511/511-bigskin-5.jpg',
+      lastMessage: '周末有空吗?',
+      time: '昨天',
+      signature: '追求进步',
+      location: '广州',
+      age: 24
+    },
+    {
+      id: 9,
+      name: '王五',
+      avatar: 'https://game.gtimg.cn/images/yxzj/img201606/skin/hero-info/511/511-bigskin-5.jpg',
+      lastMessage: '周末有空吗?',
+      time: '昨天',
+      signature: '追求进步',
+      location: '广州',
+      age: 24
+    },
+    {
+      id: 10,
+      name: '王五',
+      avatar: 'https://game.gtimg.cn/images/yxzj/img201606/skin/hero-info/511/511-bigskin-5.jpg',
+      lastMessage: '周末有空吗?',
+      time: '昨天',
+      signature: '追求进步',
+      location: '广州',
+      age: 24
+    },
+    {
+      id: 11,
+      name: '王五',
+      avatar: 'https://game.gtimg.cn/images/yxzj/img201606/skin/hero-info/511/511-bigskin-5.jpg',
+      lastMessage: '周末有空吗?',
+      time: '昨天',
+      signature: '追求进步',
+      location: '广州',
+      age: 24
+    },
+    {
+      id: 12,
+      name: '王五',
+      avatar: 'https://game.gtimg.cn/images/yxzj/img201606/skin/hero-info/511/511-bigskin-5.jpg',
+      lastMessage: '周末有空吗?',
+      time: '昨天',
+      signature: '追求进步',
+      location: '广州',
+      age: 24
+    },
+    {
+      id: 13,
+      name: '王五',
+      avatar: 'https://game.gtimg.cn/images/yxzj/img201606/skin/hero-info/511/511-bigskin-5.jpg',
+      lastMessage: '周末有空吗?',
+      time: '昨天',
+      signature: '追求进步',
+      location: '广州',
+      age: 24
+    },
+    {
+      id: 14,
+      name: '王五',
+      avatar: 'https://game.gtimg.cn/images/yxzj/img201606/skin/hero-info/511/511-bigskin-5.jpg',
+      lastMessage: '周末有空吗?',
+      time: '昨天',
+      signature: '追求进步',
+      location: '广州',
+      age: 24
+    }
+  ]

+ 30 - 0
src/pages/chat/groupTypeList.js

@@ -0,0 +1,30 @@
+// 创建父级数据数组
+const generateData = () => {
+    const data = [];
+    for (let i = 1; i <= 9; i++) {
+        const parent = {
+            typeName: `Type-${i}`,        
+            id: `id-${i}`,                  
+            parentId: 0,                 
+            typeIcon: `icon-${i}`,           
+            children: []                  
+        };
+        
+        for (let j = 1; j <= 10; j++) {
+            parent.children.push({
+                typeName: `Child-Type-${i}-${j}`,
+                id: `id-${i}-${j}`,
+                parentId: `id-${i}`, 
+                typeIcon: `icon-${i}-${j}`,
+                children: [] 
+            });
+        }
+
+        data.push(parent);
+    }
+
+    return data;
+};
+const result = generateData();
+
+export default result

+ 236 - 0
src/pages/chat/index.vue

@@ -0,0 +1,236 @@
+<template>
+  <div class="bg-[#ccc] border "
+    style="height: calc(100vh - 50px);background:url('https://xiaoyaotravel.obs.cn-north-4.myhuaweicloud.com/image/TourismIm/WechatIMG2811.jpg');background-size:cover;">
+
+    <div
+      class="flex h-[650px] w-[1200px] relative border bg-[#f7f8fa] mx-auto justify-between mt-[20px] rounded-[10px] overflow-hidden show-ani"
+      style="height:calc(100vh - 100px);max-height:800px;">
+
+      <!-- 左侧好友列表 -->
+      <div class="w-[330px] ml-[20px]">
+        <div class="h-[50px] flex items-center justify-between">
+          <div class="border rounded-[6px] h-[30px] flex items-center cursor-pointer">
+            <div @click="leftSwitch = constant.CHAT_LIST" class="flex items-center justify-center w-[88px] h-full"
+              :class="[leftSwitch == constant.CHAT_LIST ? 'bg-[#fd9a00] text-[#fff]' : 'text-[#999]']">
+              聊天列表
+            </div>
+            <div class="flex items-center justify-center w-[88px] h-full"
+              :class="[leftSwitch != constant.CHAT_LIST ? 'bg-[#fd9a00] text-[#fff]' : 'text-[#999]']">
+              <el-popover trigger="hover" :popper-style="{padding:0}" >
+                <template #reference>
+                  <div @click="leftSwitch = fansBtn" class="flex items-center justify-center rounded h-[28px]">
+                    <span v-if="fansBtn == constant.FANS_EACH_OTHER"
+                      :class="[leftSwitch != constant.CHAT_LIST ? 'text-[#fff]' : 'text-[#999]']">
+                      我的互关
+                    </span>
+                    <span v-if="fansBtn == constant.FOLLOW_LIST"
+                      :class="[leftSwitch != constant.CHAT_LIST ? 'text-[#fff]' : 'text-[#999]']">
+                      我的关注
+                    </span>
+                    <span v-if="fansBtn == constant.FANS_LIST"
+                      :class="[leftSwitch != constant.CHAT_LIST ? 'text-[#fff]' : 'text-[#999]']">
+                      我的粉丝
+                    </span>
+                    <template v-if="leftSwitch == constant.FANS_EACH_OTHER || fansBtn == constant.FOLLOW_LIST || fansBtn == constant.FANS_LIST">
+                      <img class="w-[16px] h-[16px]" src="~assets/img/chat/downArrowWhite.png" alt="" />
+                    </template>
+                    <img v-else class="w-[16px] h-[16px]" src="~assets/img/chat/downArrowGray.png" alt="" />
+                  </div>
+                </template>
+                <div class="border cursor-pointer">
+                  
+                      <div @click="fansBtn = constant.FANS_EACH_OTHER,leftSwitch=constant.FANS_EACH_OTHER" class="hover:bg-[#fd9a00] hover:text-[#fff] w-full h-30 flex items-center justify-center">
+                        我的互关
+                      </div>
+                    
+                    
+                      <div @click="fansBtn = constant.FOLLOW_LIST,leftSwitch=constant.FANS_EACH_OTHER" class="hover:bg-[#fd9a00] hover:text-[#fff] w-full h-30 flex items-center justify-center">
+                        我的关注
+                      </div>
+                    
+                    
+                      <div @click="fansBtn = constant.FANS_LIST,leftSwitch=constant.FANS_EACH_OTHER" class="hover:bg-[#fd9a00] hover:text-[#fff] w-full h-30 flex items-center justify-center">
+                        我的粉丝
+                      </div>
+                    
+                </div>
+              </el-popover>
+            </div>
+          </div>
+          <div>
+            <el-dropdown trigger="hover">
+              <div
+                class="text-[#fd9a00] border border-[#fd9a00] flex items-center justify-center rounded w-[68px] h-[28px]">
+                添加
+                <img class="w-[16px] h-[16px]" src="~assets/img/chat/downArrow.png" alt="">
+              </div>
+              <template #dropdown>
+                <el-dropdown-menu class="outline-none">
+                  <el-dropdown-item>
+                    <span @click="showCreatGroupModal = true">创建群聊</span>
+                  </el-dropdown-item>
+                  <el-dropdown-item>
+                    <span @click="showGroupSquareModal = true">群聊广场</span>
+                  </el-dropdown-item>
+                  <el-dropdown-item>
+                    <span @click="leftSwitch = constant.ADD_FOLLOW">添加用户</span>
+                  </el-dropdown-item>
+                </el-dropdown-menu>
+              </template>
+            </el-dropdown>
+          </div>
+        </div>
+        <div class="bg-[#fff] overflow-y-auto scrollbar rounded" style="height:calc(100% - 50px)">
+          <ChatList v-if="leftSwitch == constant.CHAT_LIST"></ChatList>
+          <template
+            v-else-if="leftSwitch == constant.FANS_EACH_OTHER || leftSwitch == constant.FOLLOW_LIST || leftSwitch == constant.FANS_LIST">
+            <FansList :type="fansBtn" @chat="leftSwitch = constant.CHAT_LIST"></FansList>
+          </template>
+          <FansList v-else-if="leftSwitch == constant.ADD_FOLLOW" :type="constant.ADD_FOLLOW"
+            @chat="leftSwitch = constant.CHAT_LIST"></FansList>
+        </div>
+      </div>
+      <div class="w-[830px] h-full bg-[#fff]">
+
+        <Chat v-if="curConversiton?.groupId>0 && (curConversiton?.noticeType == 1 || curConversiton?.noticeType == 2 || !curConversiton?.id) "></Chat>
+        <template v-if="curConversiton?.noticeType == 3">
+          <SystemMessage v-if="curConversiton?.groupId == -1"></SystemMessage>
+          <NewFans v-if="curConversiton?.groupId == -2"></NewFans>
+          <ActiveMessage v-if="curConversiton?.groupId == -3"></ActiveMessage>
+        </template>
+      </div>
+    </div>
+
+    <audio class="fixed top-0 left-0" ref="audioRef" :src="audioTips" style="opacity: 0;z-index:-1;"></audio>
+    <!-- 创建群聊弹窗 -->
+    <template v-if="showCreatGroupModal">
+      <CreateGroup :show="showCreatGroupModal" :showConfig="true" width="560px" title="创建群聊"
+        @close="showCreatGroupModal = false" />
+    </template>
+
+    <ApplyJoinGroup v-if="showApplyJoinGroupModal" :groupId="query_groupId" :groupInfo="appllyJoinGroup_info"
+      :show="showApplyJoinGroupModal" width="560px" title="群聊详情" @close="showApplyJoinGroupModal = false">
+    </ApplyJoinGroup>
+
+    <!-- 投诉弹窗 -->
+    <Complain v-if="showComplainModal" :show="showComplainModal" width="560px" title="投诉"
+      @close="showComplainModal = false" />
+
+    <!-- 群聊广场 -->
+    <GroupSquare v-if="showGroupSquareModal" :show="showGroupSquareModal" width="560px" title="群聊广场"
+      @close="showGroupSquareModal = false"></GroupSquare>
+  </div>
+</template>
+
+<script setup>
+import CreateGroup from './components/CreateGroup.vue';
+import ApplyJoinGroup from './components/ApplyJoinGroup.vue';
+import Complain from './components/Complain.vue';
+import ChatList from './components/ChatList.vue';
+import FansList from './components/FansList.vue';
+import Chat from './components/Chat.vue';
+import SystemMessage from './components/SystemMessage.vue';
+import ActiveMessage from './components/ActiveMessage.vue';
+import NewFans from './components/NewFans.vue';
+import GroupSquare from './components/GroupSquare.vue';
+import audioTips from '@/assets/audio/message.mp3'
+import constant from './const.js';
+
+const route = useRoute()
+const chatStore = useChatStore()
+const { user, onNewMessage, curConversiton,onPlayMsgAudio } = storeToRefs(chatStore)
+const audioRef = ref(null)
+
+// 地址栏有参数groupId、time就处理加群操作
+const query_groupId = computed(() => route.query.groupId)
+const query_time = computed(() => route.query.time)
+
+const showCreatGroupModal = ref(false)
+
+const showComplainModal = ref(false)
+
+const showApplyJoinGroupModal = ref(false)
+
+const showGroupSquareModal = ref(false)
+
+// 粉丝按钮 / 我的互关 我的关注 我的粉丝 
+const fansBtn = ref(constant.FANS_EACH_OTHER)
+
+// 新增关注
+const isAddFollow = ref(false)
+
+// 左边切换开关
+const leftSwitch = ref(constant.CHAT_LIST)
+
+async function getUserInfo() {
+  const { data } = await request('/website/tourism/user/view')
+  chatStore.user = data
+  user.value = data;
+  await chatStore.createConnection(data.pass)
+  chatStore.reqChatList()
+  if (query_groupId.value) {
+    appllyJoinGroup()
+  }
+}
+
+// 处理扫码加群
+const appllyJoinGroup_info = ref({})
+async function appllyJoinGroup() {
+
+  const { data } = await request('/website/tourGroup/getGroupInfoAndMemberByGroupId', { query: { groupId: query_groupId.value } })
+  appllyJoinGroup_info.value = data || {}
+
+
+  showApplyJoinGroupModal.value = true
+}
+watch(onPlayMsgAudio, () => {
+  console.log('播放消息提示音');
+  audioRef.value && audioRef.value.play()
+})
+onMounted(() => {
+  getUserInfo()
+  // document.body.style.overflow = 'hidden'
+  nextTick(() => {
+    document.body.style.overflow = 'hidden'
+  })
+
+})
+</script>
+<style lang="scss" scoped>
+* {
+  box-sizing: border-box;
+}
+
+.scrollbar::-webkit-scrollbar {
+  width: 0px;
+  height: 0px;
+  background-color: #ccc;
+}
+
+.show-ani {
+  animation: show-ani .5s ease-in-out;
+}
+
+@keyframes show-ani {
+  0% {
+    opacity: 0;
+    transform: translateX(-100%);
+  }
+
+  100% {
+    opacity: 1;
+    transform: translateX(0);
+  }
+}
+
+/* 滚动条滑块样式 */
+// .scrollbar::-webkit-scrollbar-thumb {
+//   border-radius: 10px;
+//   -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
+//   background-color: #555;
+// }
+
+::v-deep .el-dropdown-link:focus {
+  outline: none;
+}
+</style>

+ 169 - 0
src/pages/profile/components/followModal/followList.vue

@@ -0,0 +1,169 @@
+<template>
+  <div v-loading="loading && !list.length">
+    <div v-infinite-scroll="onLoad" :infinite-scroll-disabled="loading" class="h-700 follow-list">
+      <template v-if="list.length">
+        <div v-for="(item, i) in list" :key="i">
+          <div class="follow-item flex flex-row items-center">
+            <el-avatar :size="44" :src="item.avatar" alt="头像"/>
+            <div class="flex flex-col ml-8">
+              <div class="text-base text-black-3">{{ item.nickName }}</div>
+              <div class="mt-4 text-sm text-black-9">{{ item.fansNumText }} 粉丝</div>
+            </div>
+            <!--            <div
+                @click="handleFollow(item, i)"
+                class="follow-item__btn ml-auto cursor-pointer text-base"
+                :class="['bg-[' + FANS_STATUS[item.fansStatus].bg + ']']">
+              {{ FANS_STATUS[item.fansStatus].text }}
+            </div>-->
+            <el-button class="ml-auto" :disabled="item.saveLoading" :type="FANS_STATUS[item.fansStatus].bg"  @click="handleFollow(item, i)">{{ FANS_STATUS[item.fansStatus].text }}</el-button>
+          </div>
+        </div>
+        <div class="text-center text-black-9" v-if="loading">加载中...</div>
+        <div class="text-center text-black-9" v-if="!loading && list.length === total">没有更多了~</div>
+      </template>
+      <div v-else class="h-full grid place-items-center">
+        <el-empty :image-size="100" :description="currListConfig.empty"></el-empty>
+      </div>
+    </div>
+    <span class="iconfont icon-copy "></span>
+  </div>
+</template>
+
+<script setup>
+
+import {formatNumber} from "~/utils";
+import {handleResponse} from "~/utils/request";
+
+const props = defineProps({
+  listType: String,
+})
+
+const listConfig = new Map([
+  [
+    'friend',
+    {
+      empty: '暂无互关',
+      apiUrl: '/website/tourism/fans/getFriends',
+    }
+  ],
+  [
+    'follow',
+    {
+      empty: '暂无关注',
+      apiUrl: '/website/tourism/fans/getMyConCern',
+    }
+  ],
+  [
+    'fans',
+    {
+      empty: '暂无粉丝',
+      apiUrl: '/website/tourism/fans/getMyFans',
+    },
+
+  ]
+])
+const FANS_STATUS = {
+  0: { status: 0, text: '关注', describe: '未关注', bg: 'primary' },
+  1: { status: 1, text: '已关注', describe: '已关注', bg: 'info' },
+  2: { status: 2, text: '相互关注', describe: '相互关注', bg: 'info' },
+  3: { status: 2, text: '自己', describe: '自己', bg: 'info' },
+  4: { status: 4, text: '回关', describe: '被关注', bg: 'primary' }
+}
+const currListConfig = computed(() => {
+  return listConfig.get(props.listType) ?? listConfig.get('friend')
+})
+
+let loading = ref(false);
+let isFinished = ref(false);
+let pageNum = ref(0);
+let list = ref([]);
+let total = ref(0);
+
+const onLoad = async () => {
+  try {
+    if (loading.value || isFinished.value) return;
+    loading.value = true;
+    pageNum.value++;
+
+    const res = await request(`${currListConfig.value.apiUrl}`, {
+      query: {
+        pageNum: pageNum.value
+      },
+    });
+    await handleResponse(res)
+    const {dataList, totalCount} = res.data;
+    // console.log(dataList, totalCount, 'dataTotaldataTotal')
+    //TODO 删除测试数据
+    /*const dataList = new Array(20).fill({
+      attentionIdDictMap: {
+        name: 'name'
+      },
+      fansStatus: 1,
+      headImageUrl: 'headImageUrl',
+      fansNum: '56248856'
+    });
+    const totalCount = 20 * 3;*/
+
+    // console.log(totalCount, pageNum.value, props.listType, '接口数据')
+    const _list = dataList.map((o) => ({
+      ...o,
+      avatar: o.attentionIdDictMap.avatar,
+      nickName: o.attentionIdDictMap?.name ?? '',
+      fansNumText: formatNumber(o.fansNum || 0),
+      saveLoading: false
+    }))
+
+    list.value.push(..._list);
+    total.value = totalCount;
+    isFinished.value = list.value.length >= total.value;
+  } catch (e) {
+    console.log(e, '????')
+  } finally {
+    loading.value = false;
+  }
+}
+const handleFollow = async (item) => {
+  let currIndex = list.value.findIndex((o) => o.id === item.id)
+  if(currIndex < 0) return
+
+  try {
+    if (list.value[currIndex].saveLoading) return
+    list.value[currIndex].saveLoading = true;
+    const {data} = await request(`/website/tourism/fans/saveConcern`, {
+      method: 'post',
+      body: {
+        attentionId: item.attentionId
+      }
+    });
+    if(!data) return
+
+    list.value[currIndex].fansStatus = data.fansStatus;
+    ElMessage.success('操作成功')
+  } catch (e) {
+
+  } finally {
+    list.value[currIndex].saveLoading = false;
+  }
+}
+</script>
+<style scoped lang="scss">
+.follow-list {
+  height: 700px;
+  overflow: auto;
+  touch-action: none;
+
+  .follow-item {
+    margin-bottom: 20px;
+    padding: 0 20px;
+
+    .follow-item__btn {
+      width: 72px;
+      height: 32px;
+      color: #fff;
+      border-radius: 4px;
+      display: grid;
+      place-items: center;
+    }
+  }
+}
+</style>

+ 73 - 0
src/pages/profile/components/followModal/index.vue

@@ -0,0 +1,73 @@
+<template>
+  <el-dialog
+      class="xao-yao-dialog xao-yao-dialog__body--padding-h"
+      v-model="visible"
+      title="详情"
+      width="836px"
+      @closed="emit('close')"
+  >
+    <div class="modal-container ">
+      <div class="tab-list">
+<!--        <div v-for="(tab, i) in tabList" :key="i" class="tab__item" @click="changeTab(tab)" :class="tab.listType === currListType ? 'active' : ''">
+          {{ tab.label }}
+        </div>-->
+        <template v-for="(tab) in tabList" :key="tab.listType">
+          <el-button
+              :type="tab.listType === currListType ? 'primary' : 'default'"
+              :plain="tab.listType !== currListType"
+              @click="changeTab(tab)" >{{ tab.label }}</el-button>
+        </template>
+      </div>
+      <template v-for="(tab) in tabList" :key="tab.listType">
+        <FollowList
+            :list-type="tab.listType"
+            v-show="currListType === tab.listType"/>
+      </template>
+    </div>
+  </el-dialog>
+</template>
+<script setup>
+import FollowList from './followList.vue'
+const props = defineProps({
+  title: String,
+})
+const visible = defineModel('visible', false)
+const emit = defineEmits(['close'])
+const currListType = defineModel('currListType', 'friend')
+const tabList = ref([
+  { label: '互关', listType: 'friend' },
+  { label: '关注', listType: 'follow' },
+  { label: '粉丝', listType: 'fans' }
+])
+
+const changeTab = (tab) => {
+  currListType.value = tab.listType
+}
+</script>
+<style scoped lang="scss">
+.modal-container {
+  .tab-list {
+    display: grid;
+    grid-template-columns: 88px 88px 88px;
+    column-gap: 20px;
+    justify-content: center;
+    margin-bottom: 20px;
+    .tab__item {
+      width: 88px;
+      height: 32px;
+      border-radius: 4px;
+      display: grid;
+      place-items: center;
+      border: 1px solid #999999;
+      color: #999999;
+      cursor: pointer;
+
+      &.active {
+        color: #ffffff;
+        background: #fd9a00;
+        border-color: #fd9a00;
+      }
+    }
+  }
+}
+</style>

+ 221 - 37
src/pages/profile/index.client.vue

@@ -1,57 +1,241 @@
 <template>
-  <div>
-    <img src="~/assets/img/profile/profile_top_bg.png" class="h-auto w-full" />
-    <div class="flex justify-center bg-[#e9f1f8]">
-      <div class="w-wrap">
-        <div class="mb-50 bg-white pt-10">
-          <div class="shadow-lg">
-            <NuxtLink
-              to="/profile/userinfo"
-              class="flex cursor-pointer items-center justify-center space-x-10 px-20"
-            >
-              <img
-                :src="userInfo.headImageUrl || defaultAvatar"
-                class="h-76 w-76 shrink-0 rounded-full"
-              />
-              <div class="flex flex-col space-y-5">
-                <div class="flex items-center space-x-5">
-                  <span class="text-3xl font-semibold text-black-3">{{
-                    userInfo.showName
-                  }}</span>
-                  <span
-                    class="iconfont icon-edit-square text-black-9"
-                    style="font-size: 16px"
-                  ></span>
-                </div>
-
-                <span class="break-all text-sm text-black-3"
-                  >个性签名:{{ userInfo.personalSign || '暂未填写' }}</span
-                >
-              </div>
-            </NuxtLink>
-
-            <div class="flex justify-center px-20 py-20">
-              <ProfileHomeTabs />
-            </div>
+  <div class="profile-page">
+    <div class="page-content">
+      <div class="user-card box-border flex w-full flex-row items-center p-20">
+        <div class="relative mr-20">
+          <img
+            :src="userInfo?.headImageUrl || defaultAvatar"
+            class="user-card__image rounded-full"
+            alt="头像"
+          />
+          <div
+            v-if="userInfo.sex"
+            class="absolute -bottom-10 left-1/2 grid h-20 w-20 -translate-x-1/2 place-items-center rounded-full bg-[#ffffff]"
+          >
+            <img
+              v-if="userInfo.sex == 1"
+              src="../../assets/img/profile/profile_my_boy.png"
+              height="16"
+              width="16"
+              alt="性别"
+            />
+            <img
+              v-if="userInfo.sex == 2"
+              src="../../assets/img/profile/profile_my_girl.png"
+              height="16"
+              width="16"
+              alt="性别"
+            />
           </div>
-          <div class="min-h-500 px-20 pb-100 pt-20">
-            <NuxtPage />
+        </div>
+        <div class="flex min-w-0 flex-1 flex-col">
+          <div class="black-3 text-5xl font-bold">{{ userInfo?.showName }}</div>
+          <div class="user-card__data mt-8 w-max">
+            <template v-for="(tab, i) in tabList" :key="i">
+              <div
+                @click="openDetail(tab)"
+                class="text-base text-black-9"
+                :class="tab.listType ? 'cursor-pointer' : ''"
+              >
+                {{ tab.label }}
+                <span class="pl-4 font-medium text-black-3">{{
+                  tab.numText
+                }}</span>
+              </div>
+              <div class="line" v-if="i + 1 < tabList.length"></div>
+            </template>
           </div>
+          <template v-if="personalSign">
+            <div class="flex flex-row items-center mt-8 w-max text-nowrap text-sm text-black-3">
+              {{ personalSign }}
+                <el-popover
+                    v-if="isShowMore"
+                    placement="bottom-end"
+                    :width="200"
+                    trigger="hover"
+                >
+                  <span class="text-black-6 text-sm">{{ userInfo.personalSign }}</span>
+                  <template #reference>
+                    <span class="text-sm text-black-6 ml-5 cursor-pointer">更多</span>
+                  </template>
+                </el-popover>
+            </div>
+          </template>
         </div>
+        <NuxtLink class="ml-auto mt-auto" to="/profile/userinfo">
+          <el-button type="info">编辑资料</el-button>
+        </NuxtLink>
+      </div>
+      <div class="flex justify-center px-20 py-20">
+        <ProfileHomeTabs />
+      </div>
+      <div class="min-h-500 px-20 pb-100 pt-20">
+        <NuxtPage />
       </div>
     </div>
   </div>
+  <FollowModal
+    v-if="followModalProps.visible"
+    v-model:visible="followModalProps.visible"
+    v-model:curr-list-type="followModalProps.listType"
+    @close="getData"
+  ></FollowModal>
+  <!--  <div>
+      <img src="~/assets/img/profile/profile_top_bg.png" class="h-auto w-full" />
+      <div class="flex justify-center bg-[#e9f1f8]">
+        <div class="w-wrap">
+          <div class="mb-50 bg-white pt-10">
+            <div class="shadow-lg">
+              <NuxtLink
+                to="/profile/userinfo"
+                class="flex cursor-pointer items-center justify-center space-x-10 px-20"
+              >
+                <img
+                  :src="userInfo.headImageUrl || defaultAvatar"
+                  class="h-76 w-76 shrink-0 rounded-full"
+                />
+                <div class="flex flex-col space-y-5">
+                  <div class="flex items-center space-x-5">
+                    <span class="text-3xl font-semibold text-black-3">{{
+                      userInfo.showName
+                    }}</span>
+                    <span
+                      class="iconfont icon-edit-square text-black-9"
+                      style="font-size: 16px"
+                    ></span>
+                  </div>
+  
+                  <span class="break-all text-sm text-black-3"
+                    >个性签名:{{ userInfo.personalSign || '暂未填写' }}</span
+                  >
+                </div>
+              </NuxtLink>
+  
+              <div class="flex justify-center px-20 py-20">
+                <ProfileHomeTabs />
+              </div>
+            </div>
+            <div class="min-h-500 px-20 pb-100 pt-20">
+              <NuxtPage />
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>-->
 </template>
 
 <script setup>
 import defaultAvatar from '~/assets/img/default_avatar.png'
+import FollowModal from './components/followModal'
 
 const userInfoStore = useUserInfoStore()
 const { userInfo } = storeToRefs(userInfoStore)
 
+const tabList = ref([
+  { label: '互关', listType: 'friend', numText: '0' },
+  { label: '关注', listType: 'follow', numText: '0' },
+  { label: '粉丝', listType: 'fans', numText: '0' },
+  { label: '获赞', listType: null, numText: '0' }
+])
+
+
+// 个性签名显示逻辑
+let isShowMore = ref(false)
+const personalSign = computed(() => {
+  const personalSign = userInfo?.value.personalSign
+  console.log(personalSign, 'userInfo?.personalSign')
+  if (!personalSign) {
+    isShowMore.value = false
+    return ''
+  }
+  if (personalSign.length <= 30) {
+    isShowMore.value = false
+    return personalSign
+  }
+  isShowMore.value = true
+  return personalSign.substring(0, 30) + '...'
+})
+
+
+const followModalProps = ref({
+  visible: false,
+  listType: null
+})
+const openDetail = (tab) => {
+  try {
+    if (!tab.listType) return
+    followModalProps.value.visible = true
+    followModalProps.value.listType = tab.listType
+  } catch (e) {
+  } finally {
+  }
+}
+
+const getData = async () => {
+  try {
+    const [
+      { data: friendsData },
+      { data: concernData },
+      { data: fansData },
+      { data: likeData }
+    ] = await Promise.all([
+      request('/website/tourism/fans/getFriendsCount'),
+      request('/website/tourism/fans/getMyConcern'),
+      request('/website/tourism/fans/getMyFansCount'),
+      request('/website/tourism/fans/getMylikeCount')
+    ])
+    tabList.value[0].numText = formatNumber(friendsData)
+    tabList.value[1].numText = formatNumber(concernData)
+    tabList.value[2].numText = formatNumber(fansData)
+    tabList.value[3].numText = formatNumber(likeData)
+  } catch (e) {
+  } finally {
+  }
+}
 onMounted(() => {
   userInfoStore.getUserInfo()
+  getData()
 })
 </script>
 
-<style lang="scss" scoped></style>
+<style lang="scss" scoped>
+.profile-page {
+  height: calc(100vh - 60px);
+  background: #e9f1f8;
+
+  .page-content {
+    margin: 0 auto;
+    width: 1200px;
+    min-height: 600px;
+    background: #ffffff;
+
+    .user-card {
+      height: 122px;
+      background: url('../../assets/img/profile/profile_my_bg.png');
+      background-size: 100% 100%;
+
+      .user-card__image {
+        width: 64px;
+        height: 64px;
+        border-radius: 100%;
+        background: #333333;
+      }
+
+      .user-card__data {
+        color: #999999;
+        display: grid;
+        grid-template-columns: auto 2px auto 2px auto 2px auto;
+        grid-template-rows: 20px;
+        align-items: center;
+        column-gap: 16px;
+
+        .line {
+          height: 12px;
+          width: 1px;
+          background: #999999;
+        }
+      }
+    }
+  }
+}
+</style>

+ 8 - 2
src/pages/profile/userinfo/index.vue

@@ -65,11 +65,13 @@
             </el-form-item>
             <el-form-item label="个性签名:">
               <el-input
+                class="sign-textarea"
+                :class="form?.personalSign?.length >= 80 ? 'full' : ''"
                 type="textarea"
                 v-model="form.personalSign"
                 placeholder="例:摄影师/旅居澳洲/潜水爱好者"
                 style="width: 550px"
-                :maxlength="200"
+                :maxlength="80"
                 show-word-limit
                 :rows="5"
                 resize="none"
@@ -192,4 +194,8 @@ onChange(async (files) => {
 })
 </script>
 
-<style lang="scss" scoped></style>
+<style lang="scss" scoped>
+.sign-textarea.full :deep(.el-input__count) {
+  color: #FF476A;
+}
+</style>

+ 20 - 0
src/plugins/websocket.js

@@ -0,0 +1,20 @@
+
+
+import {useChatStore} from '@/stores/chat'
+
+export default defineNuxtPlugin((nuxtApp) => {
+    const chatStore = useChatStore()
+    chatStore.messages = '嘿嘿嘿'
+    const { messages } = storeToRefs(chatStore)
+    const uuid = new Date().getTime()
+    const socket = new WebSocket('ws://192.168.1.43:8080?uuid=' + uuid)
+    socket.onopen = function (e) {
+        
+    }
+    socket.onmessage = function (e) {
+        
+    }
+    nuxtApp.provide('socket', socket)
+    nuxtApp.provide('messages', messages)
+    
+})

+ 237 - 0
src/stores/chat.js

@@ -0,0 +1,237 @@
+export const useChatStore = defineStore('chat', () => {
+
+    const config = useRuntimeConfig()
+    const baseIM = config.public.baseIM
+
+    // 当前登录用户信息
+    const user = ref({})
+
+    const isConnect = ref(false)
+    // 连接状态 0: 未连接 1: 连接中 2: 已连接 3: 已断开
+    const connectSta = ref(0)
+
+    const receive = ref([])
+    const receiveGetter = computed(() => receive.value)
+    const onNewMessage = ref(Date.now())
+    const onPlayMsgAudio = ref(Date.now())//消息提示音
+
+    // 当前会话
+    const curConversiton = ref({})
+
+
+    // 创建webboteSocket连接
+    const ws = ref(null)
+    function createConnection(token) {
+        if(!process.client){
+            console.log('非浏览器环境不支持websocket')
+            reject('仅在客户端可连接')
+            return
+        }
+        return new Promise((resolve, reject) => {
+            if (connectSta.value == 2) {
+                console.log('连接已存在,请不要重复连接')
+                reject('连接已存在,请不要重复连接')
+                return
+            }
+            console.log('创建连接')
+            connectSta.value = 1
+            ws.value = new WebSocket(baseIM + '?token=' + token)
+
+            ws.value.onopen = () => {
+                console.log('聊天连接成功')
+                isConnect.value = true
+                connectSta.value = 2
+                resolve()
+            }
+
+            ws.value.onmessage = (e) => {
+                reqChatList()
+                onNewMessage.value = Date.now() + '' + Math.random() * 1000000
+                try {
+                    const messageBody = JSON.parse(e.data)
+                    // 判断该条消息是否属于当前会话
+                    if (messageBody.groupId == curConversiton.value.groupId) {
+
+                        let isReceive = true // 判断该条消息是收到的还是发出的
+
+                        for (let i = 0; i < receive.value.length; i++) {
+                            if (receive.value[i]?.object?.id == messageBody?.object?.id) {
+
+                                //如果是发出的消息,则设置消息的发送成功状态
+                                messageBody.sendSuccess = true
+                                receive.value[i] = messageBody
+                                isReceive = false
+                                break
+                            }
+                        }
+                        console.log('isReceive', isReceive)
+                        // 如果是收到的消息,则将消息push到receive数组中
+                        if (isReceive) {
+                            onPlayMsgAudio.value = Date.now() + '' + Math.random() * 1000000
+                            receive.value.push(messageBody)
+                        }
+                    }else{
+                        onPlayMsgAudio.value = Date.now() + '' + Math.random() * 1000000
+                    }
+                } catch (error) {
+                    console.log('消息解析出错::', error)
+                }
+
+            }
+
+            ws.value.onclose = () => {
+                console.log('聊天连接已关闭')
+                isConnect.value = false
+                connectSta.value = 3
+            }
+        })
+
+    }
+
+    // 会话列表
+    const chatList = ref([])
+    const conversations = computed(() => chatList.value)
+    // 请求聊天会话列表
+    async function reqChatList() {
+        const { data } = await request('/website/tourism/fans/getTourMemberList')
+        const { list } = data || {}
+        if (Array.isArray(list)) {
+            // 找到当前会话,清除未读消息
+            for (let i = 0; i < list.length; i++) {
+                if (list[i].groupId == curConversiton.value?.groupId) {
+                    list[i].unreadMessageCount = 0
+                    break;
+                }
+            }
+            chatList.value = list
+        }
+    }
+
+    const messages = ref('first message')
+
+    /**
+     * 设置会话 
+     * isTop 是否置顶0否1是 
+     * isNotDisturb 是否免打扰0否1是 
+     * isShow 是否显示0否1是
+     * */
+    async function setConversation(data = {}) {
+
+        const { groupId, isTop, isNotDisturb, isShow } = data || {}
+
+        if (!groupId) return
+
+        if (isTop != 0 && isTop != 1 && isNotDisturb != 0 && isNotDisturb != 1 && isShow != 0 && isShow != 1) {
+            return
+        }
+
+        await request('/website/tourMember/updateSingleTourMember', { method: 'post', body: data })
+
+        // 如果删除的会话是正在聊天的会话,则将当前会话置空
+        if (groupId == curConversiton.value.groupId && isShow == 0) {
+
+            curConversiton.value = {}
+
+        }
+
+        ElMessage.success('操作成功')
+
+        reqChatList()
+
+    }
+
+    // 关注、取关
+    const followLoadding = ref(false)
+    async function handleFollow(id, fansStatus) {
+
+        if (!id) return
+
+        // 不能关注或取关自己
+        if (fansStatus == 3) return
+
+        // 确保每次操作请求结束后 再执行下一次操作
+        if (followLoadding.value) return
+
+        followLoadding.value = true
+
+        await request('/website/tourism/fans/saveConcern', { method: 'post', body: { attentionId: id } }).finally(() => followLoadding.value = false)
+
+        ElMessage.success('操作成功')
+
+    }
+
+    /**
+     * 创建会话
+     * @param {Object} data 
+     * data.getUserId 接收者id(对方)
+     * data.sendUserId 发送者id(自己)
+     * data.groupId 如果是单聊,就在本地创建一个groupId,如果是群聊,必须传一个真实存在的groupId
+     * */ 
+    async function createConversation(data={},) {
+
+        const {getUserId,sendUserId,groupId,noticeType} = data
+
+        if(noticeType!=1 && noticeType!=2) return
+
+        if(noticeType==1 && !getUserId) return ElMessage.error('缺少必要参数')
+
+        if(!sendUserId || !groupId) return ElMessage.error('缺少必要参数')
+
+        await request('/website/tourGroup/createMember',{method:'post',body:data})
+
+        await reqChatList()
+        
+        if(noticeType==1){
+            for(let i = 0;i<chatList.value.length;i++){
+                if(chatList.value[i].toUserId==getUserId){
+                    curConversiton.value = chatList.value[i]
+                    break
+                }
+            }
+        }
+
+        if(noticeType==2){
+            for(let i = 0;i<chatList.value.length;i++){
+                if(chatList.value[i].groupId==groupId){
+                    curConversiton.value = chatList.value[i]
+                }
+            }
+        }
+
+    }
+    /**
+     *  获取系统消息和关注消息
+     *  @param {number} noticeType 1.单聊消息 2 群聊消息 3系统消息 4关注信息 5点赞 6评论 7评论区艾特 8文章内艾特 9访问
+     *  @param {number} pageSize 每页条数
+     *  @param {number} pageNum 页码
+     * */ 
+    async function getListSystemAndFocusMessages(obj={}){
+        return new Promise( async (resolve,reject)=>{
+
+            const query = {...obj}
+            const res = await request('/website/tourMessage/getsListSystemAndFocusMessages',{query})
+            console.log('各种消息:',res)
+            resolve(res)
+        })
+    }
+
+    return {
+        messages,
+        reqChatList,
+        createConnection,
+        ws,
+        conversations,
+        curConversiton,
+        receive,
+        receiveGetter,
+        user,
+        chatList,
+        isConnect,
+        connectSta,
+        onNewMessage,
+        setConversation,
+        handleFollow,
+        createConversation,
+        getListSystemAndFocusMessages
+    }
+})

+ 43 - 6
src/utils/index.js

@@ -1,4 +1,6 @@
 // 立即执行计时器
+import accounting from 'accounting'
+
 const setIntervalImmediately = (fn, duration) =>
   setInterval((() => (fn(), fn))(), duration)
 
@@ -41,10 +43,45 @@ function isEmail(email) {
   return false
 }
 
-export {
-  setIntervalImmediately,
-  jumpToLoginPage,
-  priceToArray,
-  formatImgSrc,
-  isEmail
+const isEmptyValue = (value) => {
+  if (value == null) return true
+
+  if (Array.isArray(value) && value.length === 0) return true
+
+  if (typeof value === 'object' && Object.keys(value).length === 0) return true
+
+  return false
+}
+
+const formatNumber = (n) => {
+  let num = Number(n ?? '')
+
+  // 如果数字大于或等于10,000万,则显示为9999w+
+  if (num >= 10000 * 10000) {
+    return '9999万+'
+  }
+  // 如果数字大于或等于1万,则转换为以“万”为单位,不四舍五入,最多保留1位小数
+  else if (num >= 10000) {
+    let w = Math.floor(num / 10000) // 取整,避免四舍五入
+    let remainder = num % 10000
+
+    // 计算小数部分并限制为最多两位
+    let decimalPart = ''
+    if (remainder > 0) {
+      // 将余数转换为1位小数,但不进行四舍五入
+      decimalPart = ('.' + Math.floor(remainder / 100)).slice(0, 2)
+      // 移除结尾的0,如果有的话
+      decimalPart = decimalPart.replace(/\.?0+$/, '')
+    }
+
+    return `${w}${decimalPart}万`
+  }
+
+  // 对于小于1万的数字直接输出
+  else {
+    return accounting.formatNumber(num)
+  }
 }
+
+export { setIntervalImmediately, jumpToLoginPage, formatImgSrc, isEmail, formatNumber, isEmptyValue }
+

+ 18 - 0
src/utils/request.js

@@ -92,3 +92,21 @@ const showErrorMessage = (msg) => {
   // const { message } = useMessage()
   message.error(msg)
 }
+
+export const handleResponse = (response, isNeedData = true) => {
+  return new Promise((resolve, reject) => {
+    const success = response.success
+    switch (success) {
+      case true: {
+        if (isNeedData) {
+          if (response.data && !isEmptyValue(response.data)) return resolve()
+          return reject(response)
+        }
+        return resolve()
+      }
+      default: {
+        return reject(response)
+      }
+    }
+  })
+}

Some files were not shown because too many files changed in this diff