Procházet zdrojové kódy

Merge branch 'dev_suwenjiang' into dev

# Conflicts:
#	.env.development
#	src/pages/profile/index.vue
#	src/utils/index.js
suwenjiang před 1 měsícem
rodič
revize
0503316995
100 změnil soubory, kde provedl 8821 přidání a 294 odebrání
  1. 28 2
      .env.development
  2. 1 0
      nuxt.config.ts
  3. 10 0
      package.json
  4. 182 120
      pnpm-lock.yaml
  5. binární
      src/assets/audio/message.mp3
  6. 540 11
      src/assets/iconfont/demo_index.html
  7. 97 5
      src/assets/iconfont/iconfont.css
  8. 0 0
      src/assets/iconfont/iconfont.js
  9. 163 2
      src/assets/iconfont/iconfont.json
  10. 47 1
      src/assets/iconfont/iconfont.svg
  11. binární
      src/assets/iconfont/iconfont.ttf
  12. binární
      src/assets/iconfont/iconfont.woff
  13. binární
      src/assets/iconfont/iconfont.woff2
  14. 12 0
      src/assets/img/chat/chat-code.svg
  15. 5 0
      src/assets/img/chat/check.svg
  16. 11 0
      src/assets/img/chat/city-one.svg
  17. 8 0
      src/assets/img/chat/close-remind.svg
  18. 8 0
      src/assets/img/chat/comment-orange.svg
  19. 8 0
      src/assets/img/chat/comment.svg
  20. 8 0
      src/assets/img/chat/comments-black.svg
  21. 8 0
      src/assets/img/chat/comments-orange.svg
  22. 8 0
      src/assets/img/chat/comments-white.svg
  23. 9 0
      src/assets/img/chat/ellipsis.svg
  24. 14 0
      src/assets/img/chat/group-avatar.svg
  25. 2 0
      src/assets/img/chat/guangchang.svg
  26. binární
      src/assets/img/chat/image-error.png
  27. binární
      src/assets/img/chat/image-loading.png
  28. 7 0
      src/assets/img/chat/like-orange.svg
  29. 7 0
      src/assets/img/chat/like.svg
  30. binární
      src/assets/img/chat/link-icon.png
  31. 7 0
      src/assets/img/chat/medical-files-orange.svg
  32. 7 0
      src/assets/img/chat/medical-files.svg
  33. 104 0
      src/assets/img/chat/no-conment.svg
  34. 3 0
      src/assets/img/chat/polygon.svg
  35. binární
      src/assets/img/chat/qr-code-box.png
  36. 7 0
      src/assets/img/chat/remind.svg
  37. 149 0
      src/assets/img/chat/search.svg
  38. 6 0
      src/assets/img/chat/send-orange.svg
  39. 6 0
      src/assets/img/chat/send.svg
  40. 5 0
      src/assets/img/chat/tiji-orange.svg
  41. 5 0
      src/assets/img/chat/tiji.svg
  42. 9 0
      src/assets/img/chat/user-add.svg
  43. 8 0
      src/assets/img/chat/user-grey.svg
  44. 8 0
      src/assets/img/chat/user.svg
  45. 8 0
      src/assets/img/chat/weixin-shake.svg
  46. binární
      src/assets/img/profile/pofile_qr.png
  47. binární
      src/assets/img/profile/profile_banner.png
  48. binární
      src/assets/img/scan/pic.png
  49. 26 0
      src/components/Chat/Dialog.vue
  50. 46 0
      src/components/Chat/Empty.vue
  51. 54 0
      src/components/Chat/GroupAvatar.vue
  52. 24 0
      src/components/Chat/Header.vue
  53. 26 0
      src/components/Chat/HeaderBar.vue
  54. 69 0
      src/components/Chat/Image.vue
  55. 41 0
      src/components/Chat/LeftItemMessage.vue
  56. 15 0
      src/components/Chat/MsgStatus.vue
  57. 59 0
      src/components/Chat/RightItemMessage.vue
  58. 38 0
      src/components/Chat/Search.vue
  59. 67 0
      src/components/Chat/Text.vue
  60. 194 0
      src/components/MultiHeader/index.vue
  61. 71 92
      src/components/NavigationBar/LeftMenu.vue
  62. 54 34
      src/components/NavigationBar/index.client.vue
  63. 74 0
      src/components/Profile/InteractionMessage/Eit.vue
  64. 73 0
      src/components/Profile/InteractionMessage/LikesandFavorites.vue
  65. 5 0
      src/components/Profile/InteractionMessage/MyComment.vue
  66. 156 0
      src/components/Profile/InteractionMessage/ReceiveComment.vue
  67. 69 0
      src/components/Profile/InteractionMessage/SendComment.vue
  68. 362 0
      src/components/Profile/News/ChatInput.vue
  69. 99 0
      src/components/Profile/News/GroupChat.vue
  70. 137 0
      src/components/Profile/News/SingleChat.vue
  71. 108 0
      src/components/Profile/News/emoji.js
  72. 101 0
      src/composables/useScanCode.js
  73. 11 0
      src/layouts/scan.vue
  74. 8 0
      src/middleware/01.intercept-components.global.js
  75. 48 0
      src/middleware/02.auth.global.js
  76. 0 27
      src/middleware/auth.global.js
  77. 145 0
      src/pages/chat/announcement.vue
  78. 84 0
      src/pages/chat/background.vue
  79. 265 0
      src/pages/chat/components/chat-input/index.vue
  80. 62 0
      src/pages/chat/components/chat-message/audio-message/index.vue
  81. 135 0
      src/pages/chat/components/chat-message/image-message/index.vue
  82. 133 0
      src/pages/chat/components/chat-message/index.vue
  83. 20 0
      src/pages/chat/components/chat-message/link-message/handle.js
  84. 88 0
      src/pages/chat/components/chat-message/link-message/index.vue
  85. 44 0
      src/pages/chat/components/chat-message/text-message/index.vue
  86. 317 0
      src/pages/chat/create-group.vue
  87. 190 0
      src/pages/chat/examine.vue
  88. 259 0
      src/pages/chat/group-add.vue
  89. 154 0
      src/pages/chat/group-all.vue
  90. 359 0
      src/pages/chat/group-chat.vue
  91. 228 0
      src/pages/chat/group-member.vue
  92. 332 0
      src/pages/chat/group-square.vue
  93. 100 0
      src/pages/chat/group.vue
  94. 91 0
      src/pages/chat/qr-code.vue
  95. 339 0
      src/pages/chat/qr-results.vue
  96. 308 0
      src/pages/chat/report.vue
  97. 335 0
      src/pages/chat/set-single.vue
  98. 168 0
      src/pages/chat/set-sub/index.vue
  99. 859 0
      src/pages/chat/set.vue
  100. 256 0
      src/pages/chat/single-add.vue

+ 28 - 2
.env.development

@@ -1,8 +1,34 @@
 VITE_APP_ENV=development
 
 # VITE_APP_BASE_URL=https://service.xiaoyaotravel.com/api/
-VITE_APP_BASE_URL=http://192.168.1.204:8082
+# VITE_APP_BASE_URL=http://101.126.146.250:8082/
+# 测试服
+
+
+
+# 李忠畅本地
+# VITE_APP_BASE_URL=http://192.168.1.38:8082/
+# 本地socoket
+# VITE_APP_IM_URL=ws://192.168.1.38:8082/system/message
+# 花生壳
+# VITE_APP_BASE_URL=http://cilicli.qicp.vip
+
+# 黄雯本地
+# VITE_APP_BASE_URL=http://192.168.1.44:8082/
+# 本地socoket
+# VITE_APP_IM_URL=ws://192.168.1.44:8082/system/message
+# 花生壳
+# VITE_APP_BASE_URL=http://q9943037p3.goho.co
+# VITE_APP_IM_URL=ws://q9943037p3.goho.co/system/message
+
+# 张维本地
+VITE_APP_BASE_URL=http://192.168.1.73:8082/
+# 本地socoket
+VITE_APP_IM_URL=ws://192.168.1.73:8082/system/message
+# 花生壳
+# VITE_APP_BASE_URL=http://4eqxwr.natappfree.cc
+# VITE_APP_IM_URL=ws://4eqxwr.natappfree.cc/system/message
+
 # VITE_APP_BASE_URL=http://192.168.1.204:8082
 VITE_APP_EMOJI_API=https://v.xiaoyaotravel.com/emoji/
-VITE_APP_WEBSITE_BASE_URL=http://101.126.146.250:8086
 VITE_APP_IM_USER_SUFFIX=dev

+ 1 - 0
nuxt.config.ts

@@ -27,6 +27,7 @@ export default defineNuxtConfig({
   runtimeConfig: {
     public: {
       baseApi: process.env.VITE_APP_BASE_URL,
+      baseIM:process.env.VITE_APP_IM_URL
     },
   },
   imports: {

+ 10 - 0
package.json

@@ -15,14 +15,24 @@
   "dependencies": {
     "@pinia/nuxt": "^0.5.4",
     "@samk-dev/nuxt-vcalendar": "^1.0.4",
+    "@vant/use": "^1.6.0",
     "@vueuse/core": "^12.0.0",
     "@vueuse/nuxt": "^12.0.0",
+    "accounting": "^0.4.1",
     "dayjs": "^1.11.13",
     "dayjs-nuxt": "^2.1.11",
+    "html5-qrcode": "^2.3.8",
+    "jsqr": "^1.4.0",
     "lodash-es": "^4.17.21",
+    "mitt": "^3.0.1",
+    "mockjs": "^1.1.0",
     "nuxt": "^3.13.0",
     "nuxt-swiper": "^2.0.0",
     "pinia": "^2.2.2",
+    "pinyin-pro": "^3.26.0",
+    "reconnecting-websocket": "^4.4.0",
+    "recorder-core": "^1.3.25011100",
+    "vconsole": "^3.15.1",
     "vue": "latest",
     "vue-clipboard3": "^2.0.0",
     "vue-cropper": "^1.1.4",

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 182 - 120
pnpm-lock.yaml


binární
src/assets/audio/message.mp3


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

@@ -55,6 +55,144 @@
           <ul class="icon_lists dib-box">
           
             <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>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe77e;</span>
+                <div class="name">info-circle</div>
+                <div class="code-name">&amp;#xe77e;</div>
+              </li>
+          
+            <li class="dib">
               <span class="icon iconfont">&#xe7ec;</span>
                 <div class="name">left</div>
                 <div class="code-name">&amp;#xe7ec;</div>
@@ -110,7 +248,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>
           
@@ -198,10 +336,10 @@
 <pre><code class="language-css"
 >@font-face {
   font-family: 'iconfont';
-  src: url('iconfont.woff2?t=1735367455324') format('woff2'),
-       url('iconfont.woff?t=1735367455324') format('woff'),
-       url('iconfont.ttf?t=1735367455324') format('truetype'),
-       url('iconfont.svg?t=1735367455324#iconfont') format('svg');
+  src: url('iconfont.woff2?t=1736319276637') format('woff2'),
+       url('iconfont.woff?t=1736319276637') format('woff'),
+       url('iconfont.ttf?t=1736319276637') format('truetype'),
+       url('iconfont.svg?t=1736319276637#iconfont') format('svg');
 }
 </code></pre>
           <h3 id="-iconfont-">第二步:定义使用 iconfont 的样式</h3>
@@ -228,6 +366,213 @@
         <ul class="icon_lists dib-box">
           
           <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
+            </div>
+            <div class="code-name">.icon-caret-up
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-info-circle"></span>
+            <div class="name">
+              info-circle
+            </div>
+            <div class="code-name">.icon-info-circle
+            </div>
+          </li>
+          
+          <li class="dib">
             <span class="icon iconfont icon-left"></span>
             <div class="name">
               left
@@ -309,11 +654,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>
           
@@ -445,6 +790,190 @@
           
             <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>
+                <div class="code-name">#icon-caret-up</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-info-circle"></use>
+                </svg>
+                <div class="name">info-circle</div>
+                <div class="code-name">#icon-info-circle</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
                   <use xlink:href="#icon-left"></use>
                 </svg>
                 <div class="name">left</div>
@@ -517,10 +1046,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">

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

@@ -1,9 +1,9 @@
 @font-face {
   font-family: "iconfont"; /* Project id 4723464 */
-  src: url('iconfont.woff2?t=1735367455324') format('woff2'),
-       url('iconfont.woff?t=1735367455324') format('woff'),
-       url('iconfont.ttf?t=1735367455324') format('truetype'),
-       url('iconfont.svg?t=1735367455324#iconfont') format('svg');
+  src: url('iconfont.woff2?t=1736319276637') format('woff2'),
+       url('iconfont.woff?t=1736319276637') format('woff'),
+       url('iconfont.ttf?t=1736319276637') format('truetype'),
+       url('iconfont.svg?t=1736319276637#iconfont') format('svg');
 }
 
 .iconfont {
@@ -14,6 +14,98 @@
   -moz-osx-font-smoothing: grayscale;
 }
 
+.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";
+}
+
+.icon-info-circle:before {
+  content: "\e77e";
+}
+
 .icon-left:before {
   content: "\e7ec";
 }
@@ -50,7 +142,7 @@
   content: "\e791";
 }
 
-.icon-delete:before {
+.icon-delete-two:before {
   content: "\e7c3";
 }
 

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 0
src/assets/iconfont/iconfont.js


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

@@ -6,6 +6,167 @@
   "description": "",
   "glyphs": [
     {
+      "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",
+      "unicode": "e8ed",
+      "unicode_decimal": 59629
+    },
+    {
+      "icon_id": "4765727",
+      "name": "info-circle",
+      "font_class": "info-circle",
+      "unicode": "e77e",
+      "unicode_decimal": 59262
+    },
+    {
       "icon_id": "4767012",
       "name": "left",
       "font_class": "left",
@@ -70,8 +231,8 @@
     },
     {
       "icon_id": "4766676",
-      "name": "delete",
-      "font_class": "delete",
+      "name": "delete-two",
+      "font_class": "delete-two",
       "unicode": "e7c3",
       "unicode_decimal": 59331
     },

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 47 - 1
src/assets/iconfont/iconfont.svg


binární
src/assets/iconfont/iconfont.ttf


binární
src/assets/iconfont/iconfont.woff


binární
src/assets/iconfont/iconfont.woff2


+ 12 - 0
src/assets/img/chat/chat-code.svg

@@ -0,0 +1,12 @@
+<svg width="14" height="16" viewBox="0 0 14 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="Two-dimensional-code-two (&#228;&#186;&#140;&#231;&#187;&#180;&#231;&#160;&#129;)">
+<path id="Vector" d="M5.25 2H1.75V6H5.25V2Z" stroke="#999999" stroke-linejoin="round"/>
+<path id="Vector_2" d="M5.25 10H1.75V14H5.25V10Z" stroke="#999999" stroke-linejoin="round"/>
+<path id="Vector_3" d="M12.25 2H8.75V6H12.25V2Z" stroke="#999999" stroke-linejoin="round"/>
+<path id="Vector_4" d="M7 2V6" stroke="#999999" stroke-linecap="round"/>
+<path id="Vector_5" d="M12.25 8H1.75" stroke="#999999" stroke-linecap="round"/>
+<path id="Vector_6" d="M9.91667 10V14" stroke="#999999" stroke-linecap="round"/>
+<path id="Vector_7" d="M12.25 10V14" stroke="#999999" stroke-linecap="round"/>
+<path id="Vector_8" d="M7.58333 10V14" stroke="#999999" stroke-linecap="round"/>
+</g>
+</svg>

+ 5 - 0
src/assets/img/chat/check.svg

@@ -0,0 +1,5 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="check">
+<path id="union" d="M5.96249 9.01514L11.4073 3.57031L12.2117 4.37465L5.96249 10.6238L2.06055 6.72187L2.86488 5.91753L5.96249 9.01514Z" fill="white"/>
+</g>
+</svg>

+ 11 - 0
src/assets/img/chat/city-one.svg

@@ -0,0 +1,11 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="City-one (&#229;&#159;&#142;&#229;&#184;&#130;)">
+<path id="Vector" d="M2 21H22" stroke="#FF6C5F" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path id="Vector_2" d="M9 11H5C4.44772 11 4 11.4477 4 12V20C4 20.5523 4.44772 21 5 21H9C9.55228 21 10 20.5523 10 20V12C10 11.4477 9.55228 11 9 11Z" fill="#FF6C5F" stroke="#FF6C5F" stroke-width="0.666667" stroke-linejoin="round"/>
+<path id="Vector_3" d="M19 2H11C10.4477 2 10 2.44772 10 3V20C10 20.5523 10.4477 21 11 21H19C19.5523 21 20 20.5523 20 20V3C20 2.44772 19.5523 2 19 2Z" fill="#FF6C5F" stroke="#FF6C5F" stroke-width="0.666667" stroke-linejoin="round"/>
+<path id="Vector_4" d="M14 16.0039H16" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path id="Vector_5" d="M6 16.0039H8" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path id="Vector_6" d="M14 11.5039H16" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path id="Vector_7" d="M14 7.00391H16" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+</g>
+</svg>

+ 8 - 0
src/assets/img/chat/close-remind.svg

@@ -0,0 +1,8 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="Close-remind (&#229;&#133;&#179;&#233;&#151;&#173;&#230;&#143;&#144;&#233;&#134;&#146;)">
+<path id="Vector" d="M14 12.6666C14 12.6666 12 11 12 6.33331C12 4.12418 10.2091 2.33331 8 2.33331C7.15333 2.33331 6.36807 2.59637 5.72157 3.04522M10 12.6666H2C2 12.6666 3.8564 11.1196 3.99217 6.83331" stroke="#666666" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path id="Vector_2" d="M6 12.6667H10C10 13.7713 9.10457 14.6667 8 14.6667C6.89543 14.6667 6 13.7713 6 12.6667Z" stroke="#666666" stroke-width="1.5"/>
+<path id="Vector_3" fill-rule="evenodd" clip-rule="evenodd" d="M7.99996 0.666687C7.26359 0.666687 6.66663 1.26364 6.66663 2.00002H9.33329C9.33329 1.26364 8.73633 0.666687 7.99996 0.666687Z" fill="#666666"/>
+<path id="Vector_4" d="M2.33337 2.16669L13.6667 14.8334" stroke="#666666" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+</g>
+</svg>

+ 8 - 0
src/assets/img/chat/comment-orange.svg

@@ -0,0 +1,8 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="Comment (&#232;&#175;&#132;&#232;&#174;&#186;)">
+<path id="Vector" d="M22 3H2V18H6.5V20.5L11.5 18H22V3Z" stroke="#FF9300" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path id="Vector_2" d="M7 9.75V11.25" stroke="#FF9300" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path id="Vector_3" d="M12 9.75V11.25" stroke="#FF9300" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path id="Vector_4" d="M17 9.75V11.25" stroke="#FF9300" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+</g>
+</svg>

+ 8 - 0
src/assets/img/chat/comment.svg

@@ -0,0 +1,8 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="Comment (&#232;&#175;&#132;&#232;&#174;&#186;)">
+<path id="Vector" d="M22 3H2V18H6.5V20.5L11.5 18H22V3Z" stroke="#666666" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path id="Vector_2" d="M7 9.75V11.25" stroke="#666666" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path id="Vector_3" d="M12 9.75V11.25" stroke="#666666" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path id="Vector_4" d="M17 9.75V11.25" stroke="#666666" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+</g>
+</svg>

+ 8 - 0
src/assets/img/chat/comments-black.svg

@@ -0,0 +1,8 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="Comments (&#232;&#175;&#132;&#232;&#174;&#186;)">
+<path id="Vector" d="M16.5 19H11V15H18V11H22V19H19.5L18 20.5L16.5 19Z" stroke="#666666" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path id="Vector_2" d="M2 3H18V15H8.5L6.5 17L4.5 15H2V3Z" stroke="#666666" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path id="Vector_3" d="M6 11H9" stroke="#666666" stroke-width="1.5" stroke-linecap="round"/>
+<path id="Vector_4" d="M6 7H12" stroke="#666666" stroke-width="1.5" stroke-linecap="round"/>
+</g>
+</svg>

+ 8 - 0
src/assets/img/chat/comments-orange.svg

@@ -0,0 +1,8 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="Comments (&#232;&#175;&#132;&#232;&#174;&#186;)">
+<path id="Vector" d="M16.5 19H11V15H18V11H22V19H19.5L18 20.5L16.5 19Z" stroke="#FF9300" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path id="Vector_2" d="M2 3H18V15H8.5L6.5 17L4.5 15H2V3Z" stroke="#FF9300" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path id="Vector_3" d="M6 11H9" stroke="#FF9300" stroke-width="1.5" stroke-linecap="round"/>
+<path id="Vector_4" d="M6 7H12" stroke="#FF9300" stroke-width="1.5" stroke-linecap="round"/>
+</g>
+</svg>

+ 8 - 0
src/assets/img/chat/comments-white.svg

@@ -0,0 +1,8 @@
+<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="Comments (&#232;&#175;&#132;&#232;&#174;&#186;)">
+<path id="Vector" d="M12.375 14.25H8.25V11.25H13.5V8.25H16.5V14.25H14.625L13.5 15.375L12.375 14.25Z" stroke="white" stroke-linecap="round" stroke-linejoin="round"/>
+<path id="Vector_2" d="M1.5 2.25H13.5V11.25H6.375L4.875 12.75L3.375 11.25H1.5V2.25Z" stroke="white" stroke-linecap="round" stroke-linejoin="round"/>
+<path id="Vector_3" d="M4.5 8.25H6.75" stroke="white" stroke-linecap="round"/>
+<path id="Vector_4" d="M4.5 5.25H9" stroke="white" stroke-linecap="round"/>
+</g>
+</svg>

+ 9 - 0
src/assets/img/chat/ellipsis.svg

@@ -0,0 +1,9 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="ellipsis">
+<g id="union">
+<path d="M4.5 13.5C5.32837 13.5 6 12.8284 6 12C6 11.1716 5.32837 10.5 4.5 10.5C3.67163 10.5 3 11.1716 3 12C3 12.8284 3.67163 13.5 4.5 13.5Z" fill="#333333"/>
+<path d="M10.5 12C10.5 12.8284 11.1716 13.5 12 13.5C12.8284 13.5 13.5 12.8284 13.5 12C13.5 11.1716 12.8284 10.5 12 10.5C11.1716 10.5 10.5 11.1716 10.5 12Z" fill="#333333"/>
+<path d="M18 12C18 12.8284 18.6716 13.5 19.5 13.5C20.3284 13.5 21 12.8284 21 12C21 11.1716 20.3284 10.5 19.5 10.5C18.6716 10.5 18 11.1716 18 12Z" fill="#333333"/>
+</g>
+</g>
+</svg>

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 14 - 0
src/assets/img/chat/group-avatar.svg


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 2 - 0
src/assets/img/chat/guangchang.svg


binární
src/assets/img/chat/image-error.png


binární
src/assets/img/chat/image-loading.png


+ 7 - 0
src/assets/img/chat/like-orange.svg

@@ -0,0 +1,7 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="Like (&#229;&#150;&#156;&#230;&#172;&#162;)">
+<path id="Vector" d="M7.5 4C4.46244 4 2 6.46245 2 9.5C2 15 8.5 20 12 21.1631C15.5 20 22 15 22 9.5C22 6.46245 19.5375 4 16.5 4C14.6399 4 12.9954 4.92345 12 6.3369C11.0046 4.92345 9.36015 4 7.5 4Z" stroke="#FF9300" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path id="Line 4" d="M10 13H14" stroke="#FF9300" stroke-width="1.5" stroke-linecap="round"/>
+<path id="Line 5" d="M12 11L12 15" stroke="#FF9300" stroke-width="1.5" stroke-linecap="round"/>
+</g>
+</svg>

+ 7 - 0
src/assets/img/chat/like.svg

@@ -0,0 +1,7 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="Like (&#229;&#150;&#156;&#230;&#172;&#162;)">
+<path id="Vector" d="M7.5 4C4.46244 4 2 6.46245 2 9.5C2 15 8.5 20 12 21.1631C15.5 20 22 15 22 9.5C22 6.46245 19.5375 4 16.5 4C14.6399 4 12.9954 4.92345 12 6.3369C11.0046 4.92345 9.36015 4 7.5 4Z" stroke="#666666" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path id="Line 4" d="M10 13H14" stroke="#666666" stroke-width="1.5" stroke-linecap="round"/>
+<path id="Line 5" d="M12 11L12 15" stroke="#666666" stroke-width="1.5" stroke-linecap="round"/>
+</g>
+</svg>

binární
src/assets/img/chat/link-icon.png


+ 7 - 0
src/assets/img/chat/medical-files-orange.svg

@@ -0,0 +1,7 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="Medical-files (&#229;&#140;&#187;&#231;&#150;&#151;&#230;&#161;&#163;&#230;&#161;&#136;)">
+<path id="Vector" d="M11.5 21H9.5H7.5H4.5C3.94771 21 3.5 20.5523 3.5 20V4C3.5 3.44771 3.94771 3 4.5 3H18.5C19.0523 3 19.5 3.44771 19.5 4V7.5V9.75" stroke="#FF9300" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path id="Vector_2" d="M18.3182 13.5C19.5231 13.5 20.5 14.4402 20.5 15.6C20.5 17.1098 19.0454 18.4 18.3182 19.1C17.8333 19.5667 17.2272 20.0334 16.5 20.5C15.7728 20.0334 15.1666 19.5667 14.6818 19.1C13.9545 18.4 12.5 17.1098 12.5 15.6C12.5 14.4402 13.4768 13.5 14.6818 13.5C15.4407 13.5 16.1091 13.873 16.5 14.4388C16.8909 13.873 17.5593 13.5 18.3182 13.5Z" stroke="#FF9300" stroke-width="1.5" stroke-linejoin="round"/>
+<path id="Vector_3" d="M7.5 7H15.5" stroke="#FF9300" stroke-width="1.5" stroke-linecap="round"/>
+</g>
+</svg>

+ 7 - 0
src/assets/img/chat/medical-files.svg

@@ -0,0 +1,7 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="Medical-files (&#229;&#140;&#187;&#231;&#150;&#151;&#230;&#161;&#163;&#230;&#161;&#136;)">
+<path id="Vector" d="M11.5 21H9.5H7.5H4.5C3.94771 21 3.5 20.5523 3.5 20V4C3.5 3.44771 3.94771 3 4.5 3H18.5C19.0523 3 19.5 3.44771 19.5 4V7.5V9.75" stroke="#666666" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path id="Vector_2" d="M18.3182 13.5C19.5231 13.5 20.5 14.4402 20.5 15.6C20.5 17.1098 19.0454 18.4 18.3182 19.1C17.8333 19.5667 17.2272 20.0334 16.5 20.5C15.7728 20.0334 15.1666 19.5667 14.6818 19.1C13.9545 18.4 12.5 17.1098 12.5 15.6C12.5 14.4402 13.4768 13.5 14.6818 13.5C15.4407 13.5 16.1091 13.873 16.5 14.4388C16.8909 13.873 17.5593 13.5 18.3182 13.5Z" stroke="#666666" stroke-width="1.5" stroke-linejoin="round"/>
+<path id="Vector_3" d="M7.5 7H15.5" stroke="#666666" stroke-width="1.5" stroke-linecap="round"/>
+</g>
+</svg>

+ 104 - 0
src/assets/img/chat/no-conment.svg

@@ -0,0 +1,104 @@
+<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="&#230;&#154;&#130;&#230;&#151;&#160;&#232;&#175;&#132;&#232;&#174;&#186;">
+<mask id="mask0_366_3616" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="160" height="160">
+<rect id="Rectangle 1" width="160" height="160" fill="#D9D9D9"/>
+</mask>
+<g mask="url(#mask0_366_3616)">
+<g id="img">
+<path id="&#232;&#183;&#175;&#229;&#190;&#132; 78" fill-rule="evenodd" clip-rule="evenodd" d="M109.223 52.7596C90.0359 37.9707 92.3252 23.608 85.2995 18.225C78.2738 12.8421 34.8448 27.0372 16.7589 74.3968C-0.614342 119.89 42.7834 147.656 80.4762 145.255C114.974 143.057 160.251 113.173 149.393 75.5256C145.142 60.7885 128.411 67.5484 109.223 52.7596Z" fill="url(#paint0_linear_366_3616)"/>
+<g id="&#230;&#164;&#173;&#229;&#156;&#134;&#229;&#189;&#162;" opacity="0.97998" filter="url(#filter0_i_366_3616)">
+<circle cx="60" cy="139" r="11" fill="url(#paint1_radial_366_3616)"/>
+</g>
+<g id="&#230;&#164;&#173;&#229;&#156;&#134;&#229;&#189;&#162;&#229;&#164;&#135;&#228;&#187;&#189; 3" opacity="0.5" filter="url(#filter1_i_366_3616)">
+<circle cx="58.5" cy="34.5" r="6.5" fill="url(#paint2_radial_366_3616)"/>
+</g>
+<g id="&#230;&#164;&#173;&#229;&#156;&#134;&#229;&#189;&#162;&#229;&#164;&#135;&#228;&#187;&#189; 4" opacity="0.2" filter="url(#filter2_i_366_3616)">
+<circle cx="77.5" cy="38.5" r="3.5" fill="url(#paint3_radial_366_3616)"/>
+</g>
+<path id="&#232;&#183;&#175;&#229;&#190;&#132; 112" opacity="0.395601" fill-rule="evenodd" clip-rule="evenodd" d="M70.0008 123.617L19.2578 117.27C19.2578 117.27 5.76725 92.9653 20.9999 65.714L54.4773 96.328L70.0008 123.617Z" fill="url(#paint4_linear_366_3616)"/>
+<g id="&#232;&#175;&#132;&#232;&#174;&#186;" opacity="0.5">
+<path id="&#232;&#183;&#175;&#229;&#190;&#132; 111" fill-rule="evenodd" clip-rule="evenodd" d="M61.0308 53.7561C62.1117 53.2857 63.3587 53.4053 64.3305 54.0725L72.3287 59.5638L31.1996 78.676V104.352L23.9244 100.151C22.5502 99.3572 21.6875 97.9062 21.6467 96.3199L21.1975 78.8547C21.0781 74.2131 23.7863 69.9631 28.0437 68.1104L61.0308 53.7561Z" fill="url(#paint5_linear_366_3616)"/>
+<path id="&#232;&#183;&#175;&#229;&#190;&#132; 111_2" fill-rule="evenodd" clip-rule="evenodd" d="M68.7998 59.1C71.0597 58.1056 73.5964 59.7607 73.5964 62.2298V81.5488C73.5964 85.0944 71.5964 88.3366 68.4279 89.9277V94.4628C68.4279 94.8035 68.1308 95.0681 67.7924 95.0289L58.8588 93.9938L35.3948 104.589C32.419 105.933 29.0367 103.804 28.9606 100.54L28.5865 84.5002C28.4789 79.8854 31.1654 75.6614 35.3904 73.8021L68.7998 59.1Z" fill="#FEECD1"/>
+<path id="&#230;&#164;&#173;&#229;&#156;&#134;&#229;&#189;&#162;_2" fill-rule="evenodd" clip-rule="evenodd" d="M42.3837 88.1626C43.7999 86.4749 44.0684 84.3686 42.9834 83.4582C41.8984 82.5478 39.8708 83.178 38.4546 84.8657C37.0384 86.5535 36.7699 88.6597 37.8549 89.5701C38.9399 90.4805 40.9675 89.8504 42.3837 88.1626Z" fill="#FDC166"/>
+<path id="&#232;&#183;&#175;&#229;&#190;&#132;" fill-rule="evenodd" clip-rule="evenodd" d="M41.7165 83.0415C42.2005 83.0358 42.6393 83.1696 42.9833 83.4582C44.0683 84.3686 43.7998 86.4749 42.3836 88.1626C40.9674 89.8504 38.9398 90.4805 37.8548 89.5701C37.6313 89.3825 37.4652 89.1442 37.3547 88.8692C38.3104 88.7952 39.4146 88.2608 40.3375 87.3379C41.7356 85.9398 42.2423 84.1255 41.6077 83.0441L41.7165 83.0415Z" fill="#FDA422"/>
+<path id="&#230;&#164;&#173;&#229;&#156;&#134;&#229;&#189;&#162;_3" fill-rule="evenodd" clip-rule="evenodd" d="M53.2116 83.6035C54.6278 81.9157 54.8963 79.8095 53.8113 78.8991C52.7263 77.9887 50.6987 78.6188 49.2825 80.3066C47.8663 81.9943 47.5978 84.1006 48.6828 85.011C49.7678 85.9214 51.7954 85.2912 53.2116 83.6035Z" fill="#FDC166"/>
+<path id="&#232;&#183;&#175;&#229;&#190;&#132;_2" fill-rule="evenodd" clip-rule="evenodd" d="M52.5444 78.4823C53.0284 78.4766 53.4672 78.6104 53.8112 78.8991C54.8962 79.8095 54.6277 81.9157 53.2115 83.6035C51.7953 85.2912 49.7677 85.9214 48.6827 85.011C48.4591 84.8234 48.293 84.585 48.1826 84.31C49.1382 84.2361 50.2425 83.7016 51.1654 82.7788C52.5635 81.3807 53.0701 79.5663 52.4356 78.485L52.5444 78.4823Z" fill="#FDA422"/>
+<path id="&#230;&#164;&#173;&#229;&#156;&#134;&#229;&#189;&#162;_4" fill-rule="evenodd" clip-rule="evenodd" d="M64.6095 78.4744C66.0257 76.7867 66.2942 74.6805 65.2092 73.7701C64.1243 72.8596 62.0967 73.4898 60.6805 75.1776C59.2643 76.8653 58.9958 78.9715 60.0808 79.8819C61.1657 80.7923 63.1933 80.1622 64.6095 78.4744Z" fill="#FDC166"/>
+<path id="&#232;&#183;&#175;&#229;&#190;&#132;_3" fill-rule="evenodd" clip-rule="evenodd" d="M63.9423 73.3533C64.4263 73.3476 64.8651 73.4814 65.2091 73.7701C66.2941 74.6805 66.0256 76.7867 64.6094 78.4744C63.1932 80.1622 61.1656 80.7923 60.0806 79.8819C59.8571 79.6944 59.691 79.456 59.5806 79.181C60.5362 79.1071 61.6405 78.5726 62.5633 77.6497C63.9614 76.2517 64.4681 74.4373 63.8335 73.3559L63.9423 73.3533Z" fill="#FDA422"/>
+</g>
+<g id="&#232;&#175;&#132;&#232;&#174;&#186;_2">
+<path id="&#232;&#183;&#175;&#229;&#190;&#132; 111_3" fill-rule="evenodd" clip-rule="evenodd" d="M122.346 34.8353C124.243 34.0099 126.431 34.2198 128.136 35.3905L142.171 45.0262L70.0009 78.5626V123.617L57.2351 116.245C54.8238 114.852 53.31 112.306 53.2384 109.523L52.4501 78.8762C52.2406 70.7316 56.9926 63.2739 64.4633 60.0231L122.346 34.8353Z" fill="url(#paint6_linear_366_3616)"/>
+<path id="&#232;&#183;&#175;&#229;&#190;&#132; 111_4" fill-rule="evenodd" clip-rule="evenodd" d="M135.978 44.2124C139.944 42.4673 144.395 45.3717 144.395 49.7041V83.6036C144.395 89.825 140.886 95.5143 135.326 98.3062V106.264C135.326 106.862 134.805 107.326 134.211 107.257L118.535 105.441L77.3621 124.032C72.1404 126.39 66.2056 122.656 66.072 116.928L65.4156 88.7825C65.2267 80.6848 69.9407 73.2728 77.3545 70.0103L135.978 44.2124Z" fill="#FEECD1"/>
+<path id="&#230;&#164;&#173;&#229;&#156;&#134;&#229;&#189;&#162;_5" fill-rule="evenodd" clip-rule="evenodd" d="M89.6257 95.2089C92.1107 92.2474 92.5819 88.5516 90.678 86.9541C88.7742 85.3566 85.2163 86.4623 82.7313 89.4239C80.2463 92.3854 79.7752 96.0812 81.679 97.6787C83.5829 99.2762 87.1407 98.1705 89.6257 95.2089Z" fill="#FDC166"/>
+<path id="&#232;&#183;&#175;&#229;&#190;&#132;_4" fill-rule="evenodd" clip-rule="evenodd" d="M88.4551 86.2228C89.3044 86.2128 90.0744 86.4476 90.678 86.9541C92.5819 88.5516 92.1107 92.2474 89.6257 95.2089C87.1407 98.1705 83.5828 99.2762 81.679 97.6787C81.2867 97.3496 80.9953 96.9313 80.8015 96.4488C82.4784 96.319 84.4161 95.3812 86.0354 93.7618C88.4886 91.3086 89.3777 88.1249 88.2643 86.2274L88.4551 86.2228Z" fill="#FDA422"/>
+<path id="&#230;&#164;&#173;&#229;&#156;&#134;&#229;&#189;&#162;_6" fill-rule="evenodd" clip-rule="evenodd" d="M108.626 87.2089C111.111 84.2474 111.582 80.5516 109.678 78.9541C107.774 77.3566 104.216 78.4623 101.731 81.4239C99.2463 84.3854 98.7752 88.0812 100.679 89.6787C102.583 91.2762 106.141 90.1705 108.626 87.2089Z" fill="#FDC166"/>
+<path id="&#232;&#183;&#175;&#229;&#190;&#132;_5" fill-rule="evenodd" clip-rule="evenodd" d="M107.455 78.2228C108.304 78.2128 109.074 78.4476 109.678 78.9541C111.582 80.5516 111.111 84.2474 108.626 87.2089C106.141 90.1705 102.583 91.2762 100.679 89.6787C100.287 89.3496 99.9953 88.9313 99.8015 88.4488C101.478 88.319 103.416 87.3812 105.035 85.7618C107.489 83.3086 108.378 80.1249 107.264 78.2274L107.455 78.2228Z" fill="#FDA422"/>
+<path id="&#230;&#164;&#173;&#229;&#156;&#134;&#229;&#189;&#162;_7" fill-rule="evenodd" clip-rule="evenodd" d="M128.626 78.2089C131.111 75.2474 131.582 71.5516 129.678 69.9541C127.774 68.3566 124.216 69.4623 121.731 72.4239C119.246 75.3854 118.775 79.0812 120.679 80.6787C122.583 82.2762 126.141 81.1705 128.626 78.2089Z" fill="#FDC166"/>
+<path id="&#232;&#183;&#175;&#229;&#190;&#132;_6" fill-rule="evenodd" clip-rule="evenodd" d="M127.455 69.2228C128.304 69.2128 129.074 69.4476 129.678 69.9541C131.582 71.5516 131.111 75.2474 128.626 78.2089C126.141 81.1705 122.583 82.2762 120.679 80.6787C120.287 80.3496 119.995 79.9313 119.802 79.4488C121.478 79.319 123.416 78.3812 125.035 76.7618C127.489 74.3086 128.378 71.1249 127.264 69.2274L127.455 69.2228Z" fill="#FDA422"/>
+</g>
+</g>
+</g>
+</g>
+<defs>
+<filter id="filter0_i_366_3616" x="49" y="128" width="22" height="22" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
+<feFlood flood-opacity="0" result="BackgroundImageFix"/>
+<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
+<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
+<feOffset/>
+<feGaussianBlur stdDeviation="4"/>
+<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
+<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.4 0"/>
+<feBlend mode="normal" in2="shape" result="effect1_innerShadow_366_3616"/>
+</filter>
+<filter id="filter1_i_366_3616" x="52" y="28" width="13" height="13" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
+<feFlood flood-opacity="0" result="BackgroundImageFix"/>
+<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
+<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
+<feOffset/>
+<feGaussianBlur stdDeviation="2.5"/>
+<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
+<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.4 0"/>
+<feBlend mode="normal" in2="shape" result="effect1_innerShadow_366_3616"/>
+</filter>
+<filter id="filter2_i_366_3616" x="74" y="35" width="7" height="7" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
+<feFlood flood-opacity="0" result="BackgroundImageFix"/>
+<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
+<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
+<feOffset/>
+<feGaussianBlur stdDeviation="1.5"/>
+<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
+<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.4 0"/>
+<feBlend mode="normal" in2="shape" result="effect1_innerShadow_366_3616"/>
+</filter>
+<linearGradient id="paint0_linear_366_3616" x1="-8.2605" y1="50.8347" x2="38.4885" y2="166.542" gradientUnits="userSpaceOnUse">
+<stop stop-color="#FEA82E"/>
+<stop offset="1" stop-color="#FEB243"/>
+</linearGradient>
+<radialGradient id="paint1_radial_366_3616" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(54.2893 139) rotate(-90) scale(14.5573)">
+<stop stop-color="white"/>
+<stop offset="1" stop-color="#FEC575"/>
+</radialGradient>
+<radialGradient id="paint2_radial_366_3616" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(55.1255 34.5) rotate(-90) scale(8.60203)">
+<stop stop-color="white"/>
+<stop offset="1" stop-color="#FEC575"/>
+</radialGradient>
+<radialGradient id="paint3_radial_366_3616" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(75.683 38.5) rotate(-90) scale(4.63186)">
+<stop stop-color="white"/>
+<stop offset="1" stop-color="#FEC575"/>
+</radialGradient>
+<linearGradient id="paint4_linear_366_3616" x1="70.0028" y1="111.325" x2="54.1673" y2="62.3723" gradientUnits="userSpaceOnUse">
+<stop stop-color="#FA8817"/>
+<stop offset="1" stop-color="#FEAE39"/>
+</linearGradient>
+<linearGradient id="paint5_linear_366_3616" x1="42.7245" y1="49.3774" x2="14.3769" y2="67.5451" gradientUnits="userSpaceOnUse">
+<stop stop-color="#FDCB83"/>
+<stop offset="0.749149" stop-color="#FEAF4C"/>
+<stop offset="1" stop-color="#FD9217"/>
+</linearGradient>
+<linearGradient id="paint6_linear_366_3616" x1="90.1443" y1="28.62" x2="43.3365" y2="60.6591" gradientUnits="userSpaceOnUse">
+<stop stop-color="#FDCB83"/>
+<stop offset="0.749149" stop-color="#FEAF4C"/>
+<stop offset="1" stop-color="#FD9217"/>
+</linearGradient>
+</defs>
+</svg>

+ 3 - 0
src/assets/img/chat/polygon.svg

@@ -0,0 +1,3 @@
+<svg width="24" height="8" viewBox="0 0 24 8" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M12 0L24 8H0L12 0Z" fill="#F7F8FA"/>
+</svg>

binární
src/assets/img/chat/qr-code-box.png


+ 7 - 0
src/assets/img/chat/remind.svg

@@ -0,0 +1,7 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="Remind (&#230;&#143;&#144;&#233;&#134;&#146;)">
+<path id="Vector" d="M5.5 9C5.5 5.41014 8.41014 2.5 12 2.5C15.5899 2.5 18.5 5.41014 18.5 9V18.5H5.5V9Z" stroke="white"/>
+<path id="Vector_2" d="M5 19V9C5 5.134 8.134 2 12 2C15.866 2 19 5.134 19 9V19M2 19H22" stroke="white" stroke-linecap="round" stroke-linejoin="round"/>
+<path id="Vector_3" d="M12 22C13.3807 22 14.5 20.8807 14.5 19.5V19H9.5V19.5C9.5 20.8807 10.6193 22 12 22Z" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+</g>
+</svg>

+ 149 - 0
src/assets/img/chat/search.svg

@@ -0,0 +1,149 @@
+<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="&#230;&#154;&#130;&#230;&#151;&#160;&#230;&#148;&#175;&#228;&#187;&#152;&#232;&#174;&#176;&#229;&#189;&#149;">
+<g id="img">
+<path id="&#232;&#183;&#175;&#229;&#190;&#132; 78" fill-rule="evenodd" clip-rule="evenodd" d="M118.351 66.3036C106.1 45.404 113.603 32.9447 109.106 25.3218C104.608 17.699 59.0238 14.5916 24.5136 51.7276C-8.63666 87.4002 21.1998 129.401 57.0475 141.295C89.8562 152.18 143.031 141.434 147.067 102.46C148.646 87.2033 130.601 87.2033 118.351 66.3036Z" fill="url(#paint0_linear_2611_11727)"/>
+<g id="&#230;&#184;&#144;&#229;&#143;&#152;&#229;&#157;&#151;">
+<mask id="mask0_2611_11727" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="10" y="20" width="138" height="126">
+<path id="&#232;&#146;&#153;&#231;&#137;&#136;" fill-rule="evenodd" clip-rule="evenodd" d="M118.351 66.3036C106.1 45.404 113.603 32.9447 109.106 25.3218C104.608 17.699 59.0238 14.5916 24.5136 51.7276C-8.63666 87.4002 21.1998 129.401 57.0475 141.295C89.8562 152.18 143.031 141.434 147.067 102.46C148.646 87.2033 130.601 87.2033 118.351 66.3036Z" fill="white"/>
+</mask>
+<g mask="url(#mask0_2611_11727)">
+<path id="&#232;&#183;&#175;&#229;&#190;&#132; 86" fill-rule="evenodd" clip-rule="evenodd" d="M40.4753 35.3262C40.4753 35.3262 61.8925 90.562 51.1839 147.781C40.4753 205 -30.1666 70.884 29.8378 40.0093C30.3504 40.0093 40.4753 35.3262 40.4753 35.3262Z" fill="url(#paint1_linear_2611_11727)"/>
+<path id="&#232;&#183;&#175;&#229;&#190;&#132; 85" fill-rule="evenodd" clip-rule="evenodd" d="M18.2404 58.8809C18.2404 58.8809 38.5956 66.8519 51.1839 140.091C45.1415 152.027 -7.99268 104.258 13.6429 65.1106L18.2404 58.8809Z" fill="#FDA423"/>
+<path id="&#232;&#183;&#175;&#229;&#190;&#132; 87" fill-rule="evenodd" clip-rule="evenodd" d="M131.147 78.4185C131.147 78.4185 118.927 95.7468 123 144C123 144 176.403 106.784 135.084 81.4221L131.147 78.4185Z" fill="#FDA423"/>
+<circle id="&#230;&#164;&#173;&#229;&#156;&#134;&#229;&#189;&#162;" cx="137.5" cy="109.5" r="3.5" fill="#FDB247"/>
+<g id="&#230;&#164;&#173;&#229;&#156;&#134;&#229;&#189;&#162;&#229;&#164;&#135;&#228;&#187;&#189;" filter="url(#filter0_i_2611_11727)">
+<circle cx="92.0001" cy="36" r="5" fill="url(#paint2_linear_2611_11727)"/>
+</g>
+<g id="&#230;&#164;&#173;&#229;&#156;&#134;&#229;&#189;&#162;&#229;&#164;&#135;&#228;&#187;&#189; 2" filter="url(#filter1_i_2611_11727)">
+<circle cx="41.0001" cy="54" r="6" fill="url(#paint3_linear_2611_11727)"/>
+</g>
+</g>
+</g>
+<g id="&#230;&#156;&#172;&#229;&#173;&#144;">
+<path id="&#232;&#183;&#175;&#229;&#190;&#132; 90" fill-rule="evenodd" clip-rule="evenodd" d="M115.814 86.166C115.814 86.166 125.83 92.621 116.895 102.272C107.961 111.924 99.2351 127.314 96.1153 132.783C92.9956 138.251 87.9847 130.439 87.9847 130.439L107.788 88.135L115.814 86.166Z" fill="url(#paint4_linear_2611_11727)"/>
+<path id="&#232;&#183;&#175;&#229;&#190;&#132; 89" fill-rule="evenodd" clip-rule="evenodd" d="M100.005 81.8022C100.005 81.8022 113.399 84.9054 115.814 86.1661C115.814 86.1661 107.458 88.4815 104.185 102.316C100.912 116.15 94.8815 134.167 86.1066 132.698C77.3317 131.23 74.5784 117.901 86.7914 98.8257C93.3554 88.5736 100.005 81.8022 100.005 81.8022Z" fill="url(#paint5_linear_2611_11727)"/>
+<g id="&#232;&#183;&#175;&#229;&#190;&#132; 88" filter="url(#filter2_i_2611_11727)">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M70.593 48C70.593 48 104.137 64.4542 113.809 61.9568C117.53 60.9959 108.269 72.1828 93.129 89.9597C77.9894 107.737 75.2264 128.688 84.4509 132.225C84.4509 132.225 64.5847 126.719 56.619 124.725C48.6534 122.732 34.6349 120.598 37.2492 106C39.8636 91.4021 50.5713 71.8698 58.1913 64.0172C65.8113 56.1645 69.5427 52.3189 70.593 48Z" fill="url(#paint6_linear_2611_11727)"/>
+</g>
+<path id="&#232;&#183;&#175;&#229;&#190;&#132;" d="M68.021 60.4937L100.048 71.2217L99.413 73.1181L67.3858 62.3901L68.021 60.4937Z" fill="url(#paint7_linear_2611_11727)"/>
+<path id="&#232;&#183;&#175;&#229;&#190;&#132;_2" d="M61.6232 75.2881L89.2983 85.0163L88.635 86.9031L60.96 77.1749L61.6232 75.2881Z" fill="url(#paint8_linear_2611_11727)"/>
+<path id="&#232;&#183;&#175;&#229;&#190;&#132;_3" d="M58.0854 85.0093L83.4573 93.2897L82.8368 95.191L57.4649 86.9106L58.0854 85.0093Z" fill="url(#paint9_linear_2611_11727)"/>
+<path id="&#232;&#183;&#175;&#229;&#190;&#132;_4" d="M53.3838 95.228L78.1333 102.53L77.5673 104.448L52.8178 97.1463L53.3838 95.228Z" fill="url(#paint10_linear_2611_11727)"/>
+<path id="&#232;&#183;&#175;&#229;&#190;&#132;_5" d="M48.5482 107.489L75.4149 115.503L74.8433 117.419L47.9766 109.406L48.5482 107.489Z" fill="url(#paint11_linear_2611_11727)"/>
+</g>
+<g id="&#230;&#148;&#190;&#229;&#164;&#167;&#233;&#149;&#156;">
+<ellipse id="&#230;&#164;&#173;&#229;&#156;&#134;&#229;&#189;&#162;_2" opacity="0.202962" cx="68.1958" cy="96.0901" rx="18" ry="22.5" transform="rotate(53 68.1958 96.0901)" fill="url(#paint12_linear_2611_11727)"/>
+<ellipse id="&#230;&#164;&#173;&#229;&#156;&#134;&#229;&#189;&#162;_3" cx="56.7999" cy="87.8956" rx="18" ry="25.5" transform="rotate(53 56.7999 87.8956)" fill="#FDA521"/>
+<path id="&#232;&#183;&#175;&#229;&#190;&#132;_6" d="M42.3879 102.202C43.0984 101.018 44.6341 100.635 45.8179 101.345C46.9525 102.026 47.3521 103.465 46.7581 104.625L46.6749 104.775L41.8204 112.864C41.1098 114.047 39.5741 114.431 38.3903 113.721C37.2558 113.04 36.8561 111.601 37.4501 110.44L37.5333 110.29L42.3879 102.202Z" fill="url(#paint13_linear_2611_11727)"/>
+<path id="&#232;&#183;&#175;&#229;&#190;&#132;_7" d="M34.4241 110.743C35.8588 108.383 38.9346 107.634 41.294 109.068C43.5879 110.463 44.3603 113.409 43.083 115.74L42.9684 115.938L32.3701 133.368C30.9354 135.727 27.8597 136.477 25.5002 135.042C23.2063 133.648 22.4339 130.701 23.7112 128.371L23.8258 128.172L34.4241 110.743Z" fill="url(#paint14_linear_2611_11727)"/>
+<ellipse id="&#230;&#164;&#173;&#229;&#156;&#134;&#229;&#189;&#162;_4" cx="53.7999" cy="84.8956" rx="18" ry="25.5" transform="rotate(53 53.7999 84.8956)" fill="url(#paint15_linear_2611_11727)"/>
+<ellipse id="&#230;&#164;&#173;&#229;&#156;&#134;&#229;&#189;&#162;_5" cx="53.3931" cy="84.62" rx="13.5" ry="21.5" transform="rotate(53 53.3931 84.62)" fill="url(#paint16_linear_2611_11727)"/>
+<path id="&#232;&#183;&#175;&#229;&#190;&#132;_8" fill-rule="evenodd" clip-rule="evenodd" d="M45.2686 73.8384C54.7517 66.6924 66.0767 65.7265 70.5638 71.681C71.3525 72.7277 71.8775 73.9186 72.1555 75.2094C67.0048 70.957 56.8777 72.351 48.2686 78.8384C40.4534 84.7276 36.3281 92.9763 37.6303 99.0285C37.1075 98.5977 36.636 98.1078 36.2225 97.559C31.7354 91.6045 35.7855 80.9844 45.2686 73.8384Z" fill="#FECD86"/>
+<path id="&#232;&#183;&#175;&#229;&#190;&#132;_9" fill-rule="evenodd" clip-rule="evenodd" d="M70.5638 71.681L70.6826 71.8455C65.4032 68.0445 55.6214 69.5442 47.2686 75.8384C38.3345 82.5708 34.2225 92.3866 37.5285 98.4796C37.5574 98.6651 37.5914 98.8478 37.6303 99.0285C37.1075 98.5977 36.636 98.1078 36.2225 97.559C31.7354 91.6045 35.7855 80.9844 45.2686 73.8384C54.7517 66.6924 66.0767 65.7265 70.5638 71.681Z" fill="#FDA423"/>
+</g>
+</g>
+</g>
+<defs>
+<filter id="filter0_i_2611_11727" x="87.0001" y="31" width="10" height="10" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
+<feFlood flood-opacity="0" result="BackgroundImageFix"/>
+<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
+<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
+<feOffset/>
+<feGaussianBlur stdDeviation="1.5"/>
+<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
+<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.2 0"/>
+<feBlend mode="normal" in2="shape" result="effect1_innerShadow_2611_11727"/>
+</filter>
+<filter id="filter1_i_2611_11727" x="35.0001" y="48" width="12" height="12" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
+<feFlood flood-opacity="0" result="BackgroundImageFix"/>
+<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
+<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
+<feOffset/>
+<feGaussianBlur stdDeviation="3"/>
+<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
+<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.2 0"/>
+<feBlend mode="normal" in2="shape" result="effect1_innerShadow_2611_11727"/>
+</filter>
+<filter id="filter2_i_2611_11727" x="35.929" y="48" width="78.7021" height="84.2246" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
+<feFlood flood-opacity="0" result="BackgroundImageFix"/>
+<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
+<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
+<feOffset dx="-1"/>
+<feGaussianBlur stdDeviation="1.5"/>
+<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
+<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.5 0"/>
+<feBlend mode="normal" in2="shape" result="effect1_innerShadow_2611_11727"/>
+</filter>
+<linearGradient id="paint0_linear_2611_11727" x1="10.1426" y1="20.5088" x2="10.1426" y2="145.304" gradientUnits="userSpaceOnUse">
+<stop stop-color="#FEA82E"/>
+<stop offset="1" stop-color="#FEB243"/>
+</linearGradient>
+<linearGradient id="paint1_linear_2611_11727" x1="10.1053" y1="66.9505" x2="75.0676" y2="108.538" gradientUnits="userSpaceOnUse">
+<stop stop-color="#FEB244"/>
+<stop offset="1" stop-color="#FEBB5A"/>
+</linearGradient>
+<linearGradient id="paint2_linear_2611_11727" x1="97.0001" y1="31" x2="87.0001" y2="31" gradientUnits="userSpaceOnUse">
+<stop stop-color="#FDC065"/>
+<stop stop-color="#FFBC59"/>
+<stop offset="1" stop-color="#FFCD80"/>
+</linearGradient>
+<linearGradient id="paint3_linear_2611_11727" x1="47.0001" y1="48" x2="35.0001" y2="48" gradientUnits="userSpaceOnUse">
+<stop stop-color="#FDC065"/>
+<stop stop-color="#FFBC59"/>
+<stop offset="1" stop-color="#FFCD80"/>
+</linearGradient>
+<linearGradient id="paint4_linear_2611_11727" x1="102.047" y1="74.3547" x2="68.5465" y2="106.71" gradientUnits="userSpaceOnUse">
+<stop stop-color="#FAA323"/>
+<stop offset="1" stop-color="#FEB140"/>
+</linearGradient>
+<linearGradient id="paint5_linear_2611_11727" x1="87.2061" y1="69.7539" x2="56.7756" y2="103.548" gradientUnits="userSpaceOnUse">
+<stop stop-color="#FAD59D"/>
+<stop offset="0.134591" stop-color="#FDCE88"/>
+<stop offset="1" stop-color="#FAC984"/>
+</linearGradient>
+<linearGradient id="paint6_linear_2611_11727" x1="62.0811" y1="54.1805" x2="31.9871" y2="109.344" gradientUnits="userSpaceOnUse">
+<stop stop-color="white"/>
+<stop offset="1" stop-color="#FEE4C0"/>
+</linearGradient>
+<linearGradient id="paint7_linear_2611_11727" x1="108.214" y1="60.4937" x2="92.9422" y2="40.7379" gradientUnits="userSpaceOnUse">
+<stop stop-color="#FCDAB5"/>
+<stop offset="1" stop-color="white"/>
+</linearGradient>
+<linearGradient id="paint8_linear_2611_11727" x1="96.3828" y1="75.2881" x2="82.1463" y2="57.9209" gradientUnits="userSpaceOnUse">
+<stop stop-color="#FCDAB5"/>
+<stop offset="1" stop-color="#FFF4E6"/>
+</linearGradient>
+<linearGradient id="paint9_linear_2611_11727" x1="89.9554" y1="85.0093" x2="77.5982" y2="69.2361" gradientUnits="userSpaceOnUse">
+<stop stop-color="#FCDAB5"/>
+<stop offset="1" stop-color="#FFF4E6"/>
+</linearGradient>
+<linearGradient id="paint10_linear_2611_11727" x1="84.4622" y1="95.228" x2="73.4921" y2="80.1682" gradientUnits="userSpaceOnUse">
+<stop stop-color="#FCDAB5"/>
+<stop offset="1" stop-color="#FFF4E6"/>
+</linearGradient>
+<linearGradient id="paint11_linear_2611_11727" x1="82.2745" y1="107.489" x2="70.4835" y2="91.1988" gradientUnits="userSpaceOnUse">
+<stop stop-color="#FCDAB5"/>
+<stop offset="1" stop-color="#FEE6C5"/>
+</linearGradient>
+<linearGradient id="paint12_linear_2611_11727" x1="84.4147" y1="73.6633" x2="55.4988" y2="76.5286" gradientUnits="userSpaceOnUse">
+<stop stop-color="#FDA521" stop-opacity="0.01"/>
+<stop offset="1" stop-color="#FDA521"/>
+</linearGradient>
+<linearGradient id="paint13_linear_2611_11727" x1="40.3873" y1="109.74" x2="43.4897" y2="110.847" gradientUnits="userSpaceOnUse">
+<stop stop-color="#FDE5C0"/>
+<stop offset="1" stop-color="#FDA521"/>
+</linearGradient>
+<linearGradient id="paint14_linear_2611_11727" x1="29.6021" y1="126.029" x2="35.0038" y2="127.949" gradientUnits="userSpaceOnUse">
+<stop stop-color="#FDE5C0"/>
+<stop offset="1" stop-color="#F7990C"/>
+</linearGradient>
+<linearGradient id="paint15_linear_2611_11727" x1="35.7999" y1="59.3956" x2="35.7999" y2="110.396" gradientUnits="userSpaceOnUse">
+<stop stop-color="#FDF1DB"/>
+<stop offset="1" stop-color="#FDE4BD"/>
+</linearGradient>
+<linearGradient id="paint16_linear_2611_11727" x1="39.8931" y1="63.12" x2="39.8931" y2="106.12" gradientUnits="userSpaceOnUse">
+<stop stop-color="#FEEED4"/>
+<stop offset="1" stop-color="#FFCE87"/>
+</linearGradient>
+</defs>
+</svg>

+ 6 - 0
src/assets/img/chat/send-orange.svg

@@ -0,0 +1,6 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="Send (&#229;&#143;&#145;&#233;&#128;&#129;)">
+<path id="Vector" d="M21.5 2.5L14.85 21.5L11.05 12.95L2.5 9.15L21.5 2.5Z" stroke="#FF9300" stroke-width="1.5" stroke-linejoin="round"/>
+<path id="Vector_2" d="M21.5 2.5L11.05 12.95" stroke="#FF9300" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+</g>
+</svg>

+ 6 - 0
src/assets/img/chat/send.svg

@@ -0,0 +1,6 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="Send (&#229;&#143;&#145;&#233;&#128;&#129;)">
+<path id="Vector" d="M21.5 2.5L14.85 21.5L11.05 12.95L2.5 9.15L21.5 2.5Z" stroke="#666666" stroke-width="1.5" stroke-linejoin="round"/>
+<path id="Vector_2" d="M21.5 2.5L11.05 12.95" stroke="#666666" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+</g>
+</svg>

+ 5 - 0
src/assets/img/chat/tiji-orange.svg

@@ -0,0 +1,5 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="tiji.svg">
+<path id="Vector" d="M15.096 17.2522H16.2012C18.9648 16.3618 20.562 14.2558 20.9916 10.933C21.0528 9.15345 20.7756 7.81905 20.1624 6.92865C18.5664 4.37745 15.9864 3.07305 12.4248 3.01305C11.1348 2.95425 9.59161 3.06345 8.00281 3.81465C4.75681 5.34825 3.27481 8.11545 3.02881 11.2006C2.89784 12.8755 3.18225 14.5568 3.85681 16.0954C5.39281 19.1794 7.84921 20.8114 11.2272 20.9902C14.2356 21.1078 16.908 20.1886 19.2408 18.2302L18.228 17.1622C15.4644 19.3582 12.456 19.9822 9.20041 19.0318C6.37561 17.845 4.83961 15.5902 4.59481 12.2686C4.64178 10.8373 5.01965 9.43628 5.69881 8.17545C7.05121 5.80185 9.22561 4.31625 12.24 4.34745C16.6764 4.39545 19.0872 6.48345 19.518 9.68745C19.578 12.6538 18.474 14.6422 16.2012 15.6502H15.6492C15.2184 15.3538 15.0648 14.9386 15.1884 14.4046L17.4912 7.01865H15.648L15.372 7.99665L14.8188 7.28505C13.2828 6.27705 11.6868 6.24705 10.0284 7.19625C8.55481 8.26425 7.60201 9.61305 7.17241 11.245C6.74281 12.877 6.77281 14.2114 7.26481 15.2494C7.75681 16.2874 8.61601 16.8958 9.84481 17.0734C11.256 17.1934 12.5148 16.6894 13.6212 15.5614V15.6502C13.6212 16.4218 14.1132 16.9558 15.0948 17.2522H15.096ZM10.95 15.4726L9.75241 15.3826C8.83201 15.0862 8.43241 14.3146 8.55601 13.069C9.04681 10.1026 10.368 8.41305 12.516 7.99665C13.806 8.05665 14.544 8.53065 14.7276 9.42105C14.5428 12.505 13.284 14.5234 10.95 15.4726Z" fill="#FF9300"/>
+</g>
+</svg>

+ 5 - 0
src/assets/img/chat/tiji.svg

@@ -0,0 +1,5 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="tiji.svg">
+<path id="Vector" d="M15.096 17.2522H16.2012C18.9648 16.3618 20.562 14.2558 20.9916 10.933C21.0528 9.15345 20.7756 7.81905 20.1624 6.92865C18.5664 4.37745 15.9864 3.07305 12.4248 3.01305C11.1348 2.95425 9.59161 3.06345 8.00281 3.81465C4.75681 5.34825 3.27481 8.11545 3.02881 11.2006C2.89784 12.8755 3.18225 14.5568 3.85681 16.0954C5.39281 19.1794 7.84921 20.8114 11.2272 20.9902C14.2356 21.1078 16.908 20.1886 19.2408 18.2302L18.228 17.1622C15.4644 19.3582 12.456 19.9822 9.20041 19.0318C6.37561 17.845 4.83961 15.5902 4.59481 12.2686C4.64178 10.8373 5.01965 9.43628 5.69881 8.17545C7.05121 5.80185 9.22561 4.31625 12.24 4.34745C16.6764 4.39545 19.0872 6.48345 19.518 9.68745C19.578 12.6538 18.474 14.6422 16.2012 15.6502H15.6492C15.2184 15.3538 15.0648 14.9386 15.1884 14.4046L17.4912 7.01865H15.648L15.372 7.99665L14.8188 7.28505C13.2828 6.27705 11.6868 6.24705 10.0284 7.19625C8.55481 8.26425 7.60201 9.61305 7.17241 11.245C6.74281 12.877 6.77281 14.2114 7.26481 15.2494C7.75681 16.2874 8.61601 16.8958 9.84481 17.0734C11.256 17.1934 12.5148 16.6894 13.6212 15.5614V15.6502C13.6212 16.4218 14.1132 16.9558 15.0948 17.2522H15.096ZM10.95 15.4726L9.75241 15.3826C8.83201 15.0862 8.43241 14.3146 8.55601 13.069C9.04681 10.1026 10.368 8.41305 12.516 7.99665C13.806 8.05665 14.544 8.53065 14.7276 9.42105C14.5428 12.505 13.284 14.5234 10.95 15.4726Z" fill="#666666"/>
+</g>
+</svg>

+ 9 - 0
src/assets/img/chat/user-add.svg

@@ -0,0 +1,9 @@
+<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="user-add">
+<g id="union">
+<path d="M4.70455 6.06522C4.70455 3.95813 6.35295 2.25 8.38636 2.25C10.4198 2.25 12.0682 3.95813 12.0682 6.06522C12.0682 8.1723 10.4198 9.88043 8.38636 9.88043C6.35295 9.88043 4.70455 8.1723 4.70455 6.06522ZM10.8409 6.06522C10.8409 4.66049 9.74197 3.52174 8.38636 3.52174C7.03076 3.52174 5.93182 4.66049 5.93182 6.06522C5.93182 7.46994 7.03076 8.6087 8.38636 8.6087C9.74197 8.6087 10.8409 7.46994 10.8409 6.06522Z" fill="white"/>
+<path d="M8.38636 10.5163C9.23392 10.5163 10.0564 10.6276 10.8409 10.8368V12.1567C10.0632 11.9168 9.23928 11.788 8.38636 11.788C6.5611 11.788 4.8688 12.3779 3.47727 13.3837L3.47727 15.6033H10.8409V16.875H3.47727C2.79947 16.875 2.25 16.3056 2.25 15.6033V13.284C2.25 12.807 2.50755 12.3701 2.91708 12.1524L3.62682 11.788H3.62945C5.03866 10.9777 6.66051 10.5163 8.38636 10.5163Z" fill="white"/>
+<path d="M13.2955 10.5163V13.0598H15.75V14.3315H13.2955V16.875H12.0682V14.3315H9.61364V13.0598H12.0682V10.5163H13.2955Z" fill="white"/>
+</g>
+</g>
+</svg>

+ 8 - 0
src/assets/img/chat/user-grey.svg

@@ -0,0 +1,8 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="user">
+<g id="union">
+<path d="M17.25 7.5C17.25 10.3995 14.8995 12.75 12 12.75C9.1005 12.75 6.75 10.3995 6.75 7.5C6.75 4.6005 9.1005 2.25 12 2.25C14.8995 2.25 17.25 4.6005 17.25 7.5ZM15.75 7.5C15.75 5.42893 14.0711 3.75 12 3.75C9.92893 3.75 8.25 5.42893 8.25 7.5C8.25 9.57107 9.92893 11.25 12 11.25C14.0711 11.25 15.75 9.57107 15.75 7.5Z" fill="#999999"/>
+<path d="M20.9447 16.2792C21.4455 16.5183 21.75 17.032 21.75 17.5869V21C21.75 21.4142 21.4142 21.75 21 21.75H3C2.58579 21.75 2.25 21.4142 2.25 21V17.5869C2.25 17.032 2.55452 16.5183 3.0553 16.2792C5.7741 14.9806 8.79976 14.25 12 14.25C15.2002 14.25 18.2259 14.9806 20.9447 16.2792ZM12 15.75C9.05011 15.75 6.26152 16.4186 3.75 17.6097V20.25H20.25V17.6097C17.7385 16.4186 14.9499 15.75 12 15.75Z" fill="#999999"/>
+</g>
+</g>
+</svg>

+ 8 - 0
src/assets/img/chat/user.svg

@@ -0,0 +1,8 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="user">
+<g id="union">
+<path d="M17.25 7.5C17.25 10.3995 14.8995 12.75 12 12.75C9.1005 12.75 6.75 10.3995 6.75 7.5C6.75 4.6005 9.1005 2.25 12 2.25C14.8995 2.25 17.25 4.6005 17.25 7.5ZM15.75 7.5C15.75 5.42893 14.0711 3.75 12 3.75C9.92893 3.75 8.25 5.42893 8.25 7.5C8.25 9.57107 9.92893 11.25 12 11.25C14.0711 11.25 15.75 9.57107 15.75 7.5Z" fill="white"/>
+<path d="M20.9447 16.2792C21.4455 16.5183 21.75 17.032 21.75 17.5869V21C21.75 21.4142 21.4142 21.75 21 21.75H3C2.58579 21.75 2.25 21.4142 2.25 21V17.5869C2.25 17.032 2.55452 16.5183 3.0553 16.2792C5.7741 14.9806 8.79976 14.25 12 14.25C15.2002 14.25 18.2259 14.9806 20.9447 16.2792ZM12 15.75C9.05011 15.75 6.26152 16.4186 3.75 17.6097V20.25H20.25V17.6097C17.7385 16.4186 14.9499 15.75 12 15.75Z" fill="white"/>
+</g>
+</g>
+</svg>

+ 8 - 0
src/assets/img/chat/weixin-shake.svg

@@ -0,0 +1,8 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="Weixin-shake (&#229;&#190;&#174;&#228;&#191;&#161;&#230;&#145;&#135;&#228;&#184;&#128;&#230;&#145;&#135;)">
+<path id="Vector" d="M21 9.5L14.5 3L3 14.5L9.5 21L21 9.5Z" fill="white" stroke="white" stroke-width="0.666667" stroke-linecap="round" stroke-linejoin="round"/>
+<path id="Vector_2" d="M8 14.5L9.5 16" stroke="white" stroke-width="0.666667" stroke-linecap="round" stroke-linejoin="round"/>
+<path id="Vector_3" d="M15 21L21 15" stroke="white" stroke-linecap="round" stroke-linejoin="round"/>
+<path id="Vector_4" d="M3 9L9 3" stroke="white" stroke-linecap="round" stroke-linejoin="round"/>
+</g>
+</svg>

binární
src/assets/img/profile/pofile_qr.png


binární
src/assets/img/profile/profile_banner.png


binární
src/assets/img/scan/pic.png


+ 26 - 0
src/components/Chat/Dialog.vue

@@ -0,0 +1,26 @@
+<template>
+  <van-dialog
+    style="--van-dialog-header-padding-top: 21px"
+    width="260"
+    confirmButtonColor="#FF9300"
+    v-model:show="show"
+    :title="title"
+    show-cancel-button
+    :confirmButtonText="confirmText"
+    :cancelButtonText="cancelText"
+    @confirm="$emit('confirm')"
+    @cancel="$emit('confirm')"
+  >
+    <slot></slot>
+  </van-dialog>
+</template>
+
+<script setup>
+const show = defineModel('show')
+const title = defineModel('title')
+const confirmText = defineModel('confirmText')
+const cancelText = defineModel('cancelText')
+defineEmits(['confirm', 'cancel'])
+</script>
+
+<style lang="scss" scoped></style>

+ 46 - 0
src/components/Chat/Empty.vue

@@ -0,0 +1,46 @@
+<template>
+  <div
+    :class="`w-full ${top ? 'mt-' + top : 'mt-100'} flex items-center justify-center flex-wrap min-h-200`"
+  >
+    <div v-if="img" class="w-160 h-160 shrink-0 mb-25">
+      <img class="w-full h-full object-cover" :src="img" alt="" />
+    </div>
+    <div class="w-full text-center text-[#BFC8DB]">{{ title }}</div>
+  </div>
+</template>
+
+<script setup>
+import search from '~/assets/img/chat/search.svg'
+import conment from '~/assets/img/chat/no-conment.svg'
+const imageList = [
+  {
+    name: 'search',
+    img: search
+  },
+  {
+    name: 'conment',
+    img: conment
+  }
+]
+
+const props = defineProps({
+  title: {
+    type: String,
+    default: '暂无数据'
+  },
+  top: {
+    type: String,
+    default: ''
+  },
+  image: {
+    type: String,
+    default: ''
+  }
+})
+
+const img = computed(() =>
+  props.image ? imageList.find((item) => item.name == props?.image).img : ''
+)
+</script>
+
+<style lang="scss" scoped></style>

+ 54 - 0
src/components/Chat/GroupAvatar.vue

@@ -0,0 +1,54 @@
+<template>
+  <div class="w-60 h-60 rounded-full overflow-hidden flex justify-start">
+    <!-- v-for="(item, index) in list :key="index"" -->
+    <div class="" v-for="(item, index) in 6" :key="index">
+      <img class="w-full h-full object-cover" :src="H5_default" alt="" />
+    </div>
+  </div>
+</template>
+
+<script setup>
+import H5_default from '../../assets/img/comment/H5_default.png'
+defineProps({
+  list: {
+    type: Array || String,
+    default: []
+  }
+})
+
+onMounted(() => {
+  // list = [H5_default, H5_default, H5_default, H5_default]
+  // console.log(list, 'list')
+})
+</script>
+
+<style lang="scss" scoped>
+.avatar-list {
+  display: grid;
+
+  grid-auto-flow: dense; /* 确保项目会填充所有的网格单元 */
+  grid-gap: 1px;
+}
+
+.one {
+  grid-template-columns: repeat(2, 1fr);
+  .item__child {
+    grid-column: span 2;
+  }
+}
+
+.two {
+  grid-template-columns: repeat(2, 1fr);
+  .item__child {
+    grid-column: span 2;
+  }
+}
+.three {
+  grid-template-columns: repeat(3, 1fr);
+  .item__child {
+    width: 17px;
+    height: 17px;
+    grid-column: span 3;
+  }
+}
+</style>

+ 24 - 0
src/components/Chat/Header.vue

@@ -0,0 +1,24 @@
+<template>
+  <div class="w-full h-48 fixed top-0 left-0 bg-white flex justify-between items-center px-12">
+    <van-icon name="arrow-left" size="24" @click="getBack" />
+    <h1>{{ title }}</h1>
+    <div></div>
+  </div>
+</template>
+
+<script setup>
+const router = useRouter()
+
+const props = defineProps({
+  title: {
+    type: String,
+    default: ''
+  }
+})
+
+const getBack = () => {
+  router.back()
+}
+</script>
+
+<style lang="scss" scoped></style>

+ 26 - 0
src/components/Chat/HeaderBar.vue

@@ -0,0 +1,26 @@
+<template>
+  <van-nav-bar :title="title" fixed @click-left="getBack">
+    <template #left>
+      <div>
+        <van-icon name="arrow-left" color="black" size="18" />
+      </div>
+    </template>
+  </van-nav-bar>
+</template>
+
+<script setup>
+const router = useRouter()
+
+const props = defineProps({
+  title: {
+    type: String,
+    default: ''
+  }
+})
+
+const getBack = () => {
+  router.back()
+}
+</script>
+
+<style lang="scss" scoped></style>

+ 69 - 0
src/components/Chat/Image.vue

@@ -0,0 +1,69 @@
+<template>
+  <!-- 聊天消息内容的文本展示 -->
+
+  <van-popover
+    teleport="images"
+    theme="dark"
+    placement="bottom"
+    actions-direction="horizontal"
+    v-model:show="showPopover"
+    @select="select"
+  >
+    <div class="flex my-16 mx-10">
+      <div v-for="(item, index) in actions" :key="index" @click="" class="mx-10 text-center">
+        <span :class="`iconfont ${item.icon} text-white   mb-4`" style="font-size: 24px"></span>
+        <p class="text-white text-base">{{ item.text }}</p>
+      </div>
+    </div>
+    <template #reference>
+      <img
+        class="w-full images h-full object-contain"
+        :src="itemData?.url"
+        @touchstart="doTouchstart"
+        alt=""
+      />
+    </template>
+  </van-popover>
+</template>
+
+<script setup>
+import { useClipboard } from '@vueuse/core'
+const { copy, isSupported } = useClipboard()
+
+const showPopover = ref(false)
+
+defineProps({
+  itemData: {
+    type: String,
+    default: ''
+  }
+})
+
+defineEmits(['onDelete'])
+
+const actions = [
+  { text: '引用', icon: 'icon-quote' },
+  { text: '复制', icon: 'icon-copy' },
+  { text: '删除', icon: 'icon-delete-three' }
+]
+
+function doTouchstart() {
+  showPopover.value = true
+}
+
+function select(action, index) {
+  console.log(action, index)
+
+  if (action.text == '复制') {
+    copy(itemData.text)
+    showToast('已复制')
+  }
+  if (action.text == '删除') {
+    $emit('onDelete')
+    showToast('已复制')
+  }
+  showPopover.value = false
+}
+</script>
+
+<style lang="scss" scoped></style>

+ 41 - 0
src/components/Chat/LeftItemMessage.vue

@@ -0,0 +1,41 @@
+<template>
+  <div style="width: calc(100vw - 74px)" class="flex justify-start items-start">
+    <div class="shrink-0 w-40 h-40 border rounded-[4px]">
+      <img
+        v-if="itemData?.haeadImage"
+        class="w-full h-full object-cover"
+        :src="itemData.haeadImage"
+        alt=""
+      />
+      <img v-else class="w-full h-full object-cover" src="~/assets/img/default_avatar.png" alt="" />
+    </div>
+    <template v-if="itemData?.type == 'text'">
+      <div class="inline-block w-0 h-0 mt-10 border-[8px] border-transparent border-r-white"></div>
+      <ChatText item-data="" @on-delete="$emit('onDelete')"></ChatText>
+    </template>
+    <!-- <template v-if="itemData?.type == 'image'"> -->
+    <div class="ml-10 max-w-251 border">
+      <img
+        class="w-full h-full object-contain"
+        src="../../assets/img/comment/H5_default.png"
+        alt=""
+      />
+      <!-- <ChatImage :item-data="_default" @on-delete="$emit('onDelete')" /> -->
+    </div>
+    <!-- </template> -->
+    <template v-if="itemData?.type == 'like'"></template>
+  </div>
+</template>
+
+<script setup>
+defineProps({
+  itemData: {
+    type: Object,
+    default: () => ({})
+  }
+})
+
+defineEmits(['onDelete'])
+</script>
+
+<style lang="scss" scoped></style>

+ 15 - 0
src/components/Chat/MsgStatus.vue

@@ -0,0 +1,15 @@
+<template>
+  <van-icon v-if="status == 'loading'" name="warning" size="16" color="#FF476A" />
+  <div v-else class="text-black-9 text-[10px]">发送失败</div>
+</template>
+<script setup>
+const chatStore = useChatStore()
+const { ws, curConversiton, receive, receiveGetter, isConnect, onNewMessage } =
+  storeToRefs(chatStore)
+const status = ref('loading')
+onMounted(() => {
+  setTimeout(() => {
+    status.value = 'fail'
+  }, 10000)
+})
+</script>

+ 59 - 0
src/components/Chat/RightItemMessage.vue

@@ -0,0 +1,59 @@
+<template>
+  <div style="width: calc(100vw - 74px)" class="flex justify-end items-start mb-16">
+    <!-- 文字消息 -->
+    <template v-if="itemData?.messageType == 0 && itemData.messageContent.trim()">
+      <div
+        class="text-left inline-block min-h-40 max-w-full break-words overflow-auto rounded-[4px] relative bg-white box-border text-base p-5 text-wrap"
+      >
+        {{ messageContentParse(itemData.messageContent) }}
+      </div>
+      <div class="inline-block w-0 h-0 mt-10 border-[8px] border-transparent border-l-white"></div>
+    </template>
+    <!-- 图片消息 -->
+    <template v-if="item.messageType == 1">
+      <div class="ml-10 max-w-251 border right left">
+        <img
+          @click="handleimagePreview"
+          class="w-full h-full object-contain"
+          src="../../assets/img/comment/H5_default.png"
+          alt=""
+        />
+        <!-- <ChatImage :item-data="_default" @on-delete="$emit('onDelete')" /> -->
+      </div>
+    </template>
+
+    <div class="shrink-0 w-40 h-40 rounded-[4px]">
+      <img
+        v-if="itemData?.haeadImage"
+        class="w-full h-full object-cover"
+        :src="itemData.haeadImage"
+        alt=""
+      />
+      <img v-else class="w-full h-full object-cover" src="~/assets/img/default_avatar.png" alt="" />
+    </div>
+  </div>
+</template>
+
+<script setup>
+defineProps({
+  itemData: {
+    type: Object,
+    default: () => ({})
+  }
+})
+
+defineEmits(['onDelete'])
+
+// 图片预览
+const handleimagePreview = () => {
+  showImagePreview({
+    images: [
+      'https://fastly.jsdelivr.net/npm/@vant/assets/apple-1.jpeg',
+      'https://fastly.jsdelivr.net/npm/@vant/assets/apple-2.jpeg'
+    ],
+    startPosition: 1
+  })
+}
+</script>
+
+<style lang="scss" scoped></style>

+ 38 - 0
src/components/Chat/Search.vue

@@ -0,0 +1,38 @@
+<template>
+  <!-- fixed top-60 left-0 -->
+  <div class="w-full pt-62 bg-white px-16">
+    <div class="flex h-40 mb-12 w-full items-center justify-between rounded-full border bg-white">
+      <input
+        type="text"
+        v-model="searchString"
+        class="ml-5 w-[80%] h-full pl-10 text-[13px]"
+        :placeholder="placeholder"
+        @keydown.enter="$emit('search')"
+        style="outline: none; background: none"
+      />
+      <!--  @change="$emit('search')" -->
+      <button
+        @click="$emit('search')"
+        class="h-full border-l-[1px] flex justify-center items-center w-66 shrink-0 rounded-r-full"
+      >
+        <div style="color: white" class="w-16 h-16 shrink-0">
+          <img class="w-full h-full object-cover" src="~/assets/img/visa/search.svg" alt="" />
+        </div>
+      </button>
+    </div>
+  </div>
+</template>
+
+<script setup>
+const searchString = defineModel('searchString')
+defineProps({
+  placeholder: {
+    type: String,
+    default: '搜索'
+  }
+})
+
+defineEmits(['search'])
+</script>
+
+<style lang="scss" scoped></style>

+ 67 - 0
src/components/Chat/Text.vue

@@ -0,0 +1,67 @@
+<template>
+  <!-- 聊天消息内容的文本展示 -->
+
+  <van-popover
+    theme="dark"
+    placement="bottom"
+    v-model:show="showPopover"
+    :actions="actions"
+    actions-direction="horizontal"
+    @select="select"
+  >
+    <template #reference>
+      <div
+        @touchstart="doTouchstart"
+        class="inline-block min-h-40 max-w-full break-words overflow-auto rounded-[4px] relative bg-white box-border text-base p-5 text-wrap"
+      >
+        {{ item }}看哈金沙萨空间的哈师大将扩大解释
+      </div>
+    </template>
+  </van-popover>
+</template>
+
+<script setup>
+import { useClipboard } from '@vueuse/core'
+const { copy, isSupported } = useClipboard()
+
+const showPopover = ref(false)
+
+defineProps({
+  itemData: {
+    type: Object,
+    default: () => ({})
+  }
+})
+
+defineEmits(['onDelete'])
+// const longPressDuration = 1000
+// const pressTimer = ref(null)
+const actions = [
+  // { text: '引用', icon: 'add-o' },
+  { text: '复制', icon: 'add-o' },
+  { text: '删除', icon: 'add-o' },
+  { text: '撤回', icon: 'add-o' }
+]
+
+function doTouchstart() {
+  // pressTimer.value = setTimeout(() => {
+  showPopover.value = true
+  // }, longPressDuration)
+}
+
+function select(action, index) {
+  console.log(action, index)
+
+  if (action.text == '复制') {
+    copy(itemData.text)
+    showToast('已复制')
+  }
+  if (action.text == '删除') {
+    $emit('onDelete')
+    showToast('已复制')
+  }
+  showPopover.value = false
+}
+</script>
+
+<style lang="scss" scoped></style>

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

@@ -0,0 +1,194 @@
+<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">
+            <van-image width="100%" height="100%" fit="cover" :src="img" />
+          </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: 0px 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>

+ 71 - 92
src/components/NavigationBar/LeftMenu.vue

@@ -5,22 +5,13 @@
     </div>
     <div class="px-20">
       <NuxtLink v-if="!token" to="/login" class="flex items-center space-x-15">
-        <div
-          class="flex items-center justify-center bg-[#d9d9d9] rounded-full h-60 w-60"
-        >
-          <span
-            class="iconfont icon-profile text-black-6"
-            style="font-size: 30px"
-          ></span>
+        <div class="flex items-center justify-center bg-[#d9d9d9] rounded-full h-60 w-60">
+          <span class="iconfont icon-profile text-black-6" style="font-size: 30px"></span>
         </div>
         <span class="text-black-6 text-base">登录</span>
       </NuxtLink>
 
-      <div
-        @click="handleToProfile"
-        v-else-if="token"
-        class="flex items-center space-x-15"
-      >
+      <div @click="handleToProfile" v-else-if="token" class="flex items-center space-x-15">
         <van-image
           :src="userInfo.headImageUrl || defaultAvatar"
           height="60"
@@ -34,9 +25,7 @@
           </div>
           <div v-if="userInfo.personalSign" class="text-black-6 text-sm">
             <span class="text-black-6">个性签名:</span>
-            <span class="text-black-3 break-all">{{
-              userInfo.personalSign
-            }}</span>
+            <span class="text-black-3 break-all">{{ userInfo.personalSign }}</span>
           </div>
         </div>
       </div>
@@ -72,154 +61,144 @@
       </div>
     </div>
 
-    <van-button
-      v-if="token"
-      round=""
-      @click="handleLogout"
-      plain
-      style="margin: 20px 20px"
-    >
+    <van-button v-if="token" round="" @click="handleLogout" plain style="margin: 20px 20px">
       退出登录
     </van-button>
   </div>
 </template>
 
 <script setup>
-import menu_travel_home from "~/assets/img/navbar/menu_home.png";
-import menu_car from "@/assets/img/navbar/menu_car.png";
-import menu_create_note from "@/assets/img/navbar/menu_create_note.png";
-import menu_house from "@/assets/img/navbar/menu_house.png";
-import menu_labour from "@/assets/img/navbar/menu_loubar.png";
-import menu_tickets from "@/assets/img/navbar/menu_tickets.png";
-import menu_travel_note from "@/assets/img/navbar/menu_travel_note.png";
-import menu_travel_project from "@/assets/img/navbar/menu_travel_project.png";
-import menu_visa from "@/assets/img/navbar/menu_visa.png";
-import menu_profile from "@/assets/img/navbar/menu_profile.png";
-import defaultAvatar from "~/assets/img/default_avatar.png";
-import LeftMenuItem from "./LeftMenuItem.vue";
+import menu_travel_home from '~/assets/img/navbar/menu_home.png'
+import menu_car from '@/assets/img/navbar/menu_car.png'
+import menu_create_note from '@/assets/img/navbar/menu_create_note.png'
+import menu_house from '@/assets/img/navbar/menu_house.png'
+import menu_labour from '@/assets/img/navbar/menu_loubar.png'
+import menu_tickets from '@/assets/img/navbar/menu_tickets.png'
+import menu_travel_note from '@/assets/img/navbar/menu_travel_note.png'
+import menu_travel_project from '@/assets/img/navbar/menu_travel_project.png'
+import menu_visa from '@/assets/img/navbar/menu_visa.png'
+import menu_profile from '@/assets/img/navbar/menu_profile.png'
+import defaultAvatar from '~/assets/img/default_avatar.png'
+import LeftMenuItem from './LeftMenuItem.vue'
 
-const visible = defineModel("visible");
+const visible = defineModel('visible')
 
-const route = useRoute();
+const route = useRoute()
 
-const authStore = useAuthStore();
-const { token } = storeToRefs(authStore);
+const authStore = useAuthStore()
+const { token } = storeToRefs(authStore)
 
-const userInfoStore = useUserInfoStore();
-const { userInfo } = storeToRefs(userInfoStore);
+const userInfoStore = useUserInfoStore()
+const { userInfo } = storeToRefs(userInfoStore)
 
-const menuData = ref([]);
+const menuData = ref([])
 
 const writeNoteMenu = {
-  title: "写游记",
+  title: '写游记',
   icon: menu_travel_note,
-  to: "/note-create",
-};
+  to: '/note-create'
+}
 
 const fixedMenuData = [
   {
-    title: "首页",
+    title: '首页',
     icon: menu_travel_home,
-    to: "/",
+    to: '/'
   },
   {
-    title: "旅行游记",
+    title: '旅行游记',
     icon: menu_create_note,
-    to: "/travel-notes",
+    to: '/travel-notes'
   },
   {
-    title: "旅行项目",
+    title: '旅行项目',
     icon: menu_travel_project,
-    to: "/travel-projects",
+    to: '/travel-projects'
   },
   {
-    title: "签证居留",
+    title: '签证居留',
     icon: menu_visa,
-    to: "/visa",
+    to: '/visa'
   },
   {
-    title: "全球包车",
+    title: '全球包车',
     icon: menu_car,
-    to: "/car",
+    to: '/car'
   },
   {
-    title: "房屋租售",
+    title: '房屋租售',
     icon: menu_house,
-    to: "/house",
-  },
-];
+    to: '/house'
+  }
+]
 
 const profileMenu = [
   {
-    title: "旅游订单",
-    to: "/profile/travel-orders",
+    title: '旅游订单',
+    to: '/profile/travel-orders'
   },
   {
-    title: "签证订单",
-    to: "/profile/visa-orders",
+    title: '签证订单',
+    to: '/profile/visa-orders'
   },
   {
-    title: "包车订单",
-    to: "/profile/car-orders",
+    title: '包车订单',
+    to: '/profile/car-orders'
   },
   {
-    title: "我的游记",
-    to: "/profile/notes",
+    title: '我的游记',
+    to: '/profile/notes'
   },
   {
-    title: "我的收藏",
-    to: "/profile/collection",
+    title: '我的收藏',
+    to: '/profile/collection'
   },
 
   {
-    title: "我的评论",
-    to: "/profile/my-comment",
-  },
-];
+    title: '我的消息',
+    to: '/profile/my-news'
+  }
+]
 
-const isProfileMenuExpanded = ref(true);
+const isProfileMenuExpanded = ref(true)
 
 watch(
   token,
   (val) => {
     if (!val) {
-      menuData.value = fixedMenuData;
+      menuData.value = fixedMenuData
     } else {
-      userInfoStore.getUserInfo();
+      userInfoStore.getUserInfo()
 
-      menuData.value = [
-        ...fixedMenuData.slice(0, 1),
-        writeNoteMenu,
-        ...fixedMenuData.slice(1),
-      ];
+      menuData.value = [...fixedMenuData.slice(0, 1), writeNoteMenu, ...fixedMenuData.slice(1)]
     }
   },
   {
-    immediate: true,
+    immediate: true
   }
-);
+)
 
 function handleClickMenu(item) {
-  visible.value = false;
+  visible.value = false
   navigateTo({
-    path: item.to,
-  });
+    path: item.to
+  })
 }
 
 function handleToProfile() {
-  visible.value = false;
+  visible.value = false
   navigateTo({
-    path: "/profile",
-  });
+    path: '/profile'
+  })
 }
 
 function handleLogout() {
   try {
-    request("/website/web/doLogout", { method: "post" });
+    request('/website/web/doLogout', { method: 'post' })
   } finally {
-    authStore.cleanToken();
-    navigateTo("/");
-    visible.value = false;
+    authStore.cleanToken()
+    navigateTo('/')
+    visible.value = false
   }
 }
 </script>

+ 54 - 34
src/components/NavigationBar/index.client.vue

@@ -1,26 +1,24 @@
 <template>
-  <div
-    class="h-50 bg-white justify-between w-full fixed z-[999] border-b box-border"
-  >
+  <div class="h-50 bg-white justify-between w-full fixed z-[999] border-b box-border">
+    <audio
+      class="fixed top-0 left-0"
+      ref="audioRef"
+      :src="audioTips"
+      style="opacity: 0; z-index: -1"
+    ></audio>
     <NuxtLink to="/" class="absolute top-1/2 -translate-y-1/2 left-15">
       <img src="~/assets/img/logo.png" class="h-30 object-contain" />
     </NuxtLink>
 
-    <div
-      class="absolute right-15 top-1/2 -translate-y-1/2 flex items-center space-x-20"
-    >
+    <div class="absolute right-15 top-1/2 -translate-y-1/2 flex items-center space-x-20">
       <div v-if="token" class="flex items-center space-x-10">
         <NuxtLink to="/profile" class="flex items-center">
-          <van-image
-            :src="userInfo.headImageUrl || defaultAvatar"
-            round
-            height="26"
-            width="26"
-          />
+          <van-image :src="userInfo.headImageUrl || defaultAvatar" round height="26" width="26" />
         </NuxtLink>
-        <NuxtLink to="/profile" class="max-w-70 truncate text-black-6">
-          我的订单
+        <NuxtLink to="/profile/my-news" class="max-w-70 truncate text-black-6">
+          <van-badge :dot="showDot" :offset="[-4, 5]">消息</van-badge>
         </NuxtLink>
+        <NuxtLink to="/profile" class="max-w-70 truncate text-black-6">订单</NuxtLink>
       </div>
 
       <NuxtLink
@@ -35,18 +33,10 @@
         </div>
         <span class="text-base">请登录</span>
       </NuxtLink>
-      <img
-        @click="handleClickMenu"
-        src="~/assets/img/navbar/nav_menu.png"
-        class="w-24 h-24"
-      />
+      <img @click="handleClickMenu" src="~/assets/img/navbar/nav_menu.png" class="w-24 h-24" />
     </div>
 
-    <van-popup
-      v-model:show="isMenuShow"
-      position="left"
-      :style="{ height: '100%', width: '70%' }"
-    >
+    <van-popup v-model:show="isMenuShow" position="left" :style="{ height: '100%', width: '70%' }">
       <NavigationBarLeftMenu
         v-if="isMenuShow"
         v-model:visible="isMenuShow"
@@ -57,31 +47,61 @@
 </template>
 
 <script setup>
-import defaultAvatar from "~/assets/img/default_avatar.png";
+const chatsStore = useChatsStore()
+const { chatList, chatListLoading } = storeToRefs(chatsStore)
+import { XYWebSocket } from '@/utils/XYWebSocket.ts'
+import defaultAvatar from '~/assets/img/default_avatar.png'
+import audioTips from '@/assets/audio/message.mp3'
+const audioRef = ref(null)
 
-const isMenuShow = ref(false);
+const isMenuShow = ref(false)
 
 function handleClickMenu() {
-  isMenuShow.value = true;
+  isMenuShow.value = true
 }
 
-const route = useRoute();
+const route = useRoute()
 
-const authStore = useAuthStore();
-const { token } = storeToRefs(authStore);
+const authStore = useAuthStore()
+const { token } = storeToRefs(authStore)
 
-const userInfoStore = useUserInfoStore();
-const { userInfo } = storeToRefs(userInfoStore);
+const userInfoStore = useUserInfoStore()
+const { userInfo } = storeToRefs(userInfoStore)
 
 watch(
   token,
   () => {
     if (token.value) {
-      userInfoStore.getUserInfo();
+      userInfoStore.getUserInfo()
     }
   },
   { immediate: true }
-);
+)
+
+// 监听Socket
+onMounted(() => {
+  chatsStore.getChatList()
+  XYWebSocket.SocketEventsBus.on(XYWebSocket.SocketEvents.chatEvent, () => {
+    chatsStore.getChatList()
+    audioRef.value && audioRef.value.play()
+  })
+})
+
+// 卸载Socket
+onUnmounted(() => {
+  // XYWebSocket.SocketEventsBus.off(XYWebSocket.SocketEvents.chatEvent)
+})
+
+// 是否有消息显示红点
+const showDot = computed(() => {
+  let newTatol = chatList.value.reduce((sum, item) => sum + item?.unreadMessageCount, 0)
+
+  if (newTatol) {
+    return true
+  } else {
+    return false
+  }
+})
 </script>
 
 <style lang="scss" scoped></style>

+ 74 - 0
src/components/Profile/InteractionMessage/Eit.vue

@@ -0,0 +1,74 @@
+<template>
+  <div
+    class="mx-12 py-8 mb-20 box-border border-b-[1px] border-dashed flex justify-between itmes-start"
+  >
+    <div class="w-265 shrink-0 box-border flex justify-start itmes-start">
+      <div class="w-32 h-32 shrink-0 rounded-full mr-8 overflow-hidden">
+        <img
+          v-if="itemData?.headImage"
+          class="w-full h-full object-cover"
+          :src="itemData?.headImage"
+          alt=""
+        />
+        <img
+          v-else
+          class="w-full h-full object-cover"
+          src="https://www.xiaoyaotravel.com/_nuxt/default_avatar.gSq5JxK1.png"
+          alt=""
+        />
+      </div>
+
+      <div class="w-[85%]">
+        <div class="-mt-5 w-full text-sm">
+          <span class="text-sm text-black-6 pr-4">{{ itemData?.showName }}</span>
+
+          <div
+            v-if="itemData?.state == 2"
+            :class="`inline-block box-border px-6 text-sm text-[#3369E7] bg-[#3369E7]/[0.08]`"
+          >
+            互关
+          </div>
+          <div
+            v-if="itemData?.state == 4"
+            :class="`inline-block box-border px-6 text-sm text-[#FF9300] bg-[#FF9300]/[0.08]`"
+          >
+            粉丝
+          </div>
+        </div>
+        <h1 class="w-full my-6 font-semibold line-clamp-2 text-black-3 text-base">
+          {{ itemData.messageConten }}
+        </h1>
+        <p class="text-black-9 text-sm">{{ itemData.creatTime }}</p>
+      </div>
+    </div>
+
+    <NuxtLink
+      itemData
+      :to="itemData.businessType == 1 ? `/yj/${itemData.businessId}` : '/'"
+      class="block w-71 -mt-5 ml-17 h-47 shrink-0 rounded-[4px] overflow-hidden"
+    >
+      <img
+        v-if="itemData?.imageUrl"
+        class="w-full h-full object-cover"
+        :src="itemData?.imageUrl"
+        alt=""
+      />
+      <img
+        v-else
+        class="w-full h-full object-cover"
+        src="~/assets/img/comment/H5_default.png"
+        alt=""
+      />
+    </NuxtLink>
+  </div>
+</template>
+<script setup>
+defineProps({
+  itemData: {
+    type: Object,
+    default: () => ({})
+  }
+})
+</script>
+
+<style lang="scss" scoped></style>

+ 73 - 0
src/components/Profile/InteractionMessage/LikesandFavorites.vue

@@ -0,0 +1,73 @@
+<template>
+  <div
+    class="mx-12 py-8 mb-20 box-border border-b-[1px] border-dashed flex justify-between itmes-start"
+  >
+    <div class="w-265 shrink-0 box-border flex justify-start itmes-start">
+      <div class="w-32 h-32 shrink-0 rounded-full mr-8 overflow-hidden">
+        <img
+          v-if="itemData?.headImageUrl"
+          class="w-full h-full object-cover"
+          :src="itemData?.headImageUrl"
+          alt=""
+        />
+        <img
+          v-else
+          class="w-full h-full object-cover"
+          src="https://www.xiaoyaotravel.com/_nuxt/default_avatar.gSq5JxK1.png"
+          alt=""
+        />
+      </div>
+
+      <div class="w-[85%]">
+        <div class="-mt-5 w-full text-sm">
+          <span class="text-sm text-black-6 pr-4">{{ itemData.showName }}</span>
+          <div
+            v-if="itemData.state == 4"
+            :class="`inline-block box-border px-6 text-sm text-[#FF9300] bg-[#FF9300]/[0.08]`"
+          >
+            粉丝
+          </div>
+          <div
+            v-if="itemData.state == 2"
+            :class="`inline-block box-border px-6 text-sm text-[#3369E7] bg-[#3369E7]/[0.08]`"
+          >
+            互关
+          </div>
+        </div>
+        <h1 class="font-semibold text-black-3 text-base">
+          {{ itemData.messageContent }}
+        </h1>
+        <p class="text-black-9 text-sm">{{ itemData.creatTime }}</p>
+      </div>
+    </div>
+
+    <NuxtLink
+      itemData
+      :to="itemData.businessType == 1 ? `/yj/${itemData.businessId}` : '/'"
+      class="block w-71 ml-17 h-47 shrink-0 rounded-[4px] overflow-hidden"
+    >
+      <img
+        v-if="itemData?.imageUrl"
+        class="w-full h-full object-cover"
+        :src="itemData?.imageUrl"
+        alt=""
+      />
+      <img
+        v-else
+        class="w-full h-full object-cover"
+        src="~/assets/img/comment/H5_default.png"
+        alt=""
+      />
+    </NuxtLink>
+  </div>
+</template>
+<script setup>
+defineProps({
+  itemData: {
+    type: Object,
+    default: () => ({})
+  }
+})
+</script>
+
+<style lang="scss" scoped></style>

+ 5 - 0
src/components/Profile/InteractionMessage/MyComment.vue

@@ -0,0 +1,5 @@
+<template>
+  <div>我的评论</div>
+</template>
+<script setup></script>
+<style lang="scss" scoped></style>

+ 156 - 0
src/components/Profile/InteractionMessage/ReceiveComment.vue

@@ -0,0 +1,156 @@
+<template>
+  <div
+    style="scrollbar-width: none"
+    class="mx-12 py-8 mb-20  box-border border-b-[1px] border-dashed flex justify-between items-start"
+  >
+
+    <div class="flex shrink-0 w-[74%]  justify-start items-start">
+      <div class="w-32 h-32 rounded-full shrink-0">
+        <img
+          v-if="itemData?.headImage"
+          class="w-full h-full object-cover rounded-full"
+          :src="itemData?.headImage"
+          alt=""
+        />
+        <img
+          v-else
+          class="w-full h-full object-cover rounded-full"
+          src="https://www.xiaoyaotravel.com/_nuxt/default_avatar.gSq5JxK1.png"
+          alt=""
+        />
+      </div>
+
+      <div class="-mt-5 ml-8 w-[85%] w-full text-sm">
+        <NuxtLink :to="goToPage(itemData)">
+          <p class="w-full line-clamp-1 ">
+            <span class="text-black-6  text-sm  pr-4">
+              {{ itemData?.showName }}
+            </span>
+
+            <div
+              :class="`inline-block text-sm px-6 py-3 text-[${bgColor(itemData?.state)}] bg-[${bgColor(itemData?.state)}]/[0.08]`"
+            >
+              {{ state(itemData?.state) }}
+            </div>
+          </p>
+
+          <p
+            :class="`w-full  line-clamp-2 my-6 text-black-3 text-base leading-4xl`"
+          >
+            <span v-if="itemData?.noticeType== 12">
+              回复{{
+                itemData?.showName != userInfo.showName
+                  ? `@${itemData?.showName}`
+                  : ''
+              }}:
+            </span>
+            <span v-if="itemData?.noticeType==6">评论:</span>
+
+            <span v-html="coveredContent( itemData?.messageContent)"></span>
+          </p>
+          <p class="w-full mb-6 text-black-9 leading-xl">{{ itemData?.createTime }}</p>
+        </NuxtLink>
+     
+        <div
+         v-if="itemData?.noticeType== 12"
+         @click="itemData.noticeType == 12 ? navigateTo({
+          path:`/yj/${itemData.businessId}` ,
+         }) :()=>{}"
+          class="box-border mb-12 line-clamp-1 rounded-full w-95 h-26 flex justify-center items-center text-base px-8 text-black-3 leading-4xl bg-[#F5F5F5] active:bg-black/[0.1]"
+        >
+          <div class="w-16 h-16 rounded-full shrink-0 mr-4">
+            <img
+              v-if="userInfo?.headImageUrl"
+              class="w-full h-full object-cover rounded-full"
+              :src="userInfo?.headImageUrl"
+              alt=""
+            />
+            <img
+              v-else
+              class="w-full h-full object-cover rounded-full"
+              src="https://www.xiaoyaotravel.com/_nuxt/default_avatar.gSq5JxK1.png"
+              alt=""
+            />
+          </div>
+          回复评论
+        </div>
+      </div>
+    </div>
+
+    <NuxtLink
+      itemData
+      :to="itemData.businessType == 1 ? `/yj/${itemData.businessId}` : '/'"
+      class="block w-71 ml-17 h-47 shrink-0 rounded-[4px] overflow-hidden"
+    >
+      <img
+        v-if="itemData?.imageUrl"
+        class="w-full h-full object-cover"
+        :src="itemData?.imageUrl"
+        alt=""
+      />
+      <img v-else class="w-full h-full object-cover" :src="defaultImg" alt="" />
+    </NuxtLink>
+  </div>
+</template>
+<script setup>
+import defaultImg from '~/assets/img/comment/H5_default.png'
+import emojiJson from '@/pages/yj/emoji.json'
+
+defineEmits(['onAddReply'])
+// 转换评论中的一些非字符emoji
+function coveredContent(val) {
+  if (!val) return ''
+  return val.replace(/\[.*?]/g, function (str) {
+    const baseApi = import.meta.env.VITE_APP_EMOJI_API
+    const emojiName = emojiJson[str]
+    if (!emojiName) return str
+    return `<img src=${baseApi}${emojiJson[str]} style="width: 20px; height: 20px;display: inline-block; vertical-align: middle"/>`
+  })
+}
+
+defineProps({
+  itemData: {
+    type: Object,
+    default: () => ({})
+  },
+  userInfo: {
+    type: Object,
+    default: () => ({})
+  }
+})
+
+// 跳转到那个页面
+const goToPage = (item) => {
+  switch (item?.businessType) {
+    case 1:
+      return `/yj/${item?.businessId}`
+    default:
+      return '/'
+  }
+}
+
+const state = (stateNum) => {
+  switch (stateNum) {
+    case 4:
+      return '粉丝'
+    case 2:
+      return '互关'
+    default:
+      return ''
+  }
+}
+
+// 背景色 和文字颜色
+const bgColor = (color) => {
+  if (color == 2) {
+    return '#3369E7'
+  }
+  if (color == 4) {
+    return '#FF9300'
+  }
+}
+
+
+
+</script>
+<style lang="scss" scoped></style>

+ 69 - 0
src/components/Profile/InteractionMessage/SendComment.vue

@@ -0,0 +1,69 @@
+<template>
+  <div
+    class="mx-12 py-8 mb-20 box-border border-b-[1px] border-dashed flex justify-between itmes-start"
+  >
+    <div class="w-265 shrink-0 box-border flex justify-start itmes-start">
+      <div class="w-32 h-32 shrink-0 rounded-full mr-8 overflow-hidden">
+        <img
+          v-if="userInfo?.headImageUrl"
+          class="w-full h-full object-cover"
+          :src="userInfo?.headImageUrl"
+          alt=""
+        />
+        <img
+          v-else
+          class="w-full h-full object-cover"
+          src="https://www.xiaoyaotravel.com/_nuxt/default_avatar.gSq5JxK1.png"
+          alt=""
+        />
+      </div>
+
+      <div class="w-223">
+        <div class="-mt-5 line-clamp-1 text-sm text-black-6 pr-4">
+          {{ userInfo?.showName }}
+        </div>
+        <h1
+          v-if="itemData.messageConten"
+          class="w-full font-semibold line-clamp-2 text-black-3 text-base"
+        >
+          <template v-if="itemData.noticeType == 12">回复:</template>
+          <template v-if="itemData.noticeType == 6">评论:</template>
+          {{ itemData.messageConten }}
+        </h1>
+        <p class="text-black-9 text-sm">{{ itemData.creatTime }}</p>
+      </div>
+    </div>
+    <NuxtLink
+      itemData
+      :to="itemData.businessType == 1 ? `/yj/${itemData.businessId}` : '/'"
+      class="block w-71 ml-17 h-47 shrink-0 rounded-[4px] overflow-hidden"
+    >
+      <img
+        v-if="itemData?.imageUrl"
+        class="w-full h-full object-cover"
+        :src="itemData?.imageUrl"
+        alt=""
+      />
+      <img
+        v-else
+        class="w-full h-full object-cover"
+        src="~/assets/img/comment/H5_default.png"
+        alt=""
+      />
+    </NuxtLink>
+  </div>
+</template>
+<script setup>
+defineProps({
+  itemData: {
+    type: Object,
+    default: () => ({})
+  },
+  userInfo: {
+    type: Object,
+    default: () => ({})
+  }
+})
+</script>
+
+<style lang="scss" scoped></style>

+ 362 - 0
src/components/Profile/News/ChatInput.vue

@@ -0,0 +1,362 @@
+<template>
+  <div @click.stop="" class="fixed bottom-0 left-0 w-full bg-[#fff] pt-10 pb-30 z-52">
+    <div :class="`flex ${inputValue.length > 13 ? 'items-end' : 'items-center'} px-15 pb-10`">
+      <!--   @click="showVoice = !showVoice" -->
+      <span
+        v-if="!showVoice"
+        :class="`iconfont icon-voice-one text-black-6 `"
+        style="font-size: 32px"
+      ></span>
+      <van-icon @click="showVoice = !showVoice" v-else name="volume" size="22" />
+
+      <div
+        v-if="showVoice"
+        @mousedown="startRecording"
+        @mouseup="stopRecording"
+        @touchstart="startRecording"
+        @touchend="stopRecording"
+        :class="`relative rounded-full bg-white justify-center  flex-1 ml-12 mr-12 pl-5 pr-5 pt-4 pb-4  h-40 border flex items-center `"
+      >
+        {{ isRecording ? '松开结束' : '按住说话' }}
+      </div>
+      <div
+        v-else
+        style="overflow-y: scroll; -webkit-scrollbar-width: 0px; -webkit-scrollbar: none"
+        class="box-border rounded-full bg-[#F3F3F3] justify-between px-16 py-8 flex-1 mx-12 min-h-40 border flex items-center"
+      >
+        <textarea
+          v-model="inputValue"
+          ref="textareaRef"
+          placeholder="请输入"
+          @focus="textareaFocus"
+          @blur="handleBlur"
+          class="ml-8 flex-1 box-border w-full h-full bg-[#F3F3F3]"
+          maxlength="5000"
+          style="height: 100%; resize: none; border: none; outline: none"
+        ></textarea>
+        <!-- @keydown.enter="addComment" -->
+      </div>
+      <span
+        @click="openEmoji"
+        class="iconfont icon-slightly-smiling-face text-black-6 mr-12"
+        style="font-size: 32px"
+      ></span>
+      <span
+        @click="openOther"
+        class="iconfont icon-close-one text-black-6"
+        style="font-size: 32px"
+      ></span>
+    </div>
+
+    <div v-if="showEmoji" class="w-full h-300 bg-[#fff] overflow-auto">
+      <div @click="closeEmojiBox" class="flex justify-end pr-15 text-black-9 text-sm">收起表情</div>
+      <div class="flex items-center flex-wrap w-full px-8">
+        <div
+          v-for="(item, index) in emojiJson"
+          :key="index"
+          class="active:bg-[#ddd] text-4xl w-[10%] aspect-[1/1] flex items-center justify-center"
+        >
+          <div @click="selectEmoji(index)" v-html="item.emoji"></div>
+        </div>
+      </div>
+      <div class="fixed bottom-45 right-20 flex">
+        <div
+          @click="delteMessage"
+          class="text-sm py-5 px-16 mr-12 bg-[#FD9A00] text-[#fff] flex items-center justify-center rounded-full shrink-0"
+        >
+          删除
+        </div>
+        <div
+          @click="addComment"
+          class="text-sm py-5 px-16 mr-12 bg-[#FD9A00] text-[#fff] flex items-center justify-center rounded-full shrink-0"
+        >
+          发送
+        </div>
+      </div>
+    </div>
+    <div v-if="showOther" class="w-full h-200 bg-[#fff] overflow-auto">
+      <div class="flex items-start flex-wrap w-full h-full py-15 px-8 bg-white">
+        <template v-for="(item, index) in otherList" :key="index">
+          <div
+            @click="index == 0 ? () => {} : item.fn"
+            v-if="item?.isShow"
+            class="mx-10 text-4xl relative active:text-[#FF9300] w-[15%] aspect-[1/1] flex flex-wrap items-center justify-center"
+          >
+            <div
+              class="w-54 h-54 active:bg-[#FF9300]/[0.1] bg-[#F3F3F3] shrink-0 rounded-full mb-5 overflow-auto flex justify-center items-center"
+            >
+              <span
+                :class="`iconfont ${item.icon} text-black-6 active:text-[#FF9300] `"
+                style="font-size: 32px"
+              ></span>
+            </div>
+            <p class="text-sm w-full text-center">{{ item.title }}</p>
+            <input
+              type="file"
+              @change="item.fn"
+              class="absolute top-0 left-0 w-full h-full"
+              style="position: absolute; top: 0; left: 0; z-index: 10; opacity: 0"
+            />
+          </div>
+        </template>
+      </div>
+    </div>
+  </div>
+</template>
+<script setup>
+import emojiJson from './emoji.js'
+
+const shareGroup = defineModel('shareGroup')
+
+const emit = defineEmits(['onSendMessage', 'onSelectImg'])
+const textareaRef = ref(null)
+
+// 显示输入框
+// const showInput = ref(true)
+// 是否展示表情
+const showEmoji = ref(false)
+// 是否展示其他功能
+const showOther = ref(false)
+
+// 是否展示语音
+const showVoice = ref(false)
+
+// 是否在录音
+const isRecording = ref(false)
+const transcript = ref('') // 存储语音识别结果
+// 输入的内容
+const inputValue = ref('')
+
+const addContent = ref(true)
+
+// 记录光标位置
+const cursorIndex = ref(0)
+
+// 创建语音识别实例
+let recognition = null
+// if ('SpeechRecognition' in window || 'webkitSpeechRecognition' in window) {
+//   recognition = new (window.SpeechRecognition || window.webkitSpeechRecognition)()
+//   recognition.continuous = true // 连续识别
+//   recognition.interimResults = true // 显示中间结果
+//   recognition.lang = 'zh-CN' // 设置识别语言(中文)
+
+//   // 监听识别结果
+//   recognition.onresult = (event) => {
+//     const lastResult = event.results[event.resultIndex]
+//     if (lastResult.isFinal) {
+//       transcript.value = lastResult[0].transcript // 获取最终结果
+//     } else {
+//       transcript.value = lastResult[0].transcript // 获取中间结果
+//     }
+//   }
+
+//   // 错误处理
+//   recognition.onerror = (event) => {
+//     console.error('语音识别出错:', event.error)
+//     isRecording.value = false
+//   }
+
+//   // 结束时重置状态
+//   recognition.onend = () => {
+//     isRecording.value = false
+//   }
+// }
+
+// 启动录音
+const startRecording = () => {
+  if (recognition) {
+    // recognition?.start()
+    isRecording.value = true
+  }
+}
+
+// 停止录音
+const stopRecording = () => {
+  if (recognition) {
+    // recognition?.stop()
+    isRecording.value = false
+  }
+}
+
+const { open, onChange } = useFileDialog({
+  accept: '.png,.png,.jpeg,.JPG,Png '
+})
+
+// 上传相册
+const uploadPictures = () => {
+  open()
+}
+
+onChange(async (files) => {
+  if (!files.length) return
+  console.log(files[0])
+  const formData = new FormData()
+  formData.append('uploadFile', files[0])
+  formData.append('asImage', true)
+  formData.append('fieldName', 'messageContent')
+  const maxSize = 10 * 1024 * 1024 // 10 MB
+
+  if (files[0].size > maxSize) {
+    showToast('上传图片过大,请重新上传')
+    return
+  } else {
+    try {
+      showLoadingToast({
+        message: '图片上传中...',
+        duration: 1000000
+      })
+
+      const { data } = await request('/website/tourMessage/upload', {
+        method: 'post',
+        body: formData
+      })
+      // form.image.push(data.fileUrl)
+      closeToast()
+      showToast('图片上传成功')
+
+      emit('onSelectImg', data.fileUrl)
+    } catch (error) {
+      // form.image.push({
+      //   url: files[0].name,
+      //   status: 'failed',
+      //   isImage: true,
+      //   message: '上传失败',
+      //   imageFit: 'contain'
+      // })
+      closeToast()
+      showToast('图片上传失败')
+
+      console.log('图片上传失败')
+    }
+  }
+})
+
+// 分享群聊
+const shareGroupChat = () => {
+  console.log('分享群聊')
+}
+
+// 按住说话 松开结束
+const changeShowVoiceing = () => {}
+
+const otherList = reactive([
+  {
+    title: '相册',
+    icon: 'icon-pic',
+    isShow: true,
+    // fn: emit('onSelectImg')
+    fn: uploadPictures
+  },
+  {
+    title: '分享群聊',
+    icon: 'icon-peoples-two',
+    isShow: shareGroup.value,
+    fn: shareGroupChat
+  }
+])
+
+// 转换评论中的一些非字符emoji
+// function coveredContent(val) {
+//   if (!val) return ''
+//   return val.replace(/\[.*?]/g, function (str) {
+//     const baseApi = import.meta.env.VITE_APP_EMOJI_API
+//     const emojiName = emojiJson[str]
+//     console.log(emojiName, 'emojiName')
+
+//     if (!emojiName) return str
+
+//     return `<img src=${baseApi}${emojiJson[str]} style="width: 20px; height: 20px;display: inline-block; vertical-align: middle"/>`
+//   })
+// }
+
+function addComment() {
+  if (!inputValue.value.trim()) {
+    showToast('发送内容不能为空!')
+    return
+  }
+  emit('onSendMessage', inputValue.value)
+  inputValue.value = ''
+}
+
+// 监听输入框的enter事件
+function addEventListenerTextarea() {
+  nextTick(() => {
+    if (textareaRef.value) {
+      textareaRef.value.removeEventListener('keydown', () => {})
+      textareaRef.value.addEventListener('keydown', function (event) {
+        if (event.key === 'Enter') {
+          event.preventDefault()
+          emit('onSendMessage', inputValue.value)
+          inputValue.value = ''
+        }
+      })
+    }
+  })
+}
+
+// 获取焦点
+function textareaFocus() {
+  showEmoji.value = false
+  // showInput.value = true
+  textareaRef.value?.focus()
+}
+
+// 文本域失焦
+function handleBlur() {
+  showEmoji.value = false
+  showOther.value = false
+  textareaRef.value?.blur()
+}
+
+// 打开表情
+function openEmoji() {
+  showOther.value = false
+  nextTick(() => {
+    textareaRef.value.selectionStart && (cursorIndex.value = textareaRef.value.selectionStart)
+    showEmoji.value = true
+  })
+}
+
+// 打开上传图片或者其他的
+function openOther() {
+  showEmoji.value = false
+  showOther.value = true
+}
+
+// 收起表情
+function closeEmojiBox() {
+  showEmoji.value = false
+  nextTick(() => {
+    textareaRef.value.focus()
+  })
+}
+
+// 选择表情
+function selectEmoji(emojiStr = '') {
+  const length = emojiStr.length
+  inputValue.value =
+    inputValue.value.slice(0, cursorIndex.value) +
+    emojiJson[emojiStr].emoji +
+    inputValue.value.slice(cursorIndex.value)
+
+  nextTick(() => {
+    cursorIndex.value += length
+    textareaRef.value.setSelectionRange(cursorIndex.value, cursorIndex.value)
+  })
+}
+
+// 删除内容
+const delteMessage = () => {
+  inputValue.value = inputValue.value.slice(1, inputValue.value.length)
+}
+
+watch(() => {
+  addEventListenerTextarea()
+})
+</script>
+
+<style lang="scss" scoped>
+.no-scrollbar::-webkit-scrollbar {
+  width: 0;
+}
+</style>

+ 99 - 0
src/components/Profile/News/GroupChat.vue

@@ -0,0 +1,99 @@
+<template>
+  <van-swipe-cell>
+    <div
+      @click="$emit('onChatPage')"
+      class="w-full relative h-82 flex justify-start items-center px-16"
+    >
+      <van-badge
+        v-if="itemData?.unreadMessageCount > 0"
+        v-bind="messageNumber(itemData?.unreadMessageCount)"
+        max="99"
+      >
+        <MultiHeader :size="48" :imgUrls="itemData?.dfGroupImage" />
+      </van-badge>
+      <MultiHeader v-else :size="48" :imgUrls="itemData?.dfGroupImage" />
+
+      <div class="h-48 w-245 ml-12 flex flex-wrap">
+        <h1 class="line-clamp-1 mb-8 w-full text-xl text-black-3 font-semibold">
+          {{ itemData?.groupRemark }}
+        </h1>
+        <p class="line-clamp-1 w-full h-20 text-base text-black/[0.6] leading-3xl">
+          {{ messageContentParse(itemData?.lastMessage?.messageContent) }}
+          <!-- {{ itemData?.lastMessage ? itemData?.lastMessage?.messageContent.messageContent : '' }} -->
+        </p>
+      </div>
+
+      <div class="w-35 h-48 shrink-0">
+        <p class="text-black/[0.6] mb-12 text-sm text-end">{{ itemData?.updateTime }}</p>
+
+        <div v-if="itemData?.isNotDisturb == 1" class="w-full shrink-0 flex justify-end">
+          <span class="iconfont icon-close-remind text-black-6" style="font-size: 16px"></span>
+        </div>
+      </div>
+      <div class="absolute bottom-0 right-0 w-[96%] h-1 border-b-[1px]"></div>
+    </div>
+    <template #right>
+      <div class="pl-2 h-full">
+        <van-button
+          style="height: 100%"
+          square
+          :text="itemData?.isNotDisturb == 1 ? '取消免打扰' : '设为免打扰'"
+          @click="$emit('onNoBother')"
+          type="danger"
+          color="#FF9300"
+          class="delete-button"
+        />
+        <van-button
+          style="height: 100%"
+          square
+          :text="itemData?.isTop == 1 ? '取消置顶' : '置顶聊天'"
+          @click="$emit('onIsTop')"
+          type="danger"
+          color="#E37318"
+          class="delete-button"
+        />
+        <van-button
+          style="height: 100%"
+          square
+          :text="itemData?.isShow == 1 ? '不显示聊天' : '显示聊天'"
+          @click="$emit('onConvDelete')"
+          type="danger"
+          color="#D54941"
+          class="delete-button"
+        />
+      </div>
+    </template>
+  </van-swipe-cell>
+</template>
+
+<script setup>
+import { messageContentParse } from '~/utils/detalTime.js'
+const props = defineProps({
+  itemData: {
+    type: Object,
+    default: () => ({})
+  }
+})
+defineEmits(['onNoBother', 'onIsTop', 'onConvDelete', 'onChatPage'])
+console.log(props.itemData.dfGroupImage, 'itemData')
+// 消息数量通知的展示  需要动态的展示
+const messageNumber = (content) => {
+  let messageNumberObj = {}
+  if (content <= 1) {
+    messageNumberObj = {
+      offset: [-5, 4],
+      dot: true,
+      content
+    }
+  }
+  if (content > 1) {
+    messageNumberObj = {
+      offset: [-10, 7],
+      content
+    }
+  }
+  return messageNumberObj
+}
+</script>
+
+<style lang="scss" scoped></style>

+ 137 - 0
src/components/Profile/News/SingleChat.vue

@@ -0,0 +1,137 @@
+<template>
+  <van-swipe-cell>
+    <div
+      @click="$emit('onChatPage')"
+      class="w-full h-82 relative flex justify-start items-center px-16 mb-20"
+    >
+      <div class="w-48 h-48">
+        <van-badge
+          v-if="itemData?.unreadMessageCount > 0"
+          v-bind="messageNumber(itemData?.unreadMessageCount)"
+          max="99"
+        >
+          <div
+            v-if="itemData?.headImage"
+            class="w-48 h-48 rounded-full overflow-hidden flex justify-center items-center"
+          >
+            <img class="w-full h-full shrink-0 object-cover" :src="itemData?.headImage" alt="" />
+          </div>
+          <div
+            v-else
+            class="w-48 h-48 rounded-full overflow-hidden flex justify-center items-center"
+          >
+            <img
+              class="w-24 h-24 shrink-0 object-cover"
+              src="~/assets/img/default_avatar.png"
+              alt=""
+            />
+          </div>
+        </van-badge>
+
+        <template v-else>
+          <div
+            v-if="itemData?.headImage"
+            class="w-48 h-48 rounded-full overflow-hidden flex justify-center items-center"
+          >
+            <img class="w-full h-full shrink-0 object-cover" :src="itemData?.headImage" alt="" />
+          </div>
+          <div
+            v-else
+            class="w-48 h-48 rounded-full overflow-hidden flex justify-center items-center"
+          >
+            <img
+              class="w-full h-full shrink-0 object-cover"
+              src="~/assets/img/default_avatar.png"
+              alt=""
+            />
+          </div>
+        </template>
+      </div>
+
+      <div class="h-48 w-245 ml-12 flex flex-wrap">
+        <h1 class="line-clamp-1 mb-8 w-full text-xl text-black-3 font-semibold">
+          {{ itemData?.groupRemark }}
+        </h1>
+        <p class="line-clamp-1 w-full h-20 text-base text-black/[0.6] leading-3xl">
+          {{ messageContentParse(itemData?.lastMessage?.messageContent) }}
+          <!-- {{ itemData?.lastMessage ? itemData?.lastMessage?.messageContent.messageContent : '' }} -->
+        </p>
+      </div>
+
+      <div class="w-35 h-48 shrink-0">
+        <p class="text-black/[0.6] text-sm text-end">{{ itemData?.updateTime }}</p>
+
+        <div v-if="itemData?.isNotDisturb == 1" class="w-full h-16 shrink-0 mt-12 flex justify-end">
+          <span class="iconfont icon-close-remind text-black-6" style="font-size: 16px"></span>
+        </div>
+      </div>
+      <div class="absolute bottom-0 right-0 w-[96%] h-1 border-b-[1px]"></div>
+    </div>
+
+    <template #right>
+      <div class="pl-2 h-full">
+        <van-button
+          style="height: 100%"
+          square
+          :text="itemData?.isNotDisturb == 1 ? '取消免打扰' : '设为免打扰'"
+          @click="$emit('onNoBother')"
+          type="danger"
+          color="#FF9300"
+          class="delete-button h-full"
+        />
+        <van-button
+          style="height: 100%"
+          square
+          :text="itemData?.isTop == 1 ? '取消置顶' : '置顶聊天'"
+          @click="$emit('onIsTop')"
+          type="danger"
+          color="#FF9300"
+          class="delete-button"
+        />
+
+        <van-button
+          style="height: 100%"
+          square
+          :text="itemData?.isShow == 1 ? '不显示聊天' : '显示聊天'"
+          @click="$emit('onConvDelete')"
+          type="danger"
+          color="#D54941"
+          class="delete-button"
+        />
+      </div>
+    </template>
+  </van-swipe-cell>
+</template>
+
+<script setup>
+import { messageContentParse } from '~/utils/detalTime.js'
+
+defineProps({
+  itemData: {
+    type: Object,
+    default: () => ({})
+  }
+})
+defineEmits(['onNoBother', 'onIsTop', 'onConvDelete', 'onChatPage'])
+
+// 消息数量通知的展示  需要动态的展示
+const messageNumber = (content) => {
+  let messageNumberObj = {}
+  if (content <= 1) {
+    messageNumberObj = {
+      offset: [-5, 4],
+      dot: true,
+      content
+    }
+  }
+  if (content > 1) {
+    messageNumberObj = {
+      offset: [-10, 7],
+      content
+    }
+  }
+  return messageNumberObj
+}
+</script>
+
+<style lang="scss" scoped></style>

+ 108 - 0
src/components/Profile/News/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: '👻' }
+]

+ 101 - 0
src/composables/useScanCode.js

@@ -0,0 +1,101 @@
+import {Html5Qrcode, Html5QrcodeScanner, Html5QrcodeScanType, Html5QrcodeSupportedFormats} from "html5-qrcode";
+
+export const useScanCode = (elementId) => {
+    const router = useRouter()
+
+    let scanInstance = null; // 扫码实例
+    let results = ref(null); // 扫码结果
+
+    const initScanInstance = () => {
+        if(!elementId) throw error('请传入放置扫码功能的元素ID')
+        if (!scanInstance) {
+            // reader放置扫码功能的元素ID
+            scanInstance = new Html5Qrcode(elementId, {
+                formatsToSupport: [
+                    Html5QrcodeSupportedFormats.QR_CODE,
+                ],
+            })
+        }
+    }
+    const openQrcode = async () => {
+        Html5Qrcode.getCameras()
+            .then(devices => {
+                if (devices && devices.length) {// 当前环境下能识别出摄像头,并且摄像头的数据可能不止一个
+                    initScanInstance()
+
+                    // isShow.value = true
+                    scanInstance.start(
+                        {facingMode: "environment"},
+                        {
+                            focusMode: 'continuous',
+                            fps: 1, // 可选,每n秒帧扫描一次
+                            qrbox: { // 扫描的UI框
+                                width: 250,
+                                height: 250
+                            },
+                            videoConstraints: {
+                                // width: 375,
+                                // height: (window.visualViewport.height - 50),
+                                aspectRatio: window.visualViewport.height / window.visualViewport.width,
+                                facingMode: "environment",
+                            }
+                        },
+                        (decodedText, decodedResult) => {
+                            showToast('识别成功')
+                            // 扫描结果
+                            results.value = {
+                                decodedText,
+                                decodedResult
+                            }
+                            // scanInstance.stop()
+                        },
+                        (errorMessage, error) => {
+                            closeQrcode(errorMessage)
+                        }
+                    )
+                }
+            })
+            .catch((err) => {
+                // 错误信息处理仅供参考,具体情况看输出!!!
+                let errorStr = ''
+                if (typeof err === "string") {
+                    errorStr = err
+                } else {
+                    if (err.name === "NotAllowedError")
+                        errorStr = "您需要授予相机访问权限"
+                    if (err.name === "NotFoundError") {
+                        errorStr = "未检测到摄像头"
+                    }
+                    if (err.name === "NotSupportedError")
+                        errorStr = "摄像头访问只支持在安全的上下文中,如https或localhost"
+                    if (err.name === "NotReadableError") errorStr = "摄像头被占用"
+                    if (err.name === "OverconstrainedError")
+                        errorStr = "安装摄像头不合适"
+                    if (err.name === "StreamApiNotSupportedError")
+                        errorStr = "此浏览器不支持流API"
+                }
+                showToast(errorStr)
+                closeQrcode()
+            })
+    }
+
+    const closeQrcode = () => {
+        // if (scanInstance) scanInstance.stop()
+        console.log(router, 'router')
+        router.back()
+    }
+
+
+    onMounted(() => {
+        initScanInstance()
+        openQrcode()
+    })
+    onUnmounted(() => {
+        if (scanInstance) scanInstance.stop()
+    })
+
+    return {
+        results,
+        closeQrcode
+    }
+}

+ 11 - 0
src/layouts/scan.vue

@@ -0,0 +1,11 @@
+<template>
+  <div class="w-full h-full">
+    <slot />
+  </div>
+</template>
+<script setup lang="ts">
+
+</script>
+<style scoped lang="scss">
+
+</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,})
+    }
+})

+ 48 - 0
src/middleware/02.auth.global.js

@@ -0,0 +1,48 @@
+import {useChatsStore} from "~/stores/useChats";
+
+
+export default defineNuxtRouteMiddleware((to, from) => {
+  if (import.meta.server) return;
+
+  const authStore = useAuthStore();
+  const { token } = storeToRefs(authStore);
+  const userInfoStore = useUserInfoStore();
+  const {userInfo} = storeToRefs(userInfoStore);
+
+  const chatsStore = useChatsStore()
+
+  if (token.value) {
+    const config = useRuntimeConfig()
+    const baseIM = config.public.baseIM
+    // 俏的
+/*
+    const chatStore = useChatStore()
+    chatStore.createConnection(baseIM + '?token=' + userInfo.value.pass)
+*/
+
+     userInfoStore.getUserInfo().then(() => {
+      // chatsStore.initWebSocket(baseIM + '?token=' + userInfo.value.pass)
+      chatsStore.initWebSocket(baseIM + '?token=' + userInfo.value.pass)
+    })
+    return
+  }
+
+  if (to.fullPath.includes("/profile")) {
+    return navigateTo("/login", {
+      replace: true,
+      query: {
+        redirect: to.fullPath,
+      },
+    });
+  }
+
+  if (to.fullPath.includes("/note-create")) {
+    return navigateTo("/login", {
+      replace: true,
+      query: {
+        redirect: to.fullPath,
+      },
+    });
+  }
+  return;
+});

+ 0 - 27
src/middleware/auth.global.js

@@ -1,27 +0,0 @@
-export default defineNuxtRouteMiddleware((to, from) => {
-  if (import.meta.server) return;
-
-  const authStore = useAuthStore();
-  const { token } = storeToRefs(authStore);
-
-  if (token.value) return;
-
-  if (to.fullPath.includes("/profile")) {
-    return navigateTo("/login", {
-      replace: true,
-      query: {
-        redirect: to.fullPath,
-      },
-    });
-  }
-
-  if (to.fullPath.includes("/note-create")) {
-    return navigateTo("/login", {
-      replace: true,
-      query: {
-        redirect: to.fullPath,
-      },
-    });
-  }
-  return;
-});

+ 145 - 0
src/pages/chat/announcement.vue

@@ -0,0 +1,145 @@
+<template>
+  <div class="w-full h-[100vh]">
+    <van-nav-bar title="群公告" fixed @click-left="getBack">
+      <template #left>
+        <div>
+          <van-icon name="arrow-left" color="black" size="18" />
+        </div>
+      </template>
+      <template v-if="isRankAndFiler(queryDataName?.groupRole)" #right>
+        <div
+          :class="`font-semibold text-xl ${showBottom ? 'text-[#FF9300]' : 'text-black-9'} `"
+          @click="getUpdAnnouncement"
+        >
+          <!--   -->
+          确认
+        </div>
+      </template>
+    </van-nav-bar>
+    <div class="h-60"></div>
+    <template v-if="isRankAndFiler(queryDataName?.groupRole)">
+      <van-cell-group inset>
+        <van-field
+          style="background-color: #f7f8fa"
+          v-model="queryDataName.groupNotice"
+          required
+          size="large"
+          rows="5"
+          autosize
+          @focus="showBottom = true"
+          @update:model-value="
+            (val) => {
+              if (val.lenght >= 1000) {
+                showToast('输入的内容只能是1000个字')
+              }
+            }
+          "
+          autocorrect
+          type="textarea"
+          maxlength="1000"
+          label-align="top"
+          show-word-limit
+        />
+      </van-cell-group>
+
+      <van-cell
+        @click="
+          () => {
+            queryDataName.groupNotice = groupBulletinTemplate
+            showBottom = true
+          }
+        "
+        :label="groupBulletinTemplate"
+      >
+        <template #title>
+          <span class="font-semibold text-black-6">群公告模版</span>
+        </template>
+      </van-cell>
+    </template>
+    <template v-else>
+      <div style="height: calc(100vh - 60px)" class="w-full relative">
+        <div class="w-full px-16 mb-10" v-html="queryDataName?.groupNotice"></div>
+
+        <p class="w-full text-sm text-black-9 text-center absolute bottom-52 left-0">
+          仅群主及群管理员可编辑
+        </p>
+      </div>
+    </template>
+  </div>
+</template>
+
+<script setup>
+const router = useRouter()
+const route = useRoute()
+
+// 是否是普通成员
+const isRankAndFiler = (role) => {
+  return role == 1 || role == 2 ? true : false
+}
+
+onMounted(() => {
+  getAnnouncement()
+})
+
+const groupBulletinTemplate =
+  '1.本群提倡友好理性交流,鼓励群友多发言,多互动 2.禁止无意义刷屏、发送广告信息以及谩骂等不良消息 3.为了保证群活跃不经常发言的群友,可能会被定时清理出群'
+
+const queryDataName = reactive({
+  groupId: computed(() => route?.query?.groupId ?? ''),
+  userId: computed(() => route?.query?.userId ?? ''),
+  groupRole: computed(() => route?.query?.groupRole ?? ''),
+  groupNotice: ''
+})
+
+//
+const showBottom = ref(false)
+
+const getBack = () => {
+  router.back()
+  showBottom.value = false
+}
+definePageMeta({
+  layout: false
+})
+
+// 修改的接口
+const getUpdAnnouncement = async () => {
+  if (!showBottom.value) return
+
+  try {
+    let { data } = await request('/website/tourMessage/updateAnnouncement', {
+      method: 'post',
+      body: {
+        groupId: queryDataName.groupId,
+        messageContent: queryDataName.groupNotice
+      }
+    })
+    if (data) {
+      showBottom.value = false
+      showSuccessToast('群公告发布成功')
+      router.back()
+    } else {
+      showBottom.value = false
+    }
+  } catch (error) {}
+}
+
+// 获取公告
+const getAnnouncement = async () => {
+  const { data } = await request('/website/tourGroup/getGroupInfoAndMemberByGroupId', {
+    query: {
+      groupId: route.query.groupId
+    }
+  })
+  if (data) {
+    queryDataName.groupNotice = data.groupNotice.messageContent
+  } else {
+  }
+}
+
+useSeoMeta({
+  title: '群聊'
+})
+</script>
+
+<style lang="scss" scoped></style>

+ 84 - 0
src/pages/chat/background.vue

@@ -0,0 +1,84 @@
+<template>
+  <div class="w-full h-[100vh] bg-white">
+    <ChatHeaderBar title="设置当前聊天背景"></ChatHeaderBar>
+    <div class="pt-50">
+      <van-cell clickable @click="open" size="large" title="从相册中选择" is-link></van-cell>
+      <van-cell clickable @click="changeGroupBg" size="large" title="设置成默认背景"></van-cell>
+    </div>
+  </div>
+</template>
+<script setup>
+const route = useRoute()
+
+definePageMeta({
+  layout: false
+})
+
+useSeoMeta({
+  title: '我的消息'
+})
+
+const { open, onChange } = useFileDialog({
+  accept: '.png,.png,.jpeg,.JPG,Png '
+})
+
+const bgImage = ref('')
+
+onChange(async (files) => {
+  if (!files.length) return
+
+  const formData = new FormData()
+  formData.append('uploadFile', files[0])
+  formData.append('asImage', true)
+  formData.append('fieldName', 'image')
+  const maxSize = 20 * 1024 * 1024 // 20 MB
+  if (files[0].size > maxSize) {
+    showToast('上传图片过大,请重新上传')
+    return
+  } else {
+    try {
+      showLoadingToast({
+        message: '图片上传中...',
+        duration: 1000000
+      })
+
+      const { data } = await request('/website/tourMessage/upload', {
+        method: 'post',
+        body: formData
+      })
+      bgImage.value = data.fileUrl
+      changeGroupBg()
+      closeToast()
+    } catch (error) {
+      closeToast()
+      showToast('图片上传失败')
+    }
+  }
+})
+
+// 修改背景图
+const changeGroupBg = async () => {
+  try {
+    const res = await request('/website/tourMember/updateSingleTourMember', {
+      method: 'post',
+      body: {
+        groupId: route.query.groupId,
+        groupBackImage: bgImage.value
+      }
+    })
+
+    if (res && res?.success) {
+      navigateTo({
+        path: 'chat/group',
+        query: route.query.groupId,
+        replace: true
+      })
+      showSuccessToast('聊天背景设置成功')
+    }
+    // else {
+    //   showFailToast('聊天背景设置失败')
+    // }
+  } catch (error) {}
+}
+</script>
+<style lang="scss" scoped></style>

+ 265 - 0
src/pages/chat/components/chat-input/index.vue

@@ -0,0 +1,265 @@
+<template>
+  <div class="chat-input-comp" ref="chatInputCompRef">
+    <div class="input-box">
+      <div v-if="false" class="mr-12">语音todo</div>
+      <div
+          @click="openTool('operate')"
+          class="iconfont icon-close-one rotate-45 mr-12 text-black-6"
+          style="font-size: 32px"
+      ></div>
+      <div
+          @click="openTool('emoji')"
+          class="iconfont icon-slightly-smiling-face text-black-6 mr-12"
+          style="font-size: 32px"
+      ></div>
+      <div class="input-box__textarea mr-12" id="input-kary">
+        <van-field
+            v-model="inputValue"
+            autosize
+            ref="inputRef"
+            rows="1"
+            type="textarea"
+            placeholder="请输入"
+            @focus="handleFocus"
+            @blur="handleBlur"
+        />
+      </div>
+      <van-button size="small" type="warning" @click="sendTextMessage"> 发 送</van-button>
+    </div>
+    <div v-show="showTool" class="px-20 py-8">
+      <div class="operate-box" v-show="currTool === 'operate'">
+        <div v-for="(operate, i) in showOperateList"
+             :key="i"
+             @click="handleOperate(operate)"
+        >
+          <div
+              class="w-54 h-54 active:bg-[#FF9300]/[0.1] bg-[#F3F3F3] shrink-0 rounded-full mb-5 flex justify-center items-center">
+            <span
+                :class="`iconfont ${operate.icon} text-black-6 active:text-[#FF9300] `"
+                style="font-size: 32px"
+            ></span>
+          </div>
+          <p class="text-sm w-full text-center">{{ operate.title }}</p>
+        </div>
+      </div>
+      <div class="emoji-box h-144 overflow-y-auto" v-show="currTool === 'emoji'">
+        <div
+            v-for="(item, index) in emojiJson"
+            :key="index"
+            @click="selectEmoji(item, index)"
+            class="active:bg-[#ddd] text-4xl w-26 aspect-[1/1] grid place-items-center"
+        >
+          <div @click="selectEmoji(index)" v-html="item.emoji"></div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import emojiJson from "~/components/Profile/News/emoji";
+
+const props = defineProps({
+  operates: {
+    type: Array,
+    default: () => ['image']// ['image', 'share-group']
+  },
+})
+const emit = defineEmits(['send', 'focus', 'blur'])
+
+const inputValue = ref('')
+const inputRef = ref(null);
+
+const chatInputCompRef = ref(null);
+const operateList = [
+  {
+    title: '相册',
+    icon: 'icon-pic',
+    isShow: props.operates.includes('image'),
+    fn: 'uploadImage'
+  },
+  {
+    title: '分享群聊',
+    icon: 'icon-peoples-two',
+    isShow: props.operates.includes('share-group'),
+    fn: 'shareGroupChat'
+  }
+];
+
+const showOperateList = computed(() => {
+  return operateList.filter(o => o.isShow)
+})
+
+const {open, onChange: uploadImage} = useFileDialog({
+  accept: 'image/*'
+})
+
+const handleOperate = (operate) => {
+  try {
+    switch (operate.fn) {
+      case 'uploadImage':
+        open()
+        break;
+      case 'shareGroupChat':
+        console.log('分享群聊')
+        showToast('研发中,敬请期待~')
+        break;
+    }
+  } catch (e) {
+
+  }
+}
+
+uploadImage(async (files) => {
+  if (!files.length) return
+
+  console.log(files[0], 'files[0]files[0]')
+  const {type, size} = files[0];
+  if (!['image/jpeg', 'image/png', 'image/gif'].includes(type)) {
+    showToast('请上传图片')
+  }
+  const maxSize = 20 * 1024 * 1024 // 20 MB
+  if (size > maxSize) {
+    showToast('图片大小不能超过20MB')
+    return
+  }
+  /*  const formData = new FormData()
+    formData.append('uploadFile', files[0])
+    formData.append('asImage', true)
+    formData.append('fieldName', 'messageContent')
+    const {data} = await request('/website/tourMessage/upload', {
+      method: 'post',
+      body: formData
+    })*/
+  // closeToast()
+  emit('send', {
+    type: 'image',
+    messageContent: files[0]
+  })
+})
+
+const sendTextMessage = () => {
+  try {
+    if (!inputValue.value.trim()) {
+      showToast('发送内容不能为空!')
+      return
+    }
+    emit('send', {
+      type: 'text',
+      messageContent: inputValue.value
+    })
+    inputValue.value = ''
+  } catch (e) {
+
+  }
+}
+const selectEmoji = async (item, index) => {
+  try {
+    const position = getInputPosition()
+    const str = inputValue.value;
+    inputValue.value = `${str.slice(0, position)}${emojiJson[index].emoji}${str.slice(position)}`
+  } catch (e) {
+
+  }
+}
+
+const showTool = ref(false)
+const currTool = ref('')
+const openTool = (toolName) => {
+  showTool.value = true
+  currTool.value = toolName
+}
+const handleFocus = () => {
+  showTool.value = false
+  emit('focus')
+}
+const handleBlur = () => {
+  emit('blur')
+}
+const getInputPosition = () => {
+  try {
+    if (!inputValue.value) return 0
+    const el = inputRef.value?.$el.querySelector('input, textarea');
+    if (!el) return inputValue.value.length - 1;
+    return el.selectionStart
+  } catch (e) {
+    console.log(e, '??')
+  }
+}
+
+// 判断是否点击的非输入框
+const handleClickOutside = (event) => {
+  const chatInputCompEl = chatInputCompRef.value;
+  const isChatInputCompEl = chatInputCompEl.contains(event.target);
+
+  if (!isChatInputCompEl) {
+    showTool.value = false
+    inputRef.value.$el.blur()
+  }
+};
+onMounted(() => {
+  useEventListener('click', handleClickOutside, {target: document})
+  useEventListener('mousedown', handleClickOutside, {target: document})
+  useEventListener('touchstart', handleClickOutside, {target: document})
+
+  if (process.env.NODE_ENV === 'development') {
+    useEventListener('keyup', (event) => {
+      if (event.key === 'Enter') {
+        sendTextMessage()
+      }
+    })
+  }
+})
+</script>
+
+<style scoped lang="scss">
+.chat-input-comp {
+  width: 100%;
+  border-top: 1px solid #E7E7E7;
+
+  .input-box {
+    width: 100%;
+    //height: 64px;
+    padding: 12px;
+    box-sizing: border-box;
+    background: #fff;
+    display: flex;
+    align-items: center;
+
+    .input-box__textarea {
+      flex: 1;
+      background: #F3F3F3;
+      border-radius: 20px;
+      border: 1px solid #DCDCDC;
+      padding: 5px 10px;
+    }
+  }
+
+  .operate-box {
+    display: grid;
+    grid-template-columns: 54px 54px 54px 54px;
+    grid-template-rows: 54px 54px;
+    gap: 20px;
+  }
+
+  .emoji-box {
+    display: grid;
+    gap: 5px;
+    grid-template-columns: repeat(6, 1fr);
+    grid-auto-rows: auto;
+    box-sizing: border-box;
+    justify-content: center;
+    align-content: center;
+    justify-items: center;
+    align-items: center;
+  }
+}
+
+:deep(.van-cell) {
+  padding: 0;
+  background: transparent;
+  min-height: 24px;
+  max-height: 100px;
+  overflow-y: auto;
+}
+</style>

+ 62 - 0
src/pages/chat/components/chat-message/audio-message/index.vue

@@ -0,0 +1,62 @@
+<script setup>
+const props = defineProps({
+  messageContent: String|Number, // 消息内容
+  viewType: Number, // 0发送 1接收
+});
+
+let videoUrl = ref('')
+let loading = ref(false)
+
+const initMessage = () => {
+  switch (props.viewType) {
+    case 0: // 发送 上传
+      uploadMessage()
+      break;
+    case 1: // 接收
+      videoUrl.value = props.messageContent
+      break;
+  }
+}
+
+const uploadMessage = async () => {
+  try {
+    loading.value = true
+    const formData = new FormData()
+    formData.append('uploadFile', props.messageContent.blob)
+    formData.append('asImage', false)
+    formData.append('fieldName', 'messageContent')
+    const res = await request(`/website/tourMessage/upload`, {
+      method: 'post',
+      body: formData
+    })
+    await handleResponse(res)
+    // videoUrl.value = res.data?.fileUrl ?? '';
+    videoUrl.value = 'https://fastly.jsdelivr.net/npm/@vant/assets/cat.jpeg';
+  } catch (e) {
+
+  } finally {
+    loading.value = false
+  }
+}
+
+onMounted(() => {
+  initMessage()
+})
+
+</script>
+
+<template>
+  <div class="flex flex-row">
+    <div v-if="loading">loading...</div>
+    <template v-if="viewType">
+      <video :src="videoUrl"></video>
+    </template>
+    <template v-else>
+      <video :src="videoUrl"></video>
+    </template>
+  </div>
+</template>
+
+<style scoped lang="scss">
+
+</style>

+ 135 - 0
src/pages/chat/components/chat-message/image-message/index.vue

@@ -0,0 +1,135 @@
+<script setup>
+const props = defineProps({
+  messageContent: String, // 消息内容
+  viewType: Number, // 0发送 1接收
+});
+
+let imgUrl = ref(null)
+let width = ref(75);
+let height = ref(75);
+let loading = ref(false)
+
+/*const initMessage = () => {
+  switch (props.viewType) {
+    case 0: // 发送 上传
+      sendMessage()
+      break;
+    case 1: // 接收
+      acceptMessage(props.messageContent)
+      break;
+  }
+}*/
+
+const resizeImageToMaxSize = (maxSize, w, h) => {
+  let scale = 1;
+  if (w > maxSize || h > maxSize) {
+    if (w > h) {
+      scale = maxSize / w;
+    } else {
+      scale = maxSize / h;
+    }
+  }
+  return {width: scale * w, height: scale * h}
+}
+
+// 处理接收到的消息
+const initMessage = async () => {
+  try {
+    const url = props.messageContent;
+    const img = new Image();
+    img.onload = function() {
+      const size = resizeImageToMaxSize(250, this.width, this.height);
+      width.value = size.width;
+      height.value = size.height
+      imgUrl.value = url;
+    };
+    img.onerror = function() {
+      console.error('图片加载失败');
+    };
+    img.src = url;
+  } catch (e) {
+
+  } finally {
+
+  }
+}
+// 处理发出去的消息
+const sendMessage = async () => {
+  try {
+    loading.value = true
+    const formData = new FormData()
+    formData.append('uploadFile', props.messageContent)
+    formData.append('asImage', true)
+    formData.append('fieldName', 'messageContent')
+    const res = await request(`/website/tourMessage/upload`, {
+      method: 'post',
+      body: formData
+    })
+    await handleResponse(res)
+    const fileUrl = res.data?.fileUrl;
+    // 应该读本地图片,但是没做发送失败的情况,就先这样吧
+    const img = new Image();
+    img.onload = function() {
+      const size = resizeImageToMaxSize(250, this.width, this.height);
+      width.value = size.width;
+      height.value = size.height
+      imgUrl.value = fileUrl;
+    };
+    img.onerror = function() {
+      console.error('图片加载失败');
+    };
+    img.src = fileUrl;
+  } catch (e) {
+
+  } finally {
+    loading.value = false
+  }
+}
+
+const preview = () => {
+  showImagePreview({
+    images: [imgUrl.value]
+  });
+}
+
+onMounted(() => {
+  initMessage()
+})
+
+</script>
+
+<template>
+  <div class="image-message">
+    <van-image :src="imgUrl" :width="width" :height="height" @click="preview">
+      <template v-slot:loading>
+        <div class="relative">
+          <div class="absolute w-full h-full left-0 right-0 flex items-center justify-center">
+            <van-loading type="spinner" size="20"/>
+          </div>
+          <img class="w-full h-full" src="../../../../../assets/img/chat/image-loading.png" alt="loading"/>
+        </div>
+      </template>
+      <template v-slot:error>
+        <img src="../../../../../assets/img/chat/image-error.png" alt="error"/>
+      </template>
+    </van-image>
+  </div>
+</template>
+
+<style scoped lang="scss">
+.image-message {
+  display: flex;
+  position: relative;
+
+  .message__operate {
+    width: 16px;
+    height: 16px;
+    border-radius: 100%;
+    background: #FF476A;
+    color: #fff;
+    position: absolute;
+    right: 8px;
+  }
+}
+
+</style>

+ 133 - 0
src/pages/chat/components/chat-message/index.vue

@@ -0,0 +1,133 @@
+<template v-if="msg">
+  <div class="chat-message" :class="msg.viewType ? 'chat-message--accept' : 'chat-message--send'">
+    <div class="h-20 grid place-items-center text-center text-sm text-[#000]/40 mb-16">{{ msg.createTime }}</div>
+    <div class="chat-message__content">
+      <van-image
+          v-if="msg.viewType === 1"
+          :src="msg.headImageUrl"
+          width="40px"
+          height="40px"
+          radius="100%"
+          class="mr-8"
+      ></van-image>
+      <div  v-if="false" class="self-center text-sm mx-5 text-black-9">
+        <van-loading v-if="sendStatus === 'loading'" size="16"/>
+        <van-icon v-if="sendStatus === 'error'" name="warning"  size="16"/>
+      </div>
+      <div class="flex flex-col" :class=" msg.viewType ?  'items-start' : 'items-end'">
+        <div v-if="showName && msg.viewType === 1" class="text-black-9 text-sm mb-2">{{ msg.nickName }}</div>
+        <div class="flex-grow-0 w-fit">
+          <TextMessage v-if="msg.type === 'text'" :message-content="msg.messageContent"
+                       :view-type="msg.viewType"></TextMessage>
+          <ImageMessage v-if="msg.type === 'image'" :message-content="msg.messageContent"
+                        :view-type="msg.viewType"></ImageMessage>
+          <AudioMessage v-if="msg.type === 'audio'" :message-content="msg.messageContent"
+                        :view-type="msg.viewType"></AudioMessage>
+          <LinkMessage v-if="msg.type === 'link'" :message-content="msg.messageContent"
+                       :view-type="msg.viewType"></LinkMessage>
+        </div>
+      </div>
+<!--      <div class="self-center text-sm mx-5 text-black-9">发送中</div>-->
+      <van-image
+          v-if="msg.viewType === 0"
+          :src="msg.headImageUrl"
+          width="40px"
+          height="40px"
+          radius="100%"
+          class="ml-8"
+      ></van-image>
+    </div>
+  </div>
+</template>
+<script setup>
+import defaultAvatar from "assets/img/default_avatar.png";
+import TextMessage from "~/pages/chat/components/chat-message/text-message/index.vue";
+import ImageMessage from "~/pages/chat/components/chat-message/image-message/index.vue";
+import AudioMessage from "~/pages/chat/components/chat-message/audio-message/index.vue";
+import LinkMessage from "~/pages/chat/components/chat-message/link-message/index.vue";
+import {findHyperlinks} from "~/pages/chat/components/chat-message/link-message/handle";
+
+const userInfoStore = useUserInfoStore();
+const {userInfo} = storeToRefs(userInfoStore);
+
+const props = defineProps({
+  message: Object,
+  showName: {
+    type: Boolean,
+    default: false
+  }
+});
+const msg = computed(() => {
+  try {
+    // console.log(props.message, 'msg')
+    const {
+      createTime,
+      getUserId,
+      sendUserId,
+      messageContent,
+      messageType,
+      object = {headImageUrl: defaultAvatar, showName: '无名大侠'}
+    } = props.message
+    return {
+      messageContent: messageContent,
+      type: getMessageType(messageType, messageContent),
+      viewType: sendUserId === userInfo?.value.pass ? 0 : 1,
+      createTime: createTime,
+      headImageUrl: object.headImageUrl,
+      nickName: object.showName
+    }
+  } catch (e) {
+    console.log(e, '??')
+    return null
+  }
+})
+
+const sendStatus = ref('success')
+const handleSendStatus = () => {
+  if (msg?.value.createTime) {
+    sendStatus.value = ''
+    return
+  }
+  sendStatus.value = 'loading'
+  setTimeout(() => {
+    if (!msg?.value?.createTime) {
+      sendStatus.value = 'error'
+    } else {
+      sendStatus.value = 'success'
+    }
+  }, 2 * 1000)
+}
+
+onMounted(() => {
+  // handleSendStatus()
+})
+
+
+const getMessageType = (messageType, messageContent) => {
+  const types = ['text', 'image', 'audio', 'video', 'link']
+  if (messageType === 0 && findHyperlinks(messageContent)) return types[4]
+  return types[messageType]
+}
+</script>
+<style scoped lang="scss">
+.chat-message {
+  margin: 20px 0;
+
+  .chat-message__content {
+    width: max-content;
+    display: flex;
+  }
+
+  &.chat-message--accept {
+    .chat-message__content {
+      margin-right: auto;
+    }
+  }
+
+  &.chat-message--send {
+    .chat-message__content {
+      margin-left: auto;
+    }
+  }
+}
+</style>

+ 20 - 0
src/pages/chat/components/chat-message/link-message/handle.js

@@ -0,0 +1,20 @@
+
+const IN_STATION_LINK = ['xiaoyaotravel.com']// 站内链接
+export const findHyperlinks = (text) => {
+  try {
+    const urlPattern = /https?:\/\/[^\s]+/g;
+    if (!text.match(urlPattern)) return null
+    const maybeUrl = text.match(urlPattern)[0];
+    const url = new URL(maybeUrl);
+    let hostname = url.hostname;
+    const mainParts = hostname.split('.').slice(-2).join('.');
+    return {
+      mainParts,
+      hostname: hostname,
+      isInStation: IN_STATION_LINK.includes(hostname),
+      url: maybeUrl
+    }
+  } catch (e) {
+    return null
+  }
+}

+ 88 - 0
src/pages/chat/components/chat-message/link-message/index.vue

@@ -0,0 +1,88 @@
+<script setup>
+import { findHyperlinks } from '~/pages/chat/components/chat-message/link-message/handle'
+
+const props = defineProps({
+  messageContent: String | Number, // 消息内容
+  viewType: Number // 0发送 1接收
+})
+
+let linkInfo = ref(null) // hostname、mainParts、isInStation、url
+
+const initMessage = () => {
+  try {
+    linkInfo.value = findHyperlinks(props.messageContent) ?? null
+  } catch (e) {
+    console.error(e, 'initMessage')
+  }
+}
+onMounted(() => {
+  initMessage()
+})
+</script>
+
+<template>
+  <div class="link-message" :class="viewType ? 'link-message--accept' : 'link-message--send'">
+    <div class="link-message__content">
+      <div>
+        {{ messageContent }}
+      </div>
+      <template v-if="linkInfo">
+        <NuxtLink
+          :to="linkInfo.url"
+          target="_blank"
+          class="bg-[#FFF] p-12 rounded-[4px] mt-12 flex flex-row"
+        >
+          <div class="flex-1 min-w-0 mr-10">
+            <div class="text-black-3 text-base text-ellipsis text-nowrap overflow-hidden ...">
+              {{ linkInfo.mainParts }}
+            </div>
+            <div class="text-black-9 text-sm text-ellipsis text-nowrap overflow-hidden ...">
+              {{ linkInfo.hostname }}
+            </div>
+          </div>
+          <div class="ml-auto w-40 h-40 rounded-2 bg-[#F1F1F1] grid place-items-center">
+            <img src="~/assets/img/chat/link-icon.png" height="24" width="25" alt="" />
+          </div>
+        </NuxtLink>
+      </template>
+    </div>
+    <template v-if="linkInfo">
+      <div v-if="linkInfo.isInStation"
+           class="bg-[#F3F3F3] rounded-[25px] px-12 py-4 text-black-9 mt-4 text-sm w-max grid place-items-center"
+      >
+        转自 第三方链接
+      </div>
+    </template>
+
+  </div>
+</template>
+
+<style scoped lang="scss">
+.link-message {
+  .link-message__content {
+    max-width: 250px;
+    padding: 12px;
+    box-sizing: border-box;
+    background: #f3f3f3;
+    color: #000000;
+    color: rgba(0, 0, 0, 0.9);
+    font-size: 14px;
+    word-wrap: break-word;
+    word-break: break-all;
+  }
+
+  &.link-message--accept {
+    .link-message__content {
+      background: #f3f3f3;
+      border-radius: 0 12px 12px 12px;
+    }
+  }
+
+  &.link-message--send {
+    .link-message__content {
+      background: #fef4e6;
+      border-radius: 12px 0 12px 12px;
+    }
+  }
+}
+</style>

+ 44 - 0
src/pages/chat/components/chat-message/text-message/index.vue

@@ -0,0 +1,44 @@
+<script setup>
+const props = defineProps({
+  messageContent: String, // 消息内容
+  viewType: Number, // 0发送 1接收
+});
+
+
+let textString = ref('')
+let loading = ref(false)
+const initMessage = () => {
+  textString.value = props.messageContent
+}
+
+initMessage()
+</script>
+
+<template>
+  <div class="text-message" :class="viewType ? 'text-message--accept' : 'text-message--send'">
+    {{messageContent}} <!--{{ textString }}-->
+  </div>
+</template>
+
+<style scoped lang="scss">
+.text-message {
+  max-width: 250px;
+  padding: 12px;
+  box-sizing: border-box;
+  background: #F3F3F3;
+  color: #000000;
+  color: rgba(0, 0, 0, 0.9);
+  font-size: 14px;
+  word-wrap: break-word;
+  word-break: break-all;
+  &.text-message--accept {
+    background: #F3F3F3;
+    border-radius: 0 12px 12px 12px;
+  }
+
+  &.text-message--send {
+    background: #FEF4E6;
+    border-radius: 12px 0 12px 12px;
+  }
+}
+</style>

+ 317 - 0
src/pages/chat/create-group.vue

@@ -0,0 +1,317 @@
+<template>
+  <!-- 创建群聊 -->
+  <div class="w-full h-[100vh] bg-[#F7F8FA] box-border pt-66">
+    <ChatHeader title="创建群聊" />
+
+    <van-cell-group style="margin-bottom: 12px" inset>
+      <van-field
+        v-model="formData.groupName"
+        rows="1"
+        autosize
+        :rules="[{ required: true, message: '请输入群名称' }]"
+        type="textarea"
+        placeholder="请输入群名称"
+        label-align="top"
+        maxlength="30"
+        show-word-limit
+      >
+        <template #label>
+          <span class="text-xl text-black-3">
+            群名称
+            <span class="text-[#EE0C0C] m-0 p-0">*</span>
+          </span>
+        </template>
+      </van-field>
+    </van-cell-group>
+    <van-cell-group style="margin-bottom: 12px" inset>
+      <van-field
+        v-model="formData.description"
+        rows="3"
+        autosize
+        type="textarea"
+        :rules="[{ required: true, message: '请输入群介绍' }]"
+        placeholder="简单说说你想在群内讨论的话题、以及希望哪些人加入群聊"
+        label-align="top"
+        maxlength="200"
+        show-word-limit
+      >
+        <template #label>
+          <span class="text-xl text-black-3">
+            群介绍
+            <span class="text-[#EE0C0C] m-0 p-0">*</span>
+          </span>
+        </template>
+      </van-field>
+    </van-cell-group>
+
+    <van-cell-group style="margin-bottom: 12px" inset>
+      <!-- value="未选择" -->
+      <van-cell @click="show = true" center is-link :value="groupTypeName">
+        <template #title>
+          <span class="text-xl text-black-3">群类型</span>
+        </template>
+        <template #label>
+          <span class="text-base text-black/[0.4]">同城生活,聊天交友等</span>
+        </template>
+      </van-cell>
+    </van-cell-group>
+
+    <van-cell-group style="margin-bottom: 12px" inset>
+      <van-cell center>
+        <template #title>
+          <span class="text-xl text-black-3">个人主页展示</span>
+        </template>
+        <template #label>
+          <span class="text-base text-black/[0.4]">开启后,在群聊广场和个人主页</span>
+        </template>
+        <template #right-icon>
+          <van-switch v-model="checked" active-color="#FF9300" inactive-color="#dcdee0" />
+        </template>
+      </van-cell>
+    </van-cell-group>
+
+    <!-- 弹窗 -->
+
+    <van-dialog width="90%" v-model:show="show" show-cancel-button>
+      <template #title>
+        <div class="w-full flex justify-between items-center px-16 py-11 border-b-[1px]">
+          <h1>群类型</h1>
+          <div class="w-32 h-32 -mt-3 shrink-0" @click="show = false">
+            <img
+              @click="visible = false"
+              class="w-full h-full object-cover"
+              src="~/assets/img/note-create/close.svg"
+              alt=""
+            />
+          </div>
+        </div>
+      </template>
+
+      <div class="w-full px-16 py-20 card-list">
+        <template v-for="(subItem, subIndex) in groupTypeList" :key="subItem?.id">
+          <div
+            :class="` h-40 mb-4  relative ${showIndex == subItem.id ? ' border-[#FF9300] border-2  shadow-[0_4px_4px_0px_rgba(0,0,0,0.1)]' : ''}  pl-22 flex justify-start items-center shrink-0 text-xl text-black-6 font-semibold bg-[#F7F8FA] rounded-md`"
+            @click="handleTypeClick(subItem)"
+          >
+            <div class="w-24 h-24 shrink-0 mr-6">
+              <img
+                class="w-full h-full object-cover"
+                :src="item?.typeIcon ? item?.typeIcon : city"
+                alt=""
+              />
+            </div>
+            <span class="line-clamp-1">
+              {{ subItem.typeName }}
+            </span>
+
+            <div
+              v-if="showIndex == subItem.id"
+              class="absolute rounded-t-md square1 -top-2 -left-2 z-1"
+            ></div>
+            <div v-if="showIndex == subItem.id" class="w-14 h-14 absolute top-0 left-0 z-2">
+              <img class="w-full h-full object-cover" alt="" src="~/assets/img/chat/check.svg" />
+            </div>
+          </div>
+
+          <div
+            v-if="showIndex == subItem.id && subItem?.children.length > 0"
+            class="item__child mb-4 w-full relative flex justify-start box-border flex-wrap pl-21 pt-14 bg-[#F7F7F7] rounded-md"
+          >
+            <div
+              :class="`w-32 h-8 absolute -top-[8px] ${subIndex % 2 != 0 ? 'right-[23px]' : 'left-[23px]'} `"
+            >
+              <img class="w-full h-full" src="~/assets/img/chat/polygon.svg" alt="" />
+            </div>
+            <template v-for="el in subItem.children" :key="el?.id">
+              <div
+                @click="childrenHandleTypeClick(el)"
+                :class="`${childrenIndex == el.id ? 'text-[#FF9300] border-[#FF9300]  border-[2px] bg-[#FF9300]/[0.08]' : 'bg-white border text-black-6'} py-5 mr-8 mb-12 rounded-[4px] text-sm  box-border px-12`"
+              >
+                {{ el.typeName }}
+              </div>
+            </template>
+          </div>
+        </template>
+      </div>
+
+      <template #footer>
+        <div class="w-full px-40 pb-30">
+          <van-button
+            style="font-size: 16px"
+            type="primary"
+            color="#FF9300"
+            round
+            block
+            @click="show = false"
+          >
+            确认
+          </van-button>
+        </div>
+      </template>
+    </van-dialog>
+
+    <div class="w-full h-72 px-16 pb-24 fixed bottom-0 left-0 box-border">
+      <van-button
+        style="font-size: 16px"
+        type="primary"
+        native-type="submit"
+        color="#FF9300"
+        round
+        block
+        @click="handleCreateGroup"
+        :loading="isSubmiting"
+      >
+        立即创建
+      </van-button>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import city from '~/assets/img/chat/city-one.svg'
+
+const userInfoStore = useUserInfoStore()
+const { userInfo } = storeToRefs(userInfoStore)
+
+const TYPE_TEXT = '未选择'
+
+const groupTypeList = ref([])
+
+const subTypeList = ref([])
+
+definePageMeta({
+  layout: false
+})
+
+const formData = reactive({
+  createType: '1',
+  groupName: '',
+  creatUserId: computed(() => userInfo.value.userId),
+  description: '',
+  belongTypeId: ''
+})
+
+// 是否群类型的弹窗打开
+const show = ref(false)
+const showIndex = ref(null)
+const groupTypeName = ref(TYPE_TEXT)
+
+const childrenIndex = ref(null)
+
+// 是否个人主页展示
+const checked = ref(true)
+
+// 获取群类型
+async function getTreeType() {
+  try {
+    const { data } = await request('/website/tourGroupType/treeType')
+    if (Array.isArray(data) && data.length) {
+      groupTypeList.value = data
+    } else {
+      groupTypeList.value = []
+    }
+  } finally {
+  }
+}
+
+const handleTypeClick = (item) => {
+  if (showIndex.value == item?.id) {
+    showIndex.value = null
+    subTypeList.value = []
+  } else {
+    showIndex.value = item.id
+    subTypeList.value = item.children
+  }
+}
+
+//
+const childrenHandleTypeClick = (item) => {
+  if (childrenIndex.value == item?.id) {
+    formData.belongTypeId = ''
+    childrenIndex.value = null
+
+    groupTypeName.value = TYPE_TEXT
+  } else {
+    formData.belongTypeId = item.id
+    childrenIndex.value = item.id
+    groupTypeName.value = item.typeName
+  }
+}
+
+const isSubmiting = ref(false)
+// 创建群聊
+async function handleCreateGroup() {
+  try {
+    isSubmiting.value = true
+
+    if (!formData.groupName) {
+      showToast('请输入群名称')
+      return
+    }
+
+    if (!formData.description) {
+      showToast('请输入群描述')
+      return
+    }
+    checked ? (formData.isPublic = 1) : (formData.isPublic = 0)
+    const { data } = await request('/website/tourGroup/createGroup', {
+      method: 'post',
+      body: {
+        ...formData
+      }
+    })
+
+    if (data) {
+      showSuccessToast('群聊创建成功')
+
+      // navigateTo({
+      //   path: '/profile/my-news',
+
+      //   replace: true
+      // })
+
+      navigateTo({
+        path: '/chat/group-chat',
+        query: {
+          groupId: data
+        },
+        replace: true
+      })
+    }
+  } finally {
+    isSubmiting.value = false
+  }
+}
+
+onMounted(() => {
+  getTreeType()
+})
+
+useSeoMeta({
+  title: '我的消息'
+})
+</script>
+
+<style lang="scss" scoped>
+::v-deep .van-dialog__header {
+  padding-top: 0;
+}
+
+.card-list {
+  display: grid;
+  grid-template-columns: repeat(2, 1fr);
+  grid-auto-flow: dense; /* 确保项目会填充所有的网格单元 */
+  grid-gap: 12px;
+
+  .item__child {
+    grid-column: span 2;
+  }
+
+  .square1 {
+    width: 0;
+    height: 0;
+    border-bottom: 28px solid transparent; /* 创建三角形 */
+    border-left: 28px solid #ff9300; /* 三角形的颜色 */
+  }
+}
+</style>

+ 190 - 0
src/pages/chat/examine.vue

@@ -0,0 +1,190 @@
+<template>
+  <div>
+    <ChatHeaderBar title="管理员审核成员"></ChatHeaderBar>
+    <ChatSearch placeholder="请输入关键词" v-model:searchString="searchString" @search="search" />
+
+    <van-pull-refresh v-model="refreshing" @refresh="onRefresh">
+      <ChatEmpty v-if="!addDataList?.length && !loading" image="search" title="暂无数据" />
+      <van-list
+        v-else-if="addDataList.length"
+        v-model:loading="loading"
+        error-text="获取失败"
+        finished-text="-- 没有更多了 --"
+        :finished="finished"
+        :immediate-check="false"
+        @load="getLoadList"
+      >
+        <van-checkbox-group v-model="checked">
+          <template v-for="(item, index) in addDataList" :key="item?.id">
+            <van-cell center clickable @click="toggle(item?.id)" disabled>
+              <template #icon>
+                <div class="flex justify-start">
+                  <van-checkbox
+                    checked-color="#FD9A00"
+                    :ref="(el) => (checkboxRefs[item?.id] = el)"
+                    :name="item"
+                    @click.stop
+                  />
+
+                  <div class="w-40 h-40 ml-13 mr-12 rounded-full overflow-hidden">
+                    <img
+                      v-if="item?.tourUser?.headImageUrl"
+                      class="w-full h-full shrink-0 object-cover"
+                      :src="item?.tourUser.headImageUrl"
+                      alt=""
+                    />
+                    <img
+                      v-else
+                      class="w-full h-full shrink-0 object-cover"
+                      src="~/assets/img/default_avatar.png"
+                      alt=""
+                    />
+                  </div>
+                </div>
+              </template>
+              <template #title>
+                <div class="flex items-center">
+                  <h1 class="text-xl text-black-3">{{ item?.tourUser?.showName }}</h1>
+                </div>
+              </template>
+            </van-cell>
+          </template>
+        </van-checkbox-group>
+      </van-list>
+    </van-pull-refresh>
+
+    <div class="fixed bottom-0 left-0 w-full flex justify-between items-center p-16 pb-40 bg-white">
+      <van-button
+        @click="handleCancel"
+        size="large"
+        style="color: #ff9300; margin-right: 8px"
+        class="font-semibold w-[48%]"
+        round
+        color="#FEF4E6"
+      >
+        取消
+      </van-button>
+      <van-button
+        @click="handlePass(2)"
+        size="large"
+        class="font-semibold w-[48%]"
+        round
+        color="#FF9300"
+      >
+        通过
+      </van-button>
+    </div>
+  </div>
+</template>
+<script setup>
+const route = useRoute()
+definePageMeta({
+  layout: false
+})
+
+useSeoMeta({
+  title: '群聊'
+})
+
+const refreshing = ref(false)
+
+const loading = ref(false)
+const finished = ref(false)
+const searchString = ref('')
+
+const checkedId = ref([])
+
+const checked = ref([])
+const checkboxRefs = ref([])
+
+onMounted(() => {
+  getList()
+})
+
+const addDataList = ref([])
+const filterDataList = ref([])
+
+const toggle = (id) => {
+  let index = checked.value.findIndex((el) => el?.id == id)
+
+  if (index != -1) {
+    checkedId.value.splice(index, 1)
+  } else {
+    checkedId.value.push(id)
+  }
+  console.log(checkedId.value, 'checkedId.value')
+  checkboxRefs.value[id].toggle()
+}
+
+const search = () => {
+  searchString.value
+  addDataList.value = filterDataList.value.map((item) => {
+    if (item.groupName.includes(searchString.value)) {
+      return item
+    }
+  })
+}
+
+const onRefresh = () => {
+  getList()
+}
+
+// 获取数据
+const getList = async () => {
+  try {
+    let url = `/website/tourGroup/getApplicationsList`
+
+    loading.value = true
+    let { data } = await request(url, {
+      query: {
+        groupId: route.query.groupId
+      }
+    })
+
+    if (Array.isArray(data) && data?.length) {
+      addDataList.value = data
+      filterDataList.value = data
+    } else {
+      addDataList.value = []
+      filterDataList.value = []
+    }
+
+    loading.value = false
+    refreshing.value = false
+  } catch (err) {
+  } finally {
+    loading.value = false
+  }
+}
+
+// 通过成员
+async function handlePass(isPass) {
+  try {
+    let { data } = await request('/website/tourGroup/applyIsPass', {
+      method: 'post',
+      body: {
+        groupId: route.query.groupId,
+        ids: checkedId.value,
+        isPass //2 同意 3 不同意
+      }
+    })
+
+    if (data) {
+      navigateTo('/chat/group-chat', {
+        replace: true
+      })
+    }
+  } catch (error) {}
+}
+// 取消 审核
+function handleCancel() {
+  if (checked.value.length == 0) {
+    navigateTo('/chat/group-chat', {
+      replace: true
+    })
+  } else {
+    handlePass(3)
+  }
+}
+</script>
+<style lang="scss" scoped></style>

+ 259 - 0
src/pages/chat/group-add.vue

@@ -0,0 +1,259 @@
+<template>
+  <div class="w-full h-[100vh]">
+    <ChatHeaderBar title="添加成员" />
+
+    <ChatSearch v-model:searchString="showName" @search="search" placeholder="请输入关键词" />
+
+    <van-pull-refresh v-model="refreshing" @refresh="onRefresh">
+      <ChatEmpty
+        image="search"
+        v-if="!addDataList?.length && !loading"
+        title="暂无数据"
+        top="100"
+      />
+
+      <van-list
+        v-model:loading="loading"
+        error-text="获取失败"
+        finished-text="-- 没有更多了 --"
+        :finished="finished"
+        :immediate-check="false"
+      >
+        <div style="height: calc(100vh - 170px)">
+          <van-checkbox-group v-model="checked">
+            <!-- <van-index-bar
+              highlight-color="#FD9A00"
+              :index-list="sortStringToAZ.sortIndex"
+              :sticky="false"
+            > -->
+            <!-- <template v-for="(el, index) in addDataList" :key="index"> -->
+            <!-- <van-index-anchor v-if="el[1].length" :index="el[0]" /> -->
+            <!-- <template v-for="(el, index) in addDataList" :key="index"> -->
+            <!-- <van-index-anchor  :index="el[0]" /> -->
+
+            <template v-for="item in addDataList" :key="item.userId">
+              <van-cell center clickable @click.stop="toggle(item)">
+                <template #icon>
+                  <div class="flex justify-start">
+                    <van-checkbox
+                      checked-color="#FD9A00"
+                      :name="item?.userId"
+                      :ref="(el) => (checkboxRefs[item?.userId] = el)"
+                      @click.stop="toggle(item)"
+                    />
+
+                    <div class="w-40 h-40 ml-13 mr-12 rounded-full overflow-hidden">
+                      <img
+                        v-if="item?.headImageUrl"
+                        class="w-full h-full shrink-0 object-cover"
+                        :src="item?.headImageUrl"
+                        alt=""
+                      />
+
+                      <img
+                        class="w-full h-full shrink-0 object-cover"
+                        src="~/assets/img/default_avatar.png"
+                        alt=""
+                      />
+                    </div>
+                  </div>
+                </template>
+                <template #title>
+                  <div class="flex items-center">
+                    <h1 class="text-xl text-black-3">
+                      {{ item?.showName }}
+                    </h1>
+                    <van-tag
+                      v-if="item.fansStatus == 2"
+                      style="margin-left: 5px; padding: 3px 6px"
+                      color="#F7F8FA"
+                      text-color="#666666"
+                    >
+                      相互关注
+                    </van-tag>
+                  </div>
+                </template>
+              </van-cell>
+            </template>
+            <!-- </template> -->
+            <!-- </template> -->
+            <!-- </van-index-bar> -->
+          </van-checkbox-group>
+        </div>
+      </van-list>
+    </van-pull-refresh>
+
+    <div
+      class="w-full box-border p-16 pb-40 bg-white fixed bottom-0 left-0 flex justify-between items-center shadow-[0px_-4px_4px_0px_rgba(0,0,0,0.1)]"
+    >
+      <div class="shrink-0 flex justify-start items-center">
+        <div class="w-118 shrink-0 flex justify-start items-center overflow-hidden">
+          <div
+            v-for="(item, index) in checkedList.slice(0, 5)"
+            :key="index + 'avatar'"
+            :class="`w-36 h-36  ${index == 0 ? '' : '-ml-16'} shrink-0 rounded-full overflow-hidden`"
+          >
+            <img
+              v-if="item?.headImageUrl"
+              class="w-full h-full object-cover"
+              :src="item?.headImageUrl"
+              alt=""
+            />
+            <img
+              v-else
+              class="w-full h-full shrink-0 object-cover"
+              src="~/assets/img/default_avatar.png"
+              alt=""
+            />
+          </div>
+        </div>
+
+        <div v-if="checkedList.length > 5" class="shrink-0 w-24 h-24 ml-8">
+          <img class="w-full h-full object-cover" src="~/assets/img/chat/ellipsis.svg" alt="" />
+        </div>
+      </div>
+      <van-button
+        :disabled="checkedList.length > 0 ? false : true"
+        @click="handleCreateGroup"
+        style="width: 160px"
+        class="shrink-0"
+        block
+        size="large"
+        color="#FD9A00"
+        round
+      >
+        完成
+        <span v-if="checkedList.length">({{ checkedList.length }})</span>
+      </van-button>
+    </div>
+  </div>
+</template>
+<script setup>
+const route = useRoute()
+
+definePageMeta({
+  layout: false
+})
+
+onMounted(() => {
+  getList()
+})
+
+const refreshing = ref(false)
+const loading = ref(false)
+const finished = ref(false)
+
+const checked = ref([])
+const showName = ref('')
+const checkboxRefs = ref([])
+const checkedList = ref([])
+
+const queryParams = reactive({
+  pageNum: 1,
+  pageSize: 10,
+  flagPage: 1
+})
+
+// const _addDataList = ref(new Map([]))
+const addDataList = ref([])
+const filterDataList = ref([])
+
+useSeoMeta({
+  title: '群聊'
+})
+
+// 选中要邀请的人
+const toggle = (item) => {
+  let index = checkedList.value.findIndex((el) => el?.userId == item?.userId)
+
+  if (index != -1) {
+    checkedList.value.splice(index, 1)
+  } else {
+    checkedList.value.push(item)
+  }
+
+  checkboxRefs.value[item.userId].toggle()
+}
+// 搜索
+const search = () => {
+  finished.value = true
+  if (showName.value) {
+    addDataList.value = filterDataList.value.filter((item) =>
+      item.showName.includes(showName.value)
+    )
+  } else {
+    addDataList.value = filterDataList.value
+  }
+}
+
+// 刷新
+const onRefresh = () => {
+  queryParams.pageNum = 1
+  addDataList.value = []
+  filterDataList.value = []
+  getList()
+}
+
+// 获取数据
+const getList = async () => {
+  try {
+    let url = `/website/tourMember/memberLit`
+
+    loading.value = true
+    let { data } = await request(url, {
+      query: {
+        groupId: route.query.groupId
+      }
+    })
+
+    if (Array.isArray(data) && data?.length) {
+      // const { sortListMap } = sortStringToAZ.sort(data, 'showName')
+
+      addDataList.value = data
+      filterDataList.value = data
+    } else {
+      addDataList.value = []
+    }
+
+    loading.value = false
+    refreshing.value = false
+    if (addDataList.value.length >= totalCount) {
+      finished.value = true
+    } else {
+      finished.value = false
+    }
+  } catch (err) {
+  } finally {
+    refreshing.value = false
+    loading.value = false
+  }
+}
+
+// 创建多人聊天
+async function handleCreateGroup() {
+  try {
+    showLoadingToast({
+      message: '准备开始群聊...',
+      duration: 100000
+    })
+    let { data } = request('/website/tourMember/invite', {
+      method: 'post',
+      body: {
+        groupId: route.query.groupId,
+        ids: checked.value
+      }
+    })
+    if (data) {
+      navigateTo({
+        path: '/chat/group',
+        query: data,
+        replace: true
+      })
+    }
+  } catch (error) {
+  } finally {
+    closeToast()
+  }
+}
+</script>
+<style lang="scss" scoped></style>

+ 154 - 0
src/pages/chat/group-all.vue

@@ -0,0 +1,154 @@
+<template>
+  <div class="w-full h-[100vh]">
+    <ChatHeader title="移除群成员" />
+    <ChatSearch v-model:searchString="showName" @search="search" />
+
+    <ChatEmpty
+      v-if="!groupMember?.length && !loading"
+      image="search"
+      :title="`没有找到&quot;${showName}&quot;相关成员`"
+    />
+
+    <div
+      v-if="groupMember?.length && !showName"
+      class="box-border w-full min-h-400 mt-16 mb-12 pt-12 pl-12"
+    >
+      <van-row>
+        <van-col
+          style="width: 54px"
+          v-for="(item, index) in groupMember"
+          :key="index"
+          span="4"
+          class="mb-12 mr-10"
+        >
+          <div class="w-40 h-40 rounded-full mx-auto overflow-hidden mb-4">
+            <img
+              v-if="item?.headImageUrl"
+              class="w-full h-full object-cover"
+              :src="item?.headImageUrl"
+              alt=""
+            />
+            <img
+              v-else
+              class="w-full h-full shrink-0 object-cover"
+              src="~/assets/img/default_avatar.png"
+              alt=""
+            />
+          </div>
+          <p class="w-full line-clamp-1 lin text-sm text-center text-black-6">
+            {{ item?.showName }}
+          </p>
+        </van-col>
+        <van-col
+          span="4"
+          class="mb-12 mr-10"
+          @click="navigateTo(`/chat/group-add?groupId=${groupId}`)"
+        >
+          <div
+            class="w-40 h-40 rounded-full flex justify-center items-center bg-[#F3F3F3] border mx-auto overflow-hidden mb-4"
+          >
+            <van-icon name="plus" size="20" />
+          </div>
+          <p class="w-full line-clamp-1 lin text-sm text-center text-black-6">添加成员</p>
+        </van-col>
+        <van-col v-if="isRankAndFiler(setData?.groupRole)" span="4 mb-12 mr-10">
+          <div
+            class="w-40 h-40 rounded-full flex justify-center items-center bg-[#F3F3F3] border mx-auto overflow-hidden mb-4"
+          >
+            <van-icon name="minus" size="20" />
+          </div>
+          <p class="w-full line-clamp-1 lin text-sm text-center text-black-6">删除成员</p>
+        </van-col>
+      </van-row>
+    </div>
+    <!-- 搜索后的结果 -->
+    <div v-if="searchMember?.length && showName" class="w-full border-box min-h-400 pl-16">
+      <van-cell
+        v-for="(item, index) in searchMember"
+        :key="item?.id"
+        center
+        clickable
+        size="large"
+        class="border-b-[1px]"
+        :title="item?.nickname"
+      >
+        <template #icon>
+          <div class="w-40 h-40 rounded-full mr-12 overflow-hidden">
+            <img class="w-full h-full object-cover" :src="item?.avatar" alt="" />
+          </div>
+        </template>
+      </van-cell>
+    </div>
+  </div>
+</template>
+<script setup>
+const userInfoStore = useUserInfoStore()
+const { userInfo } = storeToRefs(userInfoStore)
+const route = useRoute()
+const loading = ref(false)
+const showName = ref('')
+
+onMounted(() => {
+  getList()
+})
+
+// 全部成员
+const groupMember = ref([])
+
+// 搜索的成员
+const searchMember = ref([])
+
+const setData = ref(null)
+const groupId = ref(route.query.groupId)
+
+// 搜索
+const search = () => {
+  if (showName.value) {
+    searchMember.value = groupMember.value.filter((el) => el.showName == showName.value)
+  } else {
+    searchMember.value = []
+  }
+}
+
+// 获取群设置的配置信息   ///website/tourGroup/getGroupInfoAndMemberByGroupId
+const getList = async () => {
+  try {
+    loading.value = true
+    let { data } = await request('/website/tourMember/getTourMemberInfoList', {
+      query: {
+        groupId: groupId.value
+      }
+    })
+
+    if (data) {
+      setData.value = data
+      if (Array.isArray(data?.memberList) && data.memberList?.length) {
+        showName.value
+          ? (searchMember.value = data?.memberList)
+          : (groupMember.value = data?.memberList)
+      } else {
+        showName.value ? (searchMember.value = []) : (groupMember.value = [])
+      }
+    }
+
+    loading.value = false
+  } catch (err) {
+  } finally {
+    loading.value = false
+  }
+}
+
+// 是否是普通成员
+const isRankAndFiler = (role) => {
+  return role == 1 || role == 2 ? true : false
+}
+
+definePageMeta({
+  layout: false
+})
+
+useSeoMeta({
+  title: '群聊'
+})
+</script>
+<style lang="scss" scoped></style>

+ 359 - 0
src/pages/chat/group-chat.vue

@@ -0,0 +1,359 @@
+<template>
+  <div class="single-page h-full w-full" style="height: 100vh">
+    <van-nav-bar @click-left="router.back()" @click-right="onClickRight">
+      <template #left>
+        <div>
+          <van-icon name="arrow-left" color="black" size="18" />
+        </div>
+      </template>
+      <template #title>
+        <div class="text-2xl text-black-3 text-semibold flex items-center">
+          <div style="min-width: 50px; max-width: 200px" class="inline-block line-clamp-1">
+            {{ groupInfo?.groupName }}
+          </div>
+          <span
+            v-if="groupMemberInfo.isNotDisturb"
+            class="ml-7 iconfont icon-close-remind text-black-9"
+            style="font-size: 16px"
+          ></span>
+        </div>
+      </template>
+      <template #right>
+        <van-icon name="ellipsis" color="black" size="18" />
+      </template>
+    </van-nav-bar>
+
+    <van-notice-bar
+      v-if="groupInfo?.groupNotice?.messageContent"
+      left-icon="volume-o"
+      mode="link"
+      :text="groupInfo?.groupNotice?.messageContent"
+    ></van-notice-bar>
+
+    <template v-if="showPage">
+      <van-pull-refresh v-model="refreshing" @refresh="loadMore" class="flex-1">
+        <van-list
+          ref="chatListRef"
+          class="h-full overflow-y-auto px-12 flex flex-col"
+          :finished="true"
+          finished-text=""
+        >
+          <template v-for="(message, index) in currConversationChatList" :key="index">
+            <ChatMessage :show-name="true" :message="message"></ChatMessage>
+          </template>
+        </van-list>
+      </van-pull-refresh>
+      <div class="h-70 w-full bg-[#fff]"></div>
+      <div class="fixed bottom-0 left-0 right-0 w-full bg-[#fff]">
+        <ChatInput
+          :operates="['image', 'share-group']"
+          @focus="scrollToBottom"
+          @send="handleSendMessage"
+        ></ChatInput>
+      </div>
+    </template>
+    <template v-else>
+      <div class="flex-1 grid place-items-center text-black-9">
+        <div v-if="pageLoading">创建会话中...</div>
+        <div v-else class="grid place-items-center">
+          <div v-if="groupId">创建成功</div>
+          <div v-else class="grid place-items-center">
+            <div class="mb-10">创建会话失败</div>
+            <van-button size="small" @click="initGroupId">点击重试</van-button>
+          </div>
+        </div>
+      </div>
+    </template>
+  </div>
+</template>
+<script setup>
+import ChatMessage from './components/chat-message'
+import ChatInput from './components/chat-input'
+import { findHyperlinks } from '~/pages/chat/components/chat-message/link-message/handle'
+import {SocketEventsBus, XYWebSocket} from '~/utils/XYWebSocket'
+import { isValidJson } from '~/utils'
+
+const route = useRoute()
+const router = useRouter()
+
+const chatsStore = useChatsStore()
+const userInfoStore = useUserInfoStore()
+
+const { userInfo } = storeToRefs(userInfoStore)
+// 群聊的标题
+const groupInfo = ref({})
+// 每个成员的信息
+const groupMemberInfo = ref({})
+
+// 聊天列表
+const chatListRef = ref(null)
+
+const pageLoading = ref(true)
+const getUserId = ref(null) // 消息接收者的用户id
+const sendUserId = computed(() => userInfo?.value.pass) // 消息发送者:当前登录用户的加密id
+const groupId = ref(route.query?.groupId) // 会话ID
+const showPage = computed(() => groupId.value)
+
+const initGroupId = async () => {
+  try {
+    if (!groupId.value) return
+    await getAnnouncement()
+    await getChatList('init')
+  } catch (e) {
+  } finally {
+    pageLoading.value = false
+  }
+}
+
+let pageNum = ref(0)
+let totalCount = ref(0)
+const currConversationChatList = ref([])
+/**
+ * 加载当前聊天信息
+ * @param type init|more
+ * @returns {Promise<void>}
+ */
+const getChatList = async (type = 'init') => {
+  try {
+    let lastMessageId = null
+    let page = 1
+    if (type === 'more') {
+      lastMessageId = getLastMessageId()
+      page = pageNum.value + 1
+    }
+    const res = await chatsStore.getChatHistory({
+      pageNum: page,
+      pageSize: 50,
+      groupId: groupId.value,
+      messageId: type === 'more' ? lastMessageId : null
+    })
+    pageNum.value = page
+    await handleResponse(res)
+    let resList = chatsStore.handleMessageList(res.data?.data)
+    totalCount.value = res.data.count
+
+    if (type === 'more') {
+      // 滚动条位置
+      resList = resList.filter((o) => o.messageId !== lastMessageId)
+      currConversationChatList.value = [...resList, ...currConversationChatList.value]
+    } else {
+      currConversationChatList.value = resList
+      await scrollToBottom()
+    }
+  } catch (e) {
+    console.error(e)
+  } finally {
+  }
+}
+
+const getLastMessageId = () => {
+  const len = currConversationChatList.value.length
+  if (len) {
+    return currConversationChatList.value[0]?.messageId ?? null
+  }
+  return null
+}
+
+// 发送文本消息
+const sendTextMessage = async (text) => {
+  try {
+    console.log(text, 'sendTextMessage')
+    if (!text) return
+    let msg = {
+      groupId: groupId.value,
+      getUserId: getUserId.value,
+      sendUserId: sendUserId.value,
+      specialUserId: '',
+      messageContent: text,
+      messageType: 0,
+      noticeType: 2,
+      object: {
+        id: getLocalId(),
+        // TODO 聊天时候改了头像昵称 会出现找不到的情况
+        headImageUrl: userInfo?.value.headImageUrl,
+        showName: userInfo?.value.showName
+      }
+    }
+    const isLink = !!findHyperlinks(text)
+    if (isLink) msg.messageType = 4
+    currConversationChatList.value.push(msg)
+    await scrollToBottom()
+    const res = await chatsStore.sendSocketMessage(msg)
+    console.log('luck:', res)
+  } catch (e) {
+    console.log(e, '2')
+  } finally {
+  }
+}
+
+// 选择发送图片
+const sendImageMessage = async (file) => {
+  try {
+    const formData = new FormData()
+    formData.append('uploadFile', file)
+    formData.append('asImage', true)
+    formData.append('fieldName', 'messageContent')
+    const { data } = await request('/website/tourMessage/upload', {
+      method: 'post',
+      body: formData
+    })
+
+    let msg = {
+      groupId: groupId.value,
+      getUserId: getUserId.value,
+      sendUserId: sendUserId.value,
+      specialUserId: '',
+      messageContent: data.fileUrl,
+      messageType: 1,
+      noticeType: 2,
+      object: {
+        id: getLocalId(),
+        // TODO 聊天时候改了头像昵称 会出现找不到的情况
+        headImageUrl: userInfo?.value.headImageUrl,
+        showName: userInfo?.value.showName
+      }
+    }
+    currConversationChatList.value.push(msg)
+    await scrollToBottom()
+    await chatsStore.sendSocketMessage(msg)
+  } catch (e) {
+    console.error(e, '??')
+  } finally {
+  }
+}
+
+const handleSendMessage = async ({ type, messageContent }) => {
+  try {
+    switch (type) {
+      case 'text':
+        await sendTextMessage(messageContent)
+        break
+      case 'image':
+        await sendImageMessage(messageContent)
+        break
+
+      default:
+        break
+    }
+  } catch (e) {
+  } finally {
+  }
+}
+
+const scrollToBottom = async () => {
+  setTimeout(async () => {
+    await nextTick() // 确保DOM已经更新
+    const listElement = chatListRef.value?.$el
+    if (listElement) {
+      const scrollContainer = listElement
+      scrollContainer.scrollTop = scrollContainer.scrollHeight
+    }
+  }, 200)
+}
+
+// 加载更多
+const refreshing = ref(false)
+const loadMore = async () => {
+  try {
+    refreshing.value = true
+    if (currConversationChatList.value.length) {
+      if (totalCount.value === currConversationChatList.value.length) {
+        // 已经加载了全部
+      } else {
+        await getChatList('more')
+      }
+    } else {
+      await getChatList('init')
+    }
+  } catch (e) {
+  } finally {
+    refreshing.value = false
+  }
+}
+
+const onClickRight = () => {
+  groupId.value &&
+    navigateTo({
+      path: '/chat/set',
+      query: { groupId: groupId.value }
+    })
+}
+
+// 本地生成一个唯一消息id
+function getLocalId() {
+  const random = Math.floor(Math.random() * 10000)
+  return Date.now() + '' + random
+}
+
+onMounted(() => {
+  initGroupId()
+
+    XYWebSocket.SocketEventsBus.on('chat-event', async (chat) => {
+      console.log('订阅群聊消息', chat)
+      const isCurrGroupId = chat.groupId && chat.groupId === groupId.value
+      const isOtherUserMessage = chat.sendUserId && chatsStore.isRealMessage(chat.sendUserId)
+      if (isCurrGroupId) {
+        if (isOtherUserMessage) {
+          currConversationChatList.value.push(chat)
+          await scrollToBottom()
+        }
+        if (!isOtherUserMessage) {
+          await getChatList('init')
+        }
+      }
+    })
+
+})
+
+// 查寻群公告
+async function getAnnouncement() {
+  let { data } = await request('/website/tourGroup/getGroupInfoAndMemberByGroupId', {
+    query: { groupId: groupId.value }
+  })
+  console.log(data.memberList)
+
+  if (data) {
+    groupInfo.value = data
+    if (Array.isArray(data.memberList) && data.memberList?.lenght) {
+      data.memberList.map((el) => {
+        if (el.userId == userInfo.value.userId) {
+          groupMemberInfo.value = el
+        }
+      })
+    }
+  }
+}
+
+// 用户删除消息
+const delMessage = (messageId) => {
+  showConfirmDialog({
+    width: 260,
+    message: '是否删除这条消息?',
+    confirmButtonColor: '#FF9300'
+  })
+    .then(async () => {
+      const res = await request('/website/tourMessage/delMessage', {
+        method: 'post',
+        body: {
+          messageId: [messageId]
+        }
+      })
+
+      if (res && res?.success) {
+      }
+    })
+    .catch(() => {})
+}
+
+definePageMeta({
+  layout: false
+})
+</script>
+<style lang="scss" scoped>
+.single-page {
+  height: 100vh;
+  display: flex;
+  flex-direction: column;
+  min-height: 0;
+}
+</style>

+ 228 - 0
src/pages/chat/group-member.vue

@@ -0,0 +1,228 @@
+<template>
+  <div class="w-full h-[100vh]">
+    <ChatHeaderBar title="移除群成员" />
+
+    <ChatSearch v-model:searchString="showName" @search="search" placeholder="请输入关键词" />
+
+    <van-pull-refresh v-model="refreshing" @refresh="onRefresh">
+      <ChatEmpty
+        image="search"
+        v-if="!addDataList?.length && !loading"
+        title="暂无数据"
+        top="100"
+      />
+
+      <van-list
+        v-model:loading="loading"
+        error-text="获取失败"
+        finished-text="-- 没有更多了 --"
+        :finished="finished"
+        :immediate-check="false"
+      >
+        <div style="height: calc(100vh - 170px)">
+          <van-checkbox-group v-model="checked">
+            <template v-for="item in addDataList" :key="item.userId">
+              <van-cell v-if="item.groupRole != 1" center clickable @click.stop="toggle(item)">
+                <template #icon>
+                  <div class="flex justify-start">
+                    <van-checkbox
+                      checked-color="#FD9A00"
+                      :name="item?.userId"
+                      :ref="(el) => (checkboxRefs[item?.userId] = el)"
+                      @click.stop="toggle(item)"
+                    />
+
+                    <div class="w-40 h-40 ml-13 mr-12 rounded-full overflow-hidden">
+                      <img
+                        v-if="item?.headImageUrl"
+                        class="w-full h-full shrink-0 object-cover"
+                        :src="item?.headImageUrl"
+                        alt=""
+                      />
+
+                      <img
+                        class="w-full h-full shrink-0 object-cover"
+                        src="~/assets/img/default_avatar.png"
+                        alt=""
+                      />
+                    </div>
+                  </div>
+                </template>
+                <template #title>
+                  <div class="flex items-center">
+                    <h1 class="text-xl text-black-3">
+                      {{ item?.showName }}
+                    </h1>
+                  </div>
+                </template>
+              </van-cell>
+            </template>
+          </van-checkbox-group>
+        </div>
+      </van-list>
+    </van-pull-refresh>
+
+    <div
+      class="w-full box-border p-16 pb-40 bg-white fixed bottom-0 left-0 flex justify-between items-center shadow-[0px_-4px_4px_0px_rgba(0,0,0,0.1)]"
+    >
+      <div class="shrink-0 flex justify-start items-center">
+        <div class="w-118 shrink-0 flex justify-start items-center overflow-hidden">
+          <div
+            v-for="(item, index) in checkedList.slice(0, 5)"
+            :key="item?.id + 'avatar'"
+            :class="`w-36 h-36  ${index == 0 ? '' : '-ml-16'} shrink-0 rounded-full overflow-hidden`"
+          >
+            <img
+              v-if="item?.headImageUrl"
+              class="w-full h-full object-cover"
+              :src="item?.headImageUrl"
+              alt=""
+            />
+            <img
+              v-else
+              class="w-full h-full shrink-0 object-cover"
+              src="~/assets/img/default_avatar.png"
+              alt=""
+            />
+          </div>
+        </div>
+
+        <div v-if="checkedList.length > 5" class="shrink-0 w-24 h-24 ml-8">
+          <img class="w-full h-full object-cover" src="~/assets/img/chat/ellipsis.svg" alt="" />
+        </div>
+      </div>
+      <van-button
+        :disabled="checkedList.length > 0 ? false : true"
+        @click="handleRemove"
+        style="width: 160px"
+        class="shrink-0"
+        block
+        size="large"
+        color="#FD9A00"
+        round
+      >
+        完成
+        <span v-if="checkedList.length">({{ checkedList.length }})</span>
+      </van-button>
+    </div>
+  </div>
+</template>
+<script setup>
+const route = useRoute()
+
+const refreshing = ref(false)
+const loading = ref(false)
+const finished = ref(false)
+// 被选中成员
+const checked = ref([])
+const showName = ref('')
+const checkboxRefs = ref([])
+const checkedList = ref([])
+const checkedId = ref([])
+
+const addDataList = ref([])
+const filterDataList = ref([])
+
+onMounted(() => {
+  getList()
+})
+
+// 搜索
+const search = () => {
+  finished.value = true
+  if (showName.value) {
+    addDataList.value = filterDataList.value.filter((item) =>
+      item.showName.includes(showName.value)
+    )
+  } else {
+    addDataList.value = filterDataList.value
+  }
+  finished.value = false
+}
+
+const toggle = (item) => {
+  // let index2 = checked.value.findIndex((el) => el?.id == item.id)
+  let index = checkedList.value.findIndex((el) => el?.userId == item?.userId)
+
+  // if (index2 != -1) {
+  //   checkedId.value.splice(index2, 1)
+  // } else {
+  //   checkedId.value.push(id)
+  // }
+  if (index != -1) {
+    checkedList.value.splice(index, 1)
+  } else {
+    checkedList.value.push(item)
+  }
+  console.log(checked.value, 'checkedId.value')
+  checkboxRefs.value[item.userId].toggle()
+}
+
+// 获取群设置的配置信息
+const getList = async () => {
+  try {
+    loading.value = true
+    let { data } = await request('/website/tourGroup/getGroupInfoAndMemberByGroupId', {
+      query: {
+        groupId: route.query.groupId
+      }
+    })
+    console.log(data?.memberList, '555')
+
+    if (Array.isArray(data?.memberList) && data?.memberList?.length) {
+      addDataList.value = data?.memberList
+      console.log(addDataList.value, '55')
+
+      filterDataList.value = data?.memberList
+    } else {
+      filterDataList.value = []
+      addDataList.value = []
+    }
+
+    loading.value = false
+    refreshing.value = false
+    finished.value = false
+  } catch (err) {
+  } finally {
+    loading.value = false
+    refreshing.value = false
+  }
+}
+
+// 刷新
+const onRefresh = () => {
+  refreshing.value = true
+  queryParams.pageNum = 1
+  addDataList.value = []
+  filterDataList.value = []
+  refreshing.value = false
+}
+
+// 移除群成员
+const handleRemove = async () => {
+  const res = await request('/website/tourMember/removeMember', {
+    method: 'post',
+    body: {
+      delUser: checked.value,
+      groupId: route.query.groupId
+    }
+  })
+
+  if (res && res?.success) {
+    navigateTo({
+      path: '/chat/set',
+      query: route.query.groupId,
+      replace: true
+    })
+  }
+}
+
+definePageMeta({
+  layout: false
+})
+
+useSeoMeta({
+  title: '群聊'
+})
+</script>
+<style lang="scss" scoped></style>

+ 332 - 0
src/pages/chat/group-square.vue

@@ -0,0 +1,332 @@
+<template>
+  <div class="w-full h-full">
+    <ChatHeaderBar title="群聊广场" />
+
+    <ChatSearch
+      placeholder="搜索你想找的群聊"
+      v-model:searchString="groupName"
+      image="search"
+      @search="search"
+    ></ChatSearch>
+
+    <van-pull-refresh v-model="refreshing" @refresh="onRefresh">
+      <div class="h-[80vh]">
+        <van-tabs
+          class="w-full pl-0"
+          title-active-color="#FF9300"
+          title-inactive-color="#333333"
+          v-model:active="active"
+          @click-tab="onClickTab"
+          style="--van-tabs-bottom-bar-color: #ff9300; --van-tabs-bottom-bar-width: 16px"
+          swipeable
+        >
+          <van-tab
+            class="border-t-[1px]"
+            v-for="(el, i) in squareTabList"
+            :key="el.id"
+            :title="el?.typeName"
+          >
+            <div class="w-full h-full pl-16 box-border">
+              <Empty v-if="!groupSquareList?.length && !loading" title="暂无群聊" top="100" />
+              <!-- v-model:loading="loading" -->
+              <!-- <van-list
+                v-else
+                v-model:loading="loading"
+                error-text="获取失败"
+                finished-text=""
+                :finished="finished"
+                :immediate-check="false"
+                @load="getLoadList"
+              > -->
+              <template v-if="queryParams.groupTypeId == el.id">
+                <van-cell
+                  v-for="(item, index) in groupSquareList"
+                  :key="item.id"
+                  style="--van-cell-horizontal-padding: 0; padding: 16px 16px 16px 0"
+                  center
+                  class="border-b-[1px] py-16 pl-0"
+                >
+                  <template #icon>
+                    <div style="" class="h-48 w-48 mr-12 overflow-hidden">
+                      <!-- <MultiHeader v-if="item?.userList" :size="48" :img-urls="[item?.userList]" />
+                      <MultiHeader v-else :size="48" :img-urls="[defaultAvatar]" /> -->
+                    </div>
+                  </template>
+                  <template #title>
+                    <div class="flex justify-start font-semibold text-xl text-black-3 mb-4">
+                      <h1 class="shrink-0 max-w-180 line-clamp-1">
+                        {{ item.groupName }}
+                      </h1>
+                      <span v-if="item?.memberCount" class="shrink-0">
+                        ({{ item.memberCount }})
+                      </span>
+                    </div>
+
+                    <van-tag
+                      style="--van-tag-padding: 4px; --van-tag-radius: 6px"
+                      color="#FEF4E6"
+                      text-color="#FF9300"
+                      type="primary"
+                    >
+                      {{ item?.belongTypeIdDictMap?.name }}
+                    </van-tag>
+                  </template>
+                  <template #label>
+                    <p class="w-212 line-clamp-1 text-black-6 text-base mt-5">
+                      {{ item?.description }}
+                    </p>
+                  </template>
+                  <template #value>
+                    <van-button
+                      size="small"
+                      :color="chageState(item.codeShowStatus).color"
+                      round
+                      class="w-60 text-base font-semibold"
+                      plain
+                      style="--van-button-default-padding: 0"
+                      @click="handleJoinGroup(item)"
+                    >
+                      {{ chageState(item.codeShowStatus).text }}
+                    </van-button>
+                  </template>
+                </van-cell>
+              </template>
+              <!-- </van-list> -->
+            </div>
+          </van-tab>
+        </van-tabs>
+      </div>
+    </van-pull-refresh>
+
+    <div class="fixed w-full p-16 pb-40 bottom-0 left-0 bg-white">
+      <van-button
+        size="large"
+        style="background: #fa8446; margin-top: 30px"
+        color="#fff"
+        round
+        block
+        icon="plus"
+        class="w-full"
+        @click="navigateTo('/chat/create-group')"
+      >
+        创建群聊
+      </van-button>
+    </div>
+  </div>
+</template>
+
+<script setup>
+const userInfoStore = useUserInfoStore()
+const { userInfo } = storeToRefs(userInfoStore)
+import defaultAvatar from '~/assets/img/default_avatar.png'
+definePageMeta({
+  layout: false
+})
+
+const groupName = ref('')
+const refreshing = ref(false)
+const loading = ref(false)
+const finished = ref(false)
+
+const groupSquareList = ref([])
+const squareTabList = ref([])
+
+const active = ref(0)
+
+const queryParams = reactive({
+  pageNum: 1,
+  pageSize: 10,
+  groupName: '',
+  groupTypeId: ''
+})
+
+// 获取切换的数据
+const onClickTab = ({ title }) => {
+  queryParams.groupTypeId = squareTabList.value.find((item) => item.typeName == title).id
+}
+
+// 加入群聊
+const handleJoinGroup = (item) => {
+  if (item.codeShowStatus == 0) {
+    console.log(item.id, '222')
+
+    getGroupAdd(item.id)
+  }
+  if (item.codeShowStatus == 1) {
+    navigateTo({
+      path: '/chat/group-chat',
+      query: { groupId: item.id },
+      replace: true
+    })
+  }
+  if (item.codeShowStatus == 2) {
+    navigateTo('/chat/group-chat', {
+      query: { groupId: item.id },
+      replace: true
+    })
+  }
+}
+
+// 获取群聊列表
+function getLoadList() {
+  squareTabList.value = []
+  queryParams.pageNum++
+  // finished.value = true
+  getList()
+}
+
+const getTabList = async () => {
+  try {
+    let url = `/website/tourGroup/getGroupTypeFirst`
+
+    const { data } = await request(url)
+
+    if (Array.isArray(data) && data?.length) {
+      squareTabList.value = [
+        {
+          typeName: '推荐',
+          id: ''
+        },
+        ...data
+      ]
+      getList()
+    } else {
+      groupSquareList.value = []
+    }
+  } catch (err) {
+  } finally {
+  }
+}
+
+function chageState(state) {
+  let item = {}
+  if (state == 0) {
+    item.text = '加入'
+    item.color = '#FF9300'
+  }
+
+  if (state == 1) {
+    item.text = '去聊天'
+    item.color = '#D9D9D9'
+  }
+
+  // if (state == 2) {
+  //   item.text = '此群异常,暂不能加入'
+
+  //   item.fn = () => {}
+  // }
+  // if (state == 3) {
+  //   item.text = '此群已解散'
+  // }
+  if (state == 4) {
+    item.text = '已申请'
+    item.color = '#D9D9D9'
+  }
+
+  return item
+}
+
+// 获取数据
+const getList = async () => {
+  try {
+    loading.value = true
+    let {
+      data: { dataList, totalCount }
+    } = await request('/website/tourGroup/list', {
+      query: {
+        ...queryParams
+      }
+    })
+
+    dataList.map((el) => {
+      console.log(el?.userList, 'userList')
+
+      if (Array.isArray(el?.userList) && el?.userList?.length > 0) {
+        console.log(el?.userList, 'eluserList')
+        el = {
+          ...el,
+          userList: [...changeHeadImage(el?.userList)]
+        }
+      } else {
+        el = {
+          ...el,
+          userList: [defaultAvatar]
+          // userList:
+        }
+      }
+
+      return el
+    })
+    console.log(dataList, '555')
+    if (Array.isArray(dataList) && dataList?.length) {
+      groupSquareList.value = groupSquareList.value.concat(dataList)
+    } else {
+      groupSquareList.value = []
+    }
+
+    loading.value = false
+    refreshing.value = false
+    // if (groupSquareList.value.length >= totalCount) {
+    //   finished.value = true
+    // } else {
+    finished.value = false
+    // }
+  } catch (err) {
+  } finally {
+    refreshing.value = false
+    loading.value = false
+  }
+}
+
+const changeHeadImage = (headerList) => {
+  let headImageList = headerList
+    .slice(0, 9)
+    .map((el) => (el?.headImageUrl ? el?.headImageUrl : defaultAvatar))
+
+  return headImageList
+}
+
+// 加群聊
+const getGroupAdd = async (groupId) => {
+  let { data } = await request('/website/tourMember/invite', {
+    method: 'post',
+    body: {
+      groupId,
+      ids: [userInfo.value.userId]
+    }
+  })
+  if (data) {
+    getList()
+  }
+}
+onMounted(() => {
+  userInfoStore.getUserInfo()
+  getTabList()
+})
+
+// 搜索
+const search = () => {
+  queryParams.pageNum = 1
+  groupSquareList.value = []
+  queryParams.groupName = groupName.value
+  getList()
+}
+
+// 下拉刷新
+const onRefresh = () => {
+  refreshing.value = true
+  queryParams.pageNum = 1
+  groupSquareList.value = []
+  getList()
+}
+
+useSeoMeta({
+  title: '群聊广场'
+})
+</script>
+
+<style lang="scss" scoped>
+::v-deep .van-tabs__nav--line.van-tabs__nav--complete {
+  padding-left: 5px;
+}
+</style>

+ 100 - 0
src/pages/chat/group.vue

@@ -0,0 +1,100 @@
+<template>
+  <div>
+    <!--  left-text="" title="群聊" -->
+    <van-nav-bar fixed title="群聊" @click-left="onClickLeft" @click-right="onClickRight">
+      <template #left>
+        <div>
+          <van-icon name="arrow-left" color="black" size="18" />
+        </div>
+      </template>
+
+      <template #right>
+        <van-icon name="ellipsis" color="black" size="18" />
+      </template>
+    </van-nav-bar>
+    <div class="w-full fixed top-48 left-0">
+      <van-notice-bar
+        left-icon="volume-o"
+        mode="link"
+        text="无论我们能活多久,我们能够享受的只有无法分割的此刻,此外别无其他。"
+      ></van-notice-bar>
+    </div>
+
+    <van-pull-refresh v-model="loading" @refresh="onRefresh">
+      <div
+        style="height: calc(100vh - 50px)"
+        :class="`w-full px-12 pt-60 border ${bg ? `bg-[url(${bg})] bg-cover` : 'bg-[#F3F3F3]'} `"
+      >
+        <ChatItem></ChatItem>
+      </div>
+    </van-pull-refresh>
+
+    <ProfileNewsChatInput></ProfileNewsChatInput>
+  </div>
+</template>
+<script setup>
+const route = useRoute()
+const router = useRouter()
+const userInfoStore = useUserInfoStore()
+const { userInfo } = storeToRefs(userInfoStore)
+
+definePageMeta({
+  layout: false
+})
+
+useSeoMeta({
+  title: '群聊'
+})
+
+// 刷新次数
+const count = ref(0)
+const loading = ref(false)
+const idInfo = reactive({
+  groupId: computed(() => route.query.groupId ?? '')
+})
+
+// 背景图
+const bg = ref('')
+
+const messageData = reactive({
+  pageNum: 1,
+  pageSize: 10,
+  messageId: '',
+  searchMessage: ''
+})
+
+// 刷新
+const onRefresh = () => {
+  setTimeout(() => {
+    showToast('刷新成功')
+    loading.value = false
+    count.value++
+  }, 1000)
+}
+
+const onClickLeft = () => router.back()
+
+const onClickRight = () => {
+  navigateTo({
+    path: '/chat/set',
+    query: idInfo
+  })
+}
+
+// 获取群聊消息
+async function getListMessage() {
+  let { data } = await request('/website/tourMessage/getMessageByGroupId', {})
+}
+
+//
+// 用户删除消息
+async function userDelMessage(params) {
+  let { data } = await request('/website/tourMessage/delMessage', {
+    method: 'post',
+    body: {}
+  })
+}
+
+onMounted(() => {})
+</script>
+<style lang="scss" scoped></style>

+ 91 - 0
src/pages/chat/qr-code.vue

@@ -0,0 +1,91 @@
+<template>
+  <div class="w-full h-[100vh]">
+    <ChatHeaderBar title="群聊二维码" />
+    <div class="h-100"></div>
+
+    <div class="w-60 h-60 mx-auto rounded-full border overflow-hidden">
+      <img
+        class="w-full h-full object-cover"
+        v-if="queueParmars?.groupAvatar"
+        :src="queueParmars.groupAvatar"
+        alt=""
+      />
+      <img class="w-full h-full object-cover" v-else src="~/assets/img/default_avatar.png" alt="" />
+    </div>
+    <!-- :list="queueParmars?.groupAvatar"  -->
+
+    <!-- <ChatGroupAvatar class="mx-auto"></ChatGroupAvatar> -->
+
+    <h1 title="" class="w-300 mt-16 mb-18 text-center mx-auto text-xl text-black-3 font-semibold">
+      群聊:{{ queueParmars?.groupName }}
+    </h1>
+
+    <div class="relative mb-21 w-220 h-220 bg-white rounded-lg mx-auto box-border">
+      <img class="h-full w-full" src="~/assets/img/chat/qr-code-box.png" alt="" />
+      <img
+        class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-170 h-170 shrink-0 object-cover"
+        :src="qrCode"
+        alt=""
+      />
+    </div>
+
+    <p class="w-full text-center text-sm text-black-6">该二维码7天内有效,重新进入将更新</p>
+  </div>
+</template>
+
+<script setup>
+const route = useRoute()
+definePageMeta({
+  layout: false
+})
+
+onMounted(() => {
+  getQrCode()
+})
+
+// getGroupQR
+const QRURI = ref(`${import.meta.env.VITE_APP_BASE_URL}website/tourGroup/getGroupQR`)
+const qrCode = ref('')
+const queueParmars = reactive({
+  groupId: computed(() => route.query?.groupId ?? ''),
+  groupName: computed(() => route.query?.groupName ?? ''),
+  groupAvatar: computed(() => route.query?.groupAvatar ?? '')
+})
+
+useSeoMeta({
+  title: '群聊'
+})
+
+// 转换时间
+function convertTimeStamp(timestamp) {
+  // 将时间戳转换为日期对象
+  let date = new Date(timestamp)
+
+  // 增加7天
+  date.setDate(date.getDate() + 7)
+
+  // 获取新的月份和日期
+  let month = date.getMonth() + 1 // getMonth() 返回的月份是 0-11,所以需要加 1
+  let day = date.getDate() // 获取日期
+
+  // 格式化为 "m-d" 格式
+  let formattedDate = `${month}月${day < 10 ? '0' + day : day}日`
+  return formattedDate
+}
+
+function getQrCode() {
+  try {
+    showLoadingToast({
+      message: '加载中...',
+      duration: 100000
+    })
+    qrCode.value = QRURI.value + `?groupId=${queueParmars.groupId}&systemOs=1`
+
+    closeToast()
+  } catch (error) {
+  } finally {
+    closeToast()
+  }
+}
+</script>
+<style lang="scss" scoped></style>

+ 339 - 0
src/pages/chat/qr-results.vue

@@ -0,0 +1,339 @@
+<template>
+  <div class="w-full h-[100vh]">
+    <ChatHeaderBar title="扫码结果" />
+    <div class="h-250"></div>
+
+    <template v-if="overTime && successFlag">
+      <div class="mx-auto flex justify-center">
+        <MultiHeader :size="60" :imgUrls="imgUrls" />
+      </div>
+      <h1 class="w-220 text-center text-black-3 font-semibold text-xl mb-27 mt-16 mx-auto">
+        {{ itemData?.groupName }}
+      </h1>
+      <!-- @click="
+         navigateTo({
+           path: '/',
+           query: {
+             groupId: leaderId
+           }
+         })
+      " -->
+      <!-- <van-cell center is-link size="large" :title="itemData?.showName" value="查看群主主页">
+      <template #icon>
+        <div class="w-48 h-48 rounded-full overflow-hidden border mr-12">
+          <img
+            class="w-full h-full shrink-0 object-cover"
+            src="~/assets/img/chat/user.svg"
+            alt=""
+          />
+        </div>
+      </template>
+    </van-cell> -->
+      <!-- <van-cell
+      center
+      size="large"
+      title="群介绍"
+      label="44444444444444444444444444444444"
+    ></van-cell>
+
+    <van-cell
+      center
+      size="large"
+      label="为维护逍遥游良好社区氛围,请遵守《逍遥游社区规范》如群聊疑似违规,可点击举报群聊"
+    ></van-cell> -->
+
+      <div class="fixed bottom-0 left-0 w-full box-border pb-40 pt-16 bg-white px-16">
+        <!-- <van-button
+        v-if="loading"
+        round
+        type="primary"
+        block
+        :color="colorList[itemData?.codeShowStatus]"
+        @click="chageState(itemData?.codeShowStatus)?.fn"
+      >
+        {{ showIsExist ? '立即聊天' : '加入' }}
+        {{ chageState(itemData?.codeShowStatus)?.text }}
+      </van-button> -->
+
+        <van-button
+          v-model:loading="loading"
+          round
+          type="primary"
+          block
+          :color="buttonColor"
+          @click="handleQrcode"
+        >
+          {{ buttonText }}
+        </van-button>
+
+        <!-- <van-button
+        v-if="itemData?.codeShowStatus == 1"
+        round
+        type="primary"
+        block
+        color="#FF9300"
+        @click="
+          navigateTo({
+            path: '/chat/group-chat',
+            query: {
+              groupId: itemData.value.id
+            },
+            replace: true
+          })
+        "
+      >
+        立即聊天
+      </van-button>
+
+      <template v-if="itemData?.codeShowStatus == 0">
+        <van-button round type="primary" block color="#FF9300" @click="handleQrcode">
+          加入
+        </van-button>
+      </template>
+      <van-button
+        v-if="itemData?.codeShowStatus == 4"
+        round
+        type="primary"
+        block
+        color="#D9D9D9"
+        @click=""
+      >
+        已申请
+      </van-button>
+
+      <van-button
+        v-if="itemData?.codeShowStatus == 2"
+        round
+        type="primary"
+        block
+        color="#D9D9D9"
+        @click=""
+      >
+        此群异常,暂不能加入
+      </van-button>
+      <van-button
+        v-if="itemData?.codeShowStatus == 3"
+        round
+        type="primary"
+        block
+        color="#D9D9D9"
+        @click=""
+      >
+        此群已解散
+      </van-button> -->
+      </div>
+    </template>
+
+    <template v-else>
+      <ChatEmpty image="search" :title="'无法识别二维码'" />
+    </template>
+  </div>
+</template>
+<script setup>
+import defaultAvatar from '~/assets/img/default_avatar.png'
+
+const route = useRoute()
+
+const userInfoStore = useUserInfoStore()
+const { userInfo } = storeToRefs(userInfoStore)
+
+const itemData = ref({})
+const imgUrls = ref([])
+const loading = ref(false)
+
+const successFlag = ref(route.query.success) //扫码成功
+//二维码过期与否 true 过期 false 不过期
+
+const overTime = ref(route.query.overTime)
+// 是否存在
+const showIsExist = ref(false)
+
+const buttonText = ref('')
+const buttonColor = ref('#FF9300')
+
+definePageMeta({
+  layout: false
+})
+
+useSeoMeta({
+  title: '我的消息'
+})
+
+onMounted(() => {
+  getGroupInfo()
+})
+
+async function getGroupInfo() {
+  try {
+    loading.value = true
+    let { data } = await request('/website/tourGroup/getGroupInfoAndMemberByGroupId', {
+      query: {
+        groupId: route.query?.groupId
+      }
+    })
+
+    if (typeof data == 'object') {
+      itemData.value = data
+
+      if (Array.isArray(data?.memberList) && data?.memberList?.length) {
+        imgUrls.value = changeHeadImage(data?.memberList)
+      } else {
+        imgUrls.value = [defaultAvatar]
+      }
+
+      if (data.bannedStatus) {
+        if (!overTime.value) {
+          buttonText.value = '二维码已过期'
+          buttonColor.value = '#D9D9D9'
+        } else {
+          if (showIsExist.value) {
+            buttonText.value = '立即聊天'
+            buttonColor.value = '#FF9300'
+          } else {
+            buttonText.value = '加入'
+            buttonColor.value = '#FF9300'
+          }
+        }
+      } else {
+        buttonText.value = '此群已解散'
+        buttonColor.value = '#D9D9D9'
+      }
+
+      loading.value = false
+    }
+  } catch (error) {}
+}
+
+const changeHeadImage = (headerList) => {
+  let headImageList = headerList
+    .slice(0, 9)
+    .map((el) => (el?.headImageUrl ? el?.headImageUrl : defaultAvatar))
+
+  return headImageList
+}
+
+// const colorList = ['#FF9300', '#FF9300', '#D9D9D9', '#D9D9D9', '#D9D9D9']
+
+// // 切换状态
+// function chageState(state) {
+//   let item = {}
+//   if (state == 0) {
+//     item.text = '加入'
+
+//     item.fn = handleQrcode
+//   }
+
+//   if (state == 1) {
+//     item.text = '立即聊天'
+
+//     item.fn = navigateTo({
+//       path: '/chat/group-chat',
+//       query: {
+//         groupId: itemData.value.id
+//       },
+//       replace: true
+//     })
+//   }
+//   if (state == 2) {
+//     item.text = '此群异常,暂不能加入'
+
+//     item.fn = () => {}
+//   }
+//   if (state == 3) {
+//     item.text = '此群已解散'
+
+//     item.fn = () => {}
+//   }
+//   if (state == 4) {
+//     item.text = '已申请'
+
+//     item.fn = () => {}
+//   }
+
+//   return item
+// }
+
+// 申请加入群聊
+const handleQrcode = async () => {
+  if (itemData.value.bannedStatus) {
+    if (showIsExist.value) {
+      navigateTo({
+        path: '/chat/group-chat',
+        query: {
+          groupId: itemData.value.id
+        },
+        replace: true
+      })
+    } else {
+      if (itemData.value.bannedStatus) {
+        if (itemData.value.dataState) {
+        }
+        loading.value = true
+        try {
+          let { data } = await request('/website/tourMember/invite', {
+            method: 'post',
+            body: {
+              groupId: itemData.value.id,
+              ids: [userInfo.value.userId]
+            }
+          })
+
+          if (data) {
+            buttonText.value = '已申请'
+            buttonColor.value = '#D9D9D9'
+            loading.value = false
+          }
+        } catch (error) {
+        } finally {
+          loading.value = false
+        }
+      } else {
+        showDialog({
+          width: 260,
+          title: '提示',
+          message: '该群聊异常,无法加入',
+          confirmButtonColor: '#FF9300'
+        }).then(() => {
+          buttonText.value = '此群异常,暂不能加入'
+          buttonColor.value = '#D9D9D9'
+        })
+      }
+    }
+  } else {
+    return
+  }
+
+  // try {
+  //   let { data } = await request('/website/tourMember/invite', {
+  //     method: 'post',
+  //     body: {
+  //       groupId: itemData.value.id,
+  //       ids: [userInfo.value.userId]
+  //     }
+  //   })
+
+  //   if (data == 3) {
+  //     navigateTo({
+  //       path: '/chat/group-chat',
+  //       query: {
+  //         groupId: itemData.value.id
+  //       },
+  //       replace: true
+  //     })
+  //   }
+  //   if (data == 2) {
+  //     itemData.value.codeShowStatus = 4
+  //   }
+
+  //   if (data == 1) {
+  // showDialog({
+  //   width: 260,
+  //   title: '提示',
+  //   message: '该群聊异常,无法加入',
+  //   confirmButtonColor: '#FF9300'
+  // }).then(() => {})
+  //   }
+  // } catch (error) {}
+}
+</script>
+<style scoped lang="scss"></style>

+ 308 - 0
src/pages/chat/report.vue

@@ -0,0 +1,308 @@
+<template>
+  <div class="w-full h-[100vh]">
+    <ChatHeaderBar title="举报类型" />
+    <div class="h-46"></div>
+
+    <van-form>
+      <van-cell-group>
+        <van-field
+          :rules="[{ required: true, message: '请输入内容' }]"
+          size="large"
+          rows="2"
+          readonly
+          autosize
+          type="textarea"
+          v-model="form.elseTypeReason"
+          :placeholder="
+            form.elseTypeReason
+              ? form.elseTypeReason
+              : '对话中可能含有血腥、恐怖等内容,或者其他未提及的违规类型。'
+          "
+          label-align="top"
+        >
+          <template #label>
+            <div>
+              举报描述
+              <span class="text-[#ee0a24] p-0">*</span>
+              :
+              <span class="text-[#3369E7]">{{ title }}</span>
+              <span
+                @click.stop="showPopup = true"
+                class="ml-5 iconfont icon-caret-down text-black-0/[0.9]"
+                style="width: 16px"
+              ></span>
+            </div>
+          </template>
+        </van-field>
+        <van-field
+          :rules="[{ required: true, message: '请输入违规描述' }]"
+          required
+          size="large"
+          rows="6"
+          autosize
+          type="textarea"
+          v-model="form.description"
+          label="举报描述"
+          maxlength="200"
+          placeholder="请尽可能的描述存在的问题,如:让您感到不适的画面、或其他未提及的违规内容。"
+          label-align="top"
+        />
+
+        <van-field
+          id="image"
+          class="image"
+          style="background-color: white"
+          required
+          size="large"
+          name="uploader"
+          label="图片证据"
+          label-align="top"
+          :rules="[{ required: true, message: '请至少上传一张图片' }]"
+        >
+          <template #input>
+            <div class="w-full flex justify-start items-start flex-wrap">
+              <template v-if="form.image.length">
+                <div
+                  v-for="(item, index) in form.image"
+                  :key="`img${index}`"
+                  class="shrink-0 relative w-80 h-80 mr-7 rounded-xl overflow-hidden"
+                >
+                  <img class="w-full h-full object-cover" :src="item" alt="" />
+
+                  <div
+                    @click="deleteImage(index)"
+                    class="absolute z-10 top-0 right-0 w-20 rounded-bl-md h-20 bg-black/[0.4] flex justify-center items-center"
+                  >
+                    <span class="icon iconfont text-white" style="font-size: 16px">&#xe7fc;</span>
+                  </div>
+                </div>
+              </template>
+
+              <div
+                @click="handleChangeAvatar"
+                class="border shrink-0 w-80 h-80 rounded-xl bg-[#F3F3F3] flex justify-center flex-wrap items-center"
+              >
+                <div>
+                  <span class="iconfont icon-plus text-black/[0.4]" style="font-size: 18px"></span>
+                  <p class="leading-3xl py-0 w-full text-sm text-center text-black/[0.4]">
+                    {{ form.image?.length }}/3
+                  </p>
+                </div>
+              </div>
+            </div>
+          </template>
+        </van-field>
+      </van-cell-group>
+      <div class="w-full fixed bottom-0 left-0 mt-90 px-16 pt-16 pb-40 box-border">
+        <van-button
+          size="large"
+          round
+          :color="isBtnDisabled ? '#A6A6A6' : '#FF9300'"
+          class="font-semibold"
+          block
+          @click="handleReport"
+          :loading="isSubmiting"
+        >
+          <!--  @click="isBtnDisabled ? handleReport : () => {}" -->
+          提交
+        </van-button>
+      </div>
+    </van-form>
+
+    <van-popup v-model:show="showPopup" destroy-on-close round position="bottom">
+      <van-picker
+        :columns="reportList"
+        :columns-field-names="customFieldName"
+        :model-value="reportIndex"
+        @cancel="showPopup = false"
+        @confirm="onreportFilterClose"
+      />
+    </van-popup>
+  </div>
+</template>
+<script setup>
+const route = useRoute()
+const router = useRouter()
+
+const customFieldName = {
+  text: 'typeName',
+  value: 'id'
+}
+
+definePageMeta({
+  layout: false
+})
+
+const TEXT = '举报类型'
+
+// 刷新次数
+const reportIndex = ref('')
+const loading = ref(false)
+
+const title = ref(TEXT)
+
+const showPopup = ref(false)
+
+const form = reactive({
+  typeId: null,
+  objectType: computed(() => (route?.query?.objectType == 2 ? 2 : 1)),
+  elseTypeReason: null,
+  description: null,
+  image: []
+})
+
+const { open, onChange } = useFileDialog({
+  accept: '.png,.png,.jpeg,.JPG,Png '
+})
+
+function handleChangeAvatar() {
+  open()
+}
+
+onChange(async (files) => {
+  if (!files.length) return
+
+  const formData = new FormData()
+  formData.append('uploadFile', files[0])
+  formData.append('asImage', true)
+  formData.append('fieldName', 'image')
+  const maxSize = 5 * 1024 * 1024 // 10 MB
+  if (form.image.length <= 3) {
+    if (files[0].size > maxSize) {
+      showToast('上传图片过大,请重新上传')
+      return
+    } else {
+      try {
+        showLoadingToast({
+          message: '图片上传中...',
+          duration: 1000000
+        })
+
+        const { data } = await request('/website/tourComplait/upload', {
+          method: 'post',
+          body: formData
+        })
+
+        form.image.push(data.fileUrl)
+        closeToast()
+        showToast('图片上传成功')
+      } catch (error) {
+        form.image.push({
+          url: files[0].name,
+          status: 'failed',
+          isImage: true,
+          message: '上传失败',
+          imageFit: 'contain'
+        })
+        closeToast()
+        showToast('图片上传失败')
+
+        console.log('图片上传失败')
+      }
+    }
+  } else {
+    showToast('最多上传图片数量3张')
+  }
+})
+
+// 删除图片
+const deleteImage = (index) => {
+  form.image = form.image.filter((it, filterIndex) => filterIndex != index)
+}
+
+const reportList = ref([])
+
+// 获取举报类型
+const getreportType = async () => {
+  try {
+    let { data } = await request('/website/tourComplaintType/getTypeList')
+
+    if (Array.isArray(data) && data?.length) {
+      reportIndex.value = [data[0]?.id]
+      form.typeId = data[0]?.id
+      title.value = data[0]?.typeName
+      form.elseTypeReason = data[0]?.description
+      reportList.value = data
+    } else {
+      reportList.value = []
+    }
+  } catch (err) {}
+}
+
+// 下拉菜单的方法
+function onreportFilterClose({ selectedValues, selectedOptions }) {
+  showPopup.value = false
+  reportIndex.value = selectedValues
+  let el = selectedOptions[0]
+  title.value = el.typeName
+  if (el.typeName == '其他') {
+    form.elseTypeReason = ''
+  } else {
+    form.elseTypeReason = el.description
+  }
+}
+
+const isBtnDisabled = computed(() => {
+  return !form.elseTypeReason || !form.description || !form.image.length != 0
+})
+
+const isSubmiting = ref(false)
+
+const handleReport = async () => {
+  try {
+    isSubmiting.value = true
+
+    if (form.objectType == 2) {
+      form.groupId = route.query.groupId
+    } else {
+      form.userId = route.query.userId
+    }
+    let { data } = await request('/website/tourComplait/add', {
+      method: 'post',
+      body: form
+    })
+    if (data) {
+      router.back()
+      showSuccessToast('操作成功')
+    }
+    isSubmiting.value = false
+  } catch (err) {
+  } finally {
+    isSubmiting.value = false
+  }
+}
+
+onMounted(() => {
+  getreportType()
+})
+
+useSeoMeta({
+  title: TEXT
+})
+</script>
+<style lang="scss" scoped>
+::v-deep .van-field__body {
+  background-color: #f3f3f3;
+  border-radius: 8px;
+  padding: 12px;
+  padding-bottom: 20px;
+}
+
+::v-deep .image .van-field__value .van-field__body {
+  background-color: #fff !important;
+  padding: 0;
+}
+
+::v-deep .van-field__label {
+  font-weight: 600;
+}
+
+::v-deep .van-field__label--required::after {
+  margin-left: 2px;
+  color: var(--van-field-required-mark-color);
+  content: '*';
+}
+::v-deep .van-field__label--required::before {
+  content: '';
+}
+</style>

+ 335 - 0
src/pages/chat/set-single.vue

@@ -0,0 +1,335 @@
+<template>
+  <div class="w-full h-[100vh] bg-[#F7F8FA]">
+    <ChatHeaderBar title="聊天设置" />
+    <div class="h-66"></div>
+    <van-cell-group
+      style="margin-bottom: 12px; padding-top: 12px; padding-left: 16px"
+      class="box-border"
+      inset
+    >
+      <van-row>
+        <van-col style="width: 54px" span="4" class="mb-12 mr-10">
+          <div class="w-40 h-40 rounded-full mx-auto overflow-hidden mb-4">
+            <img
+              v-if="itemData?.headImage"
+              class="w-full h-full object-cover"
+              :src="itemData?.headImage"
+              alt=""
+            />
+            <img
+              v-else
+              class="w-full h-full object-cover"
+              src="~/assets/img/default_avatar.png"
+              alt=""
+            />
+          </div>
+          <p class="w-full line-clamp-1 lin text-sm text-center text-black-6">
+            {{ itemData?.groupRemark }}
+          </p>
+        </van-col>
+        <van-col style="width: 54px" span="4" class="mb-12 mr-10">
+          <div
+            @click="navigateTo('/chat/single-add')"
+            class="w-40 h-40 flex justify-center items-center bg-[#F3F3F3] rounded-full mx-auto overflow-hidden mb-4"
+          >
+            <span class="iconfont icon-plus text-black-6" style="font-size: 24px"></span>
+          </div>
+          <p class="w-full line-clamp-1 lin text-sm text-center text-black-6">多人聊天</p>
+        </van-col>
+      </van-row>
+    </van-cell-group>
+
+    <van-cell-group style="margin-bottom: 12px" class="box-border" inset>
+      <!-- <van-cell
+        v-for="(item, index) in list"
+        :key="index"
+        size="large"
+        :title="item.title"
+        :is-link="item.isLink"
+        @click="item.fn"
+      >
+        <template #icon>
+          <span
+            :class="`iconfont ${item.icon} text-[#FF9300]  mr-12`"
+            style="font-size: 24px"
+          ></span>
+        </template>
+        <template v-if="item.vModel != null" #right-icon>
+          <van-switch v-model="item.vModel" active-color="#FF9300" inactive-color="#dcdee0" />
+        </template>
+      </van-cell> -->
+
+      <van-cell size="large" title="设置备注名" is-link @click="modifyNoteName"></van-cell>
+    </van-cell-group>
+
+    <van-cell-group style="margin-bottom: 12px" class="box-border" inset>
+      <van-cell size="large" title="查找聊天记录" is-link @click="findChatHistory"></van-cell>
+    </van-cell-group>
+    <van-cell-group style="margin-bottom: 12px" class="box-border" inset>
+      <van-cell :clickable="false" size="large" title="消息面打扰" is-link>
+        <template #right-icon>
+          <van-switch
+            :active-value="1"
+            :inactive-value="0"
+            v-model="isNotDisturb"
+            @click="notDisturb"
+            active-color="#FF9300"
+            inactive-color="#dcdee0"
+          />
+        </template>
+      </van-cell>
+      <van-cell :clickable="false" draggable size="large" title="置顶聊天" is-link>
+        <template #right-icon>
+          <van-switch
+            :active-value="1"
+            :inactive-value="0"
+            v-model="isTop"
+            @click="topChat"
+            active-color="#FF9300"
+            inactive-color="#dcdee0"
+          />
+        </template>
+      </van-cell>
+    </van-cell-group>
+
+    <!-- <van-cell-group style="margin-bottom: 12px" class="box-border" inset>
+      <van-cell size="large" title="设置当前聊天背景" is-link @click=""></van-cell>
+    </van-cell-group> -->
+    <van-cell-group style="margin-bottom: 12px" class="box-border" inset>
+      <van-cell size="large" title="举报该用户" is-link @click="reportUser"></van-cell>
+
+      <van-cell size="large" title="清空聊天记录" is-link @click="clearChatHistory"></van-cell>
+    </van-cell-group>
+    <ChatDialog
+      v-model:show="dialogParmas.show"
+      v-model:title="dialogParmas.title"
+      v-model:confirmText="dialogParmas.confirmText"
+      v-model:cancelText="dialogParmas.cancelText"
+      @confirm="confirm"
+      @cancel="cancel"
+    >
+      <div class="w-full px-12 text-center mt-4">
+        <p class="mx-auto w-[80%] text-sm text-black-9 mb-16">{{ dialogParmas.subTitle }}</p>
+
+        <van-field
+          class=""
+          style="height: 40px; background: #f5f5f5; border-radius: 8px; margin-bottom: 30px"
+          clearable
+          :placeholder="dialogParmas.placeholder"
+          v-model="dialogParmas.remark"
+          maxlength="30"
+        />
+      </div>
+    </ChatDialog>
+  </div>
+</template>
+<script setup>
+const chatStore = useChatStore()
+const { ws, chatList } = storeToRefs(chatStore)
+
+const route = useRoute()
+
+definePageMeta({
+  layout: false
+})
+
+onMounted(() => {
+  chatStore.reqChatList()
+})
+
+const isNotDisturb = ref(0)
+const isTop = ref(0)
+
+const itemData = computed(() => {
+  let item = chatList.value.filter((el) => el.toUserId == route.query.toUserId)[0]
+  isNotDisturb.value = item?.isNotDisturb
+  isTop.value = item?.isTop
+  return item
+})
+
+const dialogParmas = reactive({
+  show: false,
+  title: '',
+  placeholder: '',
+  remark: '',
+  subTitle: ''
+})
+
+// 弹窗确认的事件
+const confirm = async () => {
+  changeGroupName({ remark: dialogParmas.remark })
+}
+
+const cancel = () => {
+  dialogParmas.show = false
+}
+
+// 修改备注名
+const modifyNoteName = () => {
+  dialogParmas.show = true
+  dialogParmas.subTitle = '备注不得超过30个字'
+  dialogParmas.placeholder = '请输入备注'
+
+  if (itemData.value?.groupRemark != '') {
+    dialogParmas.title = '修改备注'
+    dialogParmas.confirmText = '确认'
+    dialogParmas.cancelText = '取消'
+    dialogParmas.remark = itemData.value?.groupRemark
+  } else {
+    dialogParmas.title = '添加备注'
+    dialogParmas.confirmText = '添加'
+    dialogParmas.cancelText = '拒绝'
+  }
+}
+
+// 修改名称
+const changeGroupName = async (body) => {
+  try {
+    const res = await request('/website/tourMember/updateTourMember', {
+      method: 'post',
+      body: {
+        groupId: itemData.value.groupId,
+        ...body
+      }
+    })
+
+    if (res && res?.success) {
+      chatStore.reqChatList()
+      dialogParmas.remark = ''
+      dialogParmas.show = false
+    }
+  } catch (error) {}
+}
+
+// 查找聊天记录
+const findChatHistory = () => {
+  console.log(itemData.value?.groupId, '555')
+
+  navigateTo({
+    path: '/chat/set-sub',
+    query: {
+      objectType: 1,
+      groupId: itemData.value?.groupId
+    }
+  })
+}
+
+// 消息免打扰
+const notDisturb = () => {
+  handleBoolean({ isNotDisturb: isNotDisturb.value })
+}
+
+// 置顶聊天
+const topChat = () => {
+  handleBoolean({ isTop: isTop.value })
+}
+
+// 是否免打扰和 是否置顶 公共
+const handleBoolean = async (params) => {
+  try {
+    let { data } = await request('/website/tourMember/updateSingleTourMember', {
+      method: 'post',
+      body: {
+        groupId: itemData.value.groupId,
+        ...params
+      }
+    })
+    if (data) {
+      if (Object.keys(params)[0] == 'isTop') {
+        isTop.value ? showToast('已置顶') : showToast('置顶取消')
+      }
+      if (Object.keys(params)[0] == 'isNotDisturb') {
+        isNotDisturb.value ? showToast('已开启面打扰') : showToast('已关闭面打扰')
+      }
+    }
+  } catch (error) {}
+}
+
+// 举报该用户
+const reportUser = () => {
+  navigateTo('/chat/report', {
+    query: {
+      objectType: 1,
+      userId: itemData.value?.toUserId
+    }
+  })
+}
+
+// 清空聊天记录
+const clearChatHistory = () => {
+  showConfirmDialog({
+    width: 260,
+    // title: '提示',
+    message: '清空聊天记录',
+    confirmButtonColor: '#FF9300'
+  })
+    .then(async () => {
+      const { data } = await request('/website/tourMessage/clearGroupMessage', {
+        query: {
+          groupId: itemData.value?.groupId
+        }
+      })
+      if (data) return
+    })
+    .catch(() => {})
+}
+
+// const list = reactive([
+//   {
+//     title: '设置备注名',
+//     // icon: setting,
+//     // icon: 'icon-setting-one',
+//     isLink: true,
+//     vModel: null,
+//     fn: modifyNoteName,
+//     value: ''
+//   },
+//   {
+//     title: '查找聊天记录',
+//     value: '',
+//     // icon: 'icon-log',
+//     isLink: true,
+//     vModel: null,
+//     fn: findChatHistory
+//   },
+//   {
+//     title: '消息面打扰',
+//     value: '',
+
+//     // icon: 'icon-close-remind',
+//     isLink: false,
+//     vModel: isNotDisturb.value,
+//     fn: notDisturb
+//   },
+//   {
+//     title: '置顶聊天',
+//     value: '',
+//     // icon: 'icon-set-top',
+//     isLink: false,
+//     vModel: isTop.value,
+//     fn: topChat
+//   },
+//   {
+//     title: '举报该用户',
+//     value: '',
+//     // icon: 'icon-jubaoguanli',
+//     isLink: true,
+//     vModel: null,
+//     fn: reportUser
+//   },
+//   {
+//     title: '清空聊天记录',
+//     value: '',
+//     // icon: 'icon-delete-one',
+//     isLink: true,
+//     vModel: null,
+//     fn: clearChatHistory
+//   }
+// ])
+
+useSeoMeta({
+  title: '我的消息'
+})
+</script>
+
+<style lang="scss" scoped></style>

+ 168 - 0
src/pages/chat/set-sub/index.vue

@@ -0,0 +1,168 @@
+<template>
+  <div class="w-full h-[100vh]">
+    <ChatHeader :title="title" />
+    <ChatSearch
+      v-model:searchString="searchString"
+      v-model:placeholder="placeholder"
+      @search="search"
+    />
+
+    <van-pull-refresh v-model="refreshing" @refresh="onRefresh">
+      <div class="w-full min-h-400 pt-20">
+        <!--  :title="`没有找到&quot;${searchString}&quot;相关的结果`"   v-if="!list?.length && !loading"-->
+        <ChatEmpty
+          v-if="!list?.length && !loading"
+          image="search"
+          title="输入关键词,搜索聊天记录"
+        />
+
+        <template v-if="searchString && list?.length > 0">
+          <van-cell v-for="(item, index) in list" :key="item?.id">
+            <template #icon>
+              <div class="w-40 h-40 rounded-full mr-12 overflow-hidden">
+                <img
+                  v-if="item?.headImageUrl"
+                  class="w-full h-full object-cover"
+                  :src="item?.headImageUrl"
+                  alt=""
+                />
+                <img
+                  v-else
+                  class="w-full h-full object-cover"
+                  src="~/assets/img/default_avatar.png"
+                  alt=""
+                />
+              </div>
+            </template>
+            <template #title>
+              <div class="w-full flex justify-between text-sm text-black-9">
+                <span class="w-150 border line-clamp-1">{{ item?.showName }}</span>
+
+                <span class="text-sm text-black-9">
+                  {{ item?.createTime }}
+                </span>
+              </div>
+            </template>
+            <template #label>
+              <van-highlight
+                :keywords="searchString"
+                :source-string="messageContentParse(item?.messageContent)"
+                class="w-full text-xl text-black-3 line-clamp-2"
+                highlight-class="custom-class"
+              />
+            </template>
+          </van-cell>
+        </template>
+
+        <!-- <template v-if="!searchString && list?.length">
+          <p class="text-bsae text-center">按以下条件查找</p>
+          <div class="w-full flex justify-center mt-15">
+            <van-button
+              icon="contact-o"
+              type="primary"
+              color="#F6F6F6"
+              round
+              @click=""
+              size="small"
+              style="color: #333333; margin-right: 10px"
+            >
+              群成员
+            </van-button>
+            <van-button
+              icon="notes-o"
+              type="primary"
+              color="#F6F6F6"
+              color-text="#333333"
+              size="small"
+              round
+              @click=""
+              style="color: #333333"
+            >
+              日期
+            </van-button>
+          </div>
+        </template> -->
+      </div>
+    </van-pull-refresh>
+  </div>
+</template>
+<script setup>
+import { beforeTime, messageContentParse } from '~/utils/detalTime'
+
+const route = useRoute()
+
+const objectType = computed(() => route.query.objectType)
+
+const queryParams = reactive({
+  pageNum: 1,
+  pageSize: 10,
+  searchMessage: ''
+})
+
+const loading = ref(false)
+const refreshing = ref(false)
+const searchString = ref('')
+
+onMounted(() => {
+  getList()
+})
+
+// 全部成员
+const list = ref([])
+
+const title = ref('聊天记录')
+
+// 搜索
+const search = () => {
+  queryParams.searchMessage = searchString.value
+  queryParams.pageNum = 1
+  list.value = []
+  getList()
+}
+
+// 下拉刷新
+const onRefresh = () => {
+  list.value.length ? queryParams.pageNum++ : (queryParams.pageNum = 1)
+
+  getList()
+}
+
+// 获取聊天记录
+const getList = async () => {
+  try {
+    loading.value = true
+
+    let { data } = await request('/website/tourMessage/getMessageByGroupId', {
+      query: {
+        groupId: route.query.groupId,
+        ...queryParams
+      }
+    })
+
+    if (Array.isArray(data.data) && data?.data?.length) {
+      list.value = list.value.concat(...data.data)
+    } else {
+      list.value = []
+    }
+    refreshing.value = false
+    loading.value = false
+  } catch (err) {
+  } finally {
+    refreshing.value = false
+    loading.value = false
+  }
+}
+
+definePageMeta({
+  layout: false
+})
+
+useSeoMeta({
+  title: objectType.value == 1 ? '我的消息' : title
+})
+</script>
+<style lang="scss" scoped>
+.custom-class {
+  color: #ff9300;
+}
+</style>

+ 859 - 0
src/pages/chat/set.vue

@@ -0,0 +1,859 @@
+<template>
+  <div>
+    <ChatHeaderBar title="聊天设置" />
+
+    <div class="w-full min-h-300 pt-60 box-border bg-[#F7F8FA]">
+      <van-cell-group
+        style="margin-bottom: 12px; padding-top: 12px; padding-left: 16px"
+        class="box-border"
+        inset
+      >
+        <van-row>
+          <van-col
+            style="width: 54px"
+            v-for="(item, index) in setData?.memberList"
+            :key="index"
+            span="4"
+            class="mb-12 mr-10"
+          >
+            <div class="w-40 h-40 rounded-full border mx-auto overflow-hidden mb-4">
+              <img
+                v-if="item.headImageUrl"
+                class="w-full h-full object-cover"
+                :src="item.headImageUrl"
+                alt=""
+              />
+              <img
+                v-else
+                class="w-full h-full object-cover"
+                src="~/assets/img/default_avatar.png"
+                alt=""
+              />
+            </div>
+            <p class="w-full line-clamp-1 lin text-sm text-center text-black-6">
+              <!-- {{ item.groupNickname }} -->
+              {{ item.showName }}
+            </p>
+          </van-col>
+          <van-col
+            span="4"
+            class="mb-12 mr-10"
+            @click="
+              navigateTo({
+                path: '/chat/group-add',
+                query: {
+                  groupId: setData?.id
+                }
+              })
+            "
+          >
+            <div
+              class="w-40 h-40 rounded-full flex justify-center items-center bg-[#F3F3F3] border mx-auto overflow-hidden mb-4"
+            >
+              <span class="iconfont icon-plus text-black-6" style="font-size: 24px"></span>
+            </div>
+            <p class="w-full line-clamp-1 lin text-sm text-center text-black-6">添加成员</p>
+          </van-col>
+          <van-col v-if="isRankAndFiler(userGroupData?.groupRole)" span="4 mb-12 mr-10">
+            <div
+              @click="
+                navigateTo({
+                  path: '/chat/group-member',
+                  query: {
+                    groupId: setData?.id
+                  }
+                })
+              "
+              class="w-40 h-40 rounded-full flex justify-center items-center bg-[#F3F3F3] border mx-auto overflow-hidden mb-4"
+            >
+              <van-icon name="minus" size="24" color="#666666" />
+            </div>
+            <p class="w-full line-clamp-1 lin text-sm text-center text-black-6">删除成员</p>
+          </van-col>
+        </van-row>
+
+        <div
+          v-if="changeState(userGroupData?.groupRole) && setData?.memberList?.lenght == 13"
+          @click="navigateTo(`/chat/group-all?groupId=${userGroupData?.groupId}`)"
+          class="w-full flex pb-5 box-border justify-center items-center leading-3xl text-sm text-black"
+        >
+          查看更多群成员
+          <van-icon name="arrow" class="-mt-2" size="16" />
+        </div>
+      </van-cell-group>
+
+      <van-cell-group style="margin-bottom: 12px" inset>
+        <van-cell
+          size="large"
+          @click="
+            changeState(userGroupData?.groupRole)
+              ? openDialog({
+                  title: '修改群名称',
+                  value: setData?.groupName,
+                  isRemark: 0,
+                  placeholder: '未命名',
+                  subTitle: '最多不能超过12个字'
+                })
+              : showDialog({
+                  title: '群名称',
+                  message: setData.groupName,
+                  confirmButtonColor: '#FF9300',
+                  showCancelButton: false
+                })
+                  .then(() => {
+                    // on close
+                  })
+                  .catch(() => {
+                    // on cancel
+                  })
+          "
+          center
+          is-link
+          title="群名称"
+        >
+          <template #value>
+            <p class="w-full line-clamp-2 text-xl text-black/[0.4] leading-5xl">
+              {{ setData?.groupName ? setData?.groupName : '未命名' }}
+            </p>
+          </template>
+        </van-cell>
+        <van-cell
+          @click="
+            navigateTo({
+              path: '/chat/qr-code',
+              query: {
+                groupName: setData.groupName,
+                groupAvatar: setData.groupAvatar,
+                groupId: setData?.id
+              }
+            })
+          "
+          size="large"
+          center
+          is-link
+          title="群二维码"
+        >
+          <template #value>
+            <div class="w-full flex justify-end items-center">
+              <img class="w-16 h-16 shrink-0" src="~/assets/img/chat/chat-code.svg" alt="" />
+            </div>
+          </template>
+        </van-cell>
+
+        <van-cell
+          :clickable="isRankAndFiler(userGroupData?.groupRole)"
+          @click="handleAnnouncement"
+          size="large"
+          center
+          is-link
+          title="群公告"
+          :value="setData?.groupNotice?.messageContent ? '' : '未设置'"
+        ></van-cell>
+
+        <van-cell @click="handleDescription" size="large" center>
+          <template #title>
+            <p class="w-full line-clamp-1">群介绍:{{ setData?.description }}</p>
+          </template>
+        </van-cell>
+        <van-cell
+          @click="getTreeType"
+          size="large"
+          :is-link="isRankAndFiler(userGroupData?.groupRole)"
+          center
+          title="群聊类型"
+        >
+          <template #value>
+            <p class="w-full line-clamp-1">{{ setData?.belongTypeIdDictMap?.name }}</p>
+          </template>
+        </van-cell>
+        <van-cell
+          @click="
+            openDialog({
+              title: '群备注',
+              value: userGroupData?.groupRemark, //groupRemark
+              isRemark: 1,
+              placeholder: '备注',
+              subTitle: '群备注仅自己可见'
+            })
+          "
+          size="large"
+          is-link
+          center
+          title="群备注"
+        >
+          <template #value>
+            <p class="w-full line-clamp-1">
+              {{ userGroupData?.groupRemark ? userGroupData?.groupRemark : '未设置' }}
+            </p>
+          </template>
+        </van-cell>
+      </van-cell-group>
+
+      <van-cell-group
+        v-if="isRankAndFiler(userGroupData?.groupRole)"
+        style="margin-bottom: 12px"
+        inset
+      >
+        <van-cell size="large" center title="个人主页展示">
+          <template #label>
+            <span>开启后,在群聊广场和个人主页</span>
+          </template>
+          <template #right-icon>
+            <van-switch
+              :active-value="1"
+              :inactive-value="0"
+              v-model="isPublic"
+              @click="changeIsPublic"
+              active-color="#FF9300"
+              inactive-color="#dcdee0"
+            />
+          </template>
+        </van-cell>
+        <van-cell size="large" center title="群聊邀请确认">
+          <template #label>
+            <span>
+              启用后,群成员需群主或群管理员确认才能邀请朋友进群。扫描二维码进群将同时停用
+            </span>
+          </template>
+          <template #right-icon>
+            <van-switch
+              :active-value="1"
+              :inactive-value="0"
+              v-model="isNeedConfirm"
+              @click="changeIsNeedConfirm"
+              active-color="#FF9300"
+              inactive-color="#dcdee0"
+            />
+          </template>
+        </van-cell>
+
+        <van-cell
+          @click="
+            navigateTo({
+              path: '/chat/examine',
+              query: {
+                groupId: setData?.id
+              }
+            })
+          "
+          size="large"
+          is-link
+          center
+          title="收到的进群申请"
+        ></van-cell>
+      </van-cell-group>
+
+      <van-cell-group style="margin-bottom: 12px" inset>
+        <van-cell
+          @click="
+            openDialog({
+              title: '我在群里的昵称',
+              value: userGroupData?.groupNickname,
+              placeholder: '昵称',
+              isRemark: 2,
+              subTitle: '昵称修改之后,只会在此群内显示,群内成员都可以看见'
+            })
+          "
+          size="large"
+          is-link
+          center
+          title="我在群里的昵称"
+        >
+          <template #value>
+            <p class="w-full line-clamp-1">
+              {{ userGroupData?.groupNickname }}
+            </p>
+          </template>
+        </van-cell>
+      </van-cell-group>
+      <van-cell-group style="margin-bottom: 12px" inset>
+        <van-cell
+          @click="
+            navigateTo({
+              path: '/chat/set-sub',
+              query: {
+                // tab: 'records',
+                groupId: setData?.id,
+                objectType: 2
+              }
+            })
+          "
+          size="large"
+          is-link
+          center
+          title="查找聊天记录"
+        ></van-cell>
+      </van-cell-group>
+      <van-cell-group style="margin-bottom: 12px" inset>
+        <van-cell size="large" is-link center title="消息免打扰">
+          <template #right-icon>
+            <van-switch
+              :active-value="1"
+              :inactive-value="0"
+              v-model="isNotDisturb"
+              @click="handleIsNotDisturb"
+              active-color="#FF9300"
+              inactive-color="#dcdee0"
+            />
+          </template>
+        </van-cell>
+        <van-cell size="large" is-link center title="置顶聊天">
+          <template #right-icon>
+            <van-switch
+              :active-value="1"
+              :inactive-value="0"
+              v-model="isTop"
+              @click="handleIsTop"
+              active-color="#FF9300"
+              inactive-color="#dcdee0"
+            />
+          </template>
+        </van-cell>
+        <van-cell
+          @click="
+            navigateTo({
+              path: '/chat/background',
+              query: {
+                groupId: setData?.id
+              }
+            })
+          "
+          size="large"
+          is-link
+          center
+          title="设置当前聊天背景"
+        ></van-cell>
+      </van-cell-group>
+
+      <van-cell-group style="margin-bottom: 12px" inset>
+        <van-cell
+          @click="
+            navigateTo({
+              path: '/chat/report',
+              query: {
+                groupId: setData?.id,
+                objectType: 2
+              }
+            })
+          "
+          size="large"
+          is-link
+          center
+          title="举报"
+        ></van-cell>
+        <van-cell
+          @click="clearChatHistory"
+          size="large"
+          is-link
+          center
+          title="清空聊天记录"
+        ></van-cell>
+      </van-cell-group>
+
+      <div class="w-full pt-10 pb-40 px-16 box-border">
+        <van-button @click="handleExitGroupChat" color="#FF9300" round block>
+          <template v-if="isRankAndFiler(userGroupData?.groupRole)">解散群聊</template>
+          <template v-else>退出群聊</template>
+        </van-button>
+      </div>
+    </div>
+
+    <ChatDialog
+      v-model:show="showDialog"
+      v-model:title="dialogTitle"
+      @confirm="confirm"
+      @cancel="cancel"
+    >
+      <div class="w-full px-12 text-center mt-4">
+        <p class="mx-auto w-[80%] text-sm text-black-9 mb-16">{{ dialogSubTitle }}</p>
+
+        <van-field
+          class=""
+          style="height: 40px; background: #f5f5f5; border-radius: 8px; margin-bottom: 30px"
+          clearable
+          :placeholder="dialogPlaceholder"
+          v-model="groupName"
+          maxlength="30"
+        />
+      </div>
+    </ChatDialog>
+
+    <van-dialog width="90%" v-model:show="showBelongTypeId" show-cancel-button>
+      <template #title>
+        <div class="w-full flex justify-between items-center px-16 py-11 border-b-[1px]">
+          <h1>群类型</h1>
+          <div class="w-32 h-32 -mt-3 shrink-0" @click="showBelongTypeId = false">
+            <img
+              @click="visible = false"
+              class="w-full h-full object-cover"
+              src="~/assets/img/note-create/close.svg"
+              alt=""
+            />
+          </div>
+        </div>
+      </template>
+
+      <div class="w-full px-16 py-20 card-list">
+        <template v-for="(subItem, subIndex) in groupTypeList" :key="subItem?.id">
+          <div
+            :class="` h-40 mb-4  relative ${showIndex == subItem.id ? ' border-[#FF9300] border-2  shadow-[0_4px_4px_0px_rgba(0,0,0,0.1)]' : ''}  pl-22 flex justify-start items-center shrink-0 text-xl text-black-6 font-semibold bg-[#F7F8FA] rounded-md`"
+            @click="handleTypeClick(subItem)"
+          >
+            <div class="w-24 h-24 shrink-0 mr-6">
+              <img
+                class="w-full h-full object-cover"
+                :src="item?.typeIcon ? item?.typeIcon : city"
+                alt=""
+              />
+            </div>
+            <span class="line-clamp-1">
+              {{ subItem.typeName }}
+            </span>
+
+            <div
+              v-if="showIndex == subItem.id"
+              class="absolute rounded-t-md square1 -top-2 -left-2 z-1"
+            ></div>
+            <div v-if="showIndex == subItem.id" class="w-14 h-14 absolute top-0 left-0 z-2">
+              <img class="w-full h-full object-cover" alt="" src="~/assets/img/chat/check.svg" />
+            </div>
+          </div>
+
+          <div
+            v-if="showIndex == subItem.id && subItem?.children.length > 0"
+            class="item__child mb-4 w-full relative flex justify-start box-border flex-wrap pl-21 pt-14 bg-[#F7F7F7] rounded-md"
+          >
+            <div
+              :class="`w-32 h-8 absolute -top-[8px] ${subIndex % 2 != 0 ? 'right-[23px]' : 'left-[23px]'} `"
+            >
+              <img class="w-full h-full" src="~/assets/img/chat/polygon.svg" alt="" />
+            </div>
+            <template v-for="el in subItem.children" :key="el?.id">
+              <div
+                @click="childrenHandleTypeClick(el)"
+                :class="`${childrenIndex == el.id ? 'text-[#FF9300] border-[#FF9300]  border-[2px] bg-[#FF9300]/[0.08]' : 'bg-white border text-black-6'} py-5 mr-8 mb-12 rounded-[4px] text-sm  box-border px-12`"
+              >
+                {{ el.typeName }}
+              </div>
+            </template>
+          </div>
+        </template>
+      </div>
+
+      <template #footer>
+        <div class="w-full px-40 pb-30">
+          <van-button
+            style="font-size: 16px"
+            type="primary"
+            color="#FF9300"
+            round
+            block
+            @click="changeBelongTypeId"
+          >
+            确认
+          </van-button>
+        </div>
+      </template>
+    </van-dialog>
+  </div>
+</template>
+<script setup>
+import city from '~/assets/img/chat/city-one.svg'
+
+const route = useRoute()
+
+const userInfoStore = useUserInfoStore()
+const { userInfo } = storeToRefs(userInfoStore)
+
+definePageMeta({
+  layout: false
+})
+
+useSeoMeta({
+  title: '群聊'
+})
+
+onMounted(() => {
+  userInfoStore.getUserInfo()
+  getGroupSetData()
+})
+
+let setData = reactive({
+  userId: userInfo.value.userId,
+  belongTypeIdDictMap: {}
+})
+
+watch([() => route.query.groupId], () => {}, {
+  immediate: true
+})
+
+const userGroupData = ref(null)
+
+const groupName = ref('')
+
+// isPublic 是否公开展示 0隐藏 1公开。
+// isNeedConfirm 是否开启群聊邀请确认 0不开启 1开启。
+// 是否显示到个人主页
+const isPublic = ref(0)
+const isNeedConfirm = ref(0)
+
+const showBelongTypeId = ref(false)
+const isNotDisturb = ref(0)
+const isTop = ref(0)
+
+// 弹出窗
+const showDialog = ref(false)
+const isRemark = ref(0) //0 群名称 1备注 2 我在群里的昵称
+const dialogTitle = ref('')
+const dialogPlaceholder = ref('')
+const dialogSubTitle = ref('')
+
+// 弹窗的方法
+const openDialog = (item) => {
+  showDialog.value = true
+  dialogTitle.value = item?.title
+  isRemark.value = item.isRemark
+  groupName.value = item?.value
+  dialogPlaceholder.value = item?.placeholder
+  dialogSubTitle.value = item?.subTitle
+}
+// 弹窗确认的事件
+const confirm = () => {
+  if (userGroupData.value?.groupRole == 1 && isRemark.value == 0)
+    changeGroupName({ groupName: groupName.value })
+
+  if (isRemark.value == 1) changeTourMember({ groupId: setData.id, remark: groupName.value })
+
+  if (isRemark.value == 2)
+    changeGroupName({
+      groupNickname: groupName.value
+    })
+
+  // showDialog.value = false
+}
+
+// groupName 修改群名称
+// belongTypeId 群聊类型
+// groupAvatar 群头像
+// isNeedConfirm  是否开启群验证 0 否 1 是
+// isPublic 是否公开展示 0隐藏 1公开
+
+// 修我在群里的昵称 修改群名称
+const changeGroupName = async (body) => {
+  try {
+    const { data } = await request('/website/tourGroup/updateGroup', {
+      method: 'post',
+      body: {
+        groupId: setData.id,
+        ...body
+      }
+    })
+
+    if (data) {
+      userGroupData.value[Object.keys(body)[0]] = groupName.value
+
+      // showSuccessToast('修改成功')
+    } else {
+      // showFailToast('修改失败')
+    }
+  } catch (error) {}
+}
+
+// 是否开启群验证
+const changeIsNeedConfirm = async () => {
+  changeGroupBelongTypeIdIsNeedConfirm({ isNeedConfirm: isNeedConfirm.value })
+}
+
+// 是否公开展示
+const changeIsPublic = async () => {
+  changeGroupBelongTypeIdIsNeedConfirm({ isPublic: isPublic.value })
+}
+
+const showIndex = ref(null)
+const childrenIndex = ref(null)
+
+const groupTypeList = ref([])
+const subTypeList = ref([])
+const groupTypeName = ref('')
+
+// 选中群类型的方法
+const handleTypeClick = (item) => {
+  if (showIndex.value == item?.id) {
+    showIndex.value = null
+    subTypeList.value = []
+  } else {
+    showIndex.value = item.id
+    subTypeList.value = item.children
+  }
+}
+// 选中群类型子集的方法
+const childrenHandleTypeClick = (item) => {
+  childrenIndex.value = item.id
+  groupTypeName.value = item.typeName
+}
+
+// 获取群类型
+async function getTreeType() {
+  if (isRankAndFiler(userGroupData.value?.groupRole)) {
+    try {
+      const { data } = await request('/website/tourGroupType/treeType')
+      if (Array.isArray(data) && data.length) {
+        groupTypeList.value = data
+
+        data.map((item) => {
+          item.children.map((el) => {
+            if (el.id == setData?.belongTypeId) {
+              showIndex.value = item.id
+              childrenIndex.value = setData.belongTypeId
+              subTypeList.value = item.children
+            }
+          })
+        })
+
+        showBelongTypeId.value = true
+      } else {
+        groupTypeList.value = []
+      }
+    } finally {
+    }
+  } else {
+    return
+  }
+}
+
+// 群聊类型
+const changeBelongTypeId = async () => {
+  if (childrenIndex.value != setData.belongTypeId) {
+    changeGroupBelongTypeIdIsNeedConfirm({ belongTypeId: childrenIndex.value })
+  }
+  showBelongTypeId = false
+}
+
+// 修改 群聊类型  是否开启群验证
+async function changeGroupBelongTypeIdIsNeedConfirm(body) {
+  try {
+    const { data } = await request('/website/tourGroup/updateGroup', {
+      method: 'post',
+      body: {
+        groupId: setData.id,
+        ...body
+      }
+    })
+    if (data) {
+      if ((userGroupData.value[Object.keys(body)[0]] = 'belongTypeId')) {
+        setData.belongTypeIdDictMap.name = groupTypeName.value
+      }
+      // showSuccessToast('修改成功')
+      return
+    }
+  } catch (error) {}
+}
+
+// 修改备注名 和修改自己在群里的备注
+const changeTourMember = async (body) => {
+  try {
+    const { data } = await request('/website/tourMember/updateTourMember', {
+      method: 'post',
+      body
+    })
+    console.log(data, '修改自己在群里的备注')
+
+    if (data) {
+      //修改成功
+      if ((Object.keys(body)[1] = 'remark')) {
+        userGroupData.value.groupRemark = groupName.value
+      } else {
+        userGroupData.value[Object.keys(body)[1]] = groupName.value
+      }
+      showSuccessToast('操作成功')
+    } else {
+      // showSuccessToast('操作失败')
+    }
+  } catch (error) {}
+}
+
+// 弹窗的取消
+const cancel = () => {
+  showDialog.value = false
+}
+
+// 数字转换成布尔值
+const changeState = (state) => {
+  if (state == 1) return true
+  if (state == 0) return false
+}
+
+// 布尔值转换成数字
+const changeStateBoolean = (state) => {
+  if (state) {
+    return 1
+  } else {
+    return 0
+  }
+}
+
+// 是否置顶
+const handleIsTop = () => {
+  // handleBoolean({ isTop: changeStateBoolean(isTop.value) })
+  handleBoolean({ isTop: isTop.value })
+}
+
+// 是否免打扰
+const handleIsNotDisturb = () => {
+  // handleBoolean({ isNotDisturb: changeStateBoolean(isNotDisturb.value) })
+  handleBoolean({ isNotDisturb: isNotDisturb.value })
+}
+
+// 是否免打扰和 是否置顶 公共
+const handleBoolean = async (params) => {
+  try {
+    let { data } = await request('/website/tourMember/updateSingleTourMember', {
+      method: 'post',
+      body: {
+        groupId: userGroupData.value.groupId,
+        ...params
+      }
+    })
+    if (data) {
+      if (Object.keys(params)[0] == 'isTop') {
+        isTop.value ? showToast('已置顶') : showToast('置顶取消')
+      }
+      if (Object.keys(params)[0] == 'isNotDisturb') {
+        isNotDisturb.value ? showToast('已开启面打扰') : showToast('已关闭面打扰')
+      }
+    }
+  } catch (error) {}
+}
+
+// 获取群设置的配置信息
+const getGroupSetData = async () => {
+  const { data } = await request('/website/tourGroup/getGroupInfoAndMemberByGroupId', {
+    query: {
+      groupId: route.query.groupId
+    }
+  })
+  if (typeof data == 'object') {
+    setData = data
+
+    userGroupData.value = data?.memberList.find((item) => item?.userId == userInfo.value?.userId)
+
+    // isPublic.value = changeState(setData.isPublic)
+    // isNeedConfirm.value = changeState(setData.isNeedConfirm)
+    isPublic.value = setData.isPublic
+    isNeedConfirm.value = setData.isNeedConfirm
+
+    // isNotDisturb.value = changeState(userGroupData.value?.isNotDisturb)
+    // isTop.value = changeState(userGroupData.value.isTop)
+    isNotDisturb.value = userGroupData.value?.isNotDisturb
+    isTop.value = userGroupData.value.isTop
+  }
+}
+
+// 是否是普通成员
+const isRankAndFiler = (role) => {
+  return role == 1 || role == 2 ? true : false
+}
+
+// 清空聊天记录
+const clearChatHistory = () => {
+  showConfirmDialog({
+    width: 260,
+    title: '提示',
+    message: '清空聊天记录',
+    confirmButtonColor: '#FF9300'
+  })
+    .then(async () => {
+      try {
+        const res = await request('/website/tourMessage/clearGroupMessage', {
+          query: {
+            groupId: setData?.id
+          }
+        })
+        if (res && res?.success) return
+      } catch (error) {}
+    })
+    .catch(() => {})
+}
+
+// 点击公告
+const handleAnnouncement = () => {
+  navigateTo({
+    path: '/chat/announcement',
+    query: {
+      groupId: setData?.id,
+      userId: userInfo.value.userId,
+      groupRole: userGroupData.value?.groupRole
+    }
+  })
+}
+
+// 修改群介绍
+const handleDescription = () => {
+  if (isRankAndFiler(userGroupData.value?.groupRole)) {
+    showDialog({
+      width: 260,
+      title: '群介绍',
+      message: setData.description,
+      confirmButtonColor: '#FF9400'
+    }).then(() => {
+      // on close
+    })
+  } else {
+    showDialog({
+      width: 260,
+      title: '群介绍',
+      message: setData.description,
+      confirmButtonColor: '#FF9400'
+    }).then(() => {
+      // on close
+    })
+  }
+}
+
+// 退出群聊
+function handleExitGroupChat() {
+  showConfirmDialog({
+    width: 260,
+    title: '提示',
+    message: `是否${isRankAndFiler(userGroupData?.groupRole) ? '解散' : '退出'}当前群聊${setData?.groupName ? `"${setData?.groupName}"` : ''}`,
+    confirmButtonColor: '#FF9300'
+  })
+    .then(async () => {
+      const res = await request('/website/tourGroup/exitGroup', {
+        query: {
+          groupId: setData.id
+        }
+      })
+      if (res && res?.success) {
+        navigateTo({
+          path: '/profile/my-news',
+          replace: true
+        })
+      }
+    })
+    .catch(() => {})
+}
+</script>
+<style lang="scss" scoped>
+::v-deep .van-dialog__header {
+  padding-top: 21px;
+}
+
+.card-list {
+  display: grid;
+  grid-template-columns: repeat(2, 1fr);
+  grid-auto-flow: dense; /* 确保项目会填充所有的网格单元 */
+  grid-gap: 12px;
+
+  .item__child {
+    grid-column: span 2;
+  }
+
+  .square1 {
+    width: 0;
+    height: 0;
+    border-bottom: 28px solid transparent; /* 创建三角形 */
+    border-left: 28px solid #ff9300; /* 三角形的颜色 */
+  }
+}
+</style>

+ 256 - 0
src/pages/chat/single-add.vue

@@ -0,0 +1,256 @@
+<template>
+  <div class="w-full h-[100vh]">
+    <ChatHeaderBar title="选择互关好友" />
+
+    <ChatSearch v-model:searchString="showName" @search="search" placeholder="请输入关键词" />
+
+    <van-pull-refresh v-model="refreshing" @refresh="onRefresh">
+      <ChatEmpty
+        image="search"
+        v-if="!addDataList?.length && !loading"
+        title="暂无数据"
+        top="100"
+      />
+
+      <van-list
+        v-else-if="addDataList.length"
+        v-model:loading="loading"
+        error-text="获取失败"
+        finished-text="-- 没有更多了 --"
+        :finished="finished"
+        :immediate-check="false"
+      >
+        <!--    @load="getLoadList"  -->
+        <div style="height: calc(100vh - 170px)">
+          <van-checkbox-group v-model="checked">
+            <!-- <van-index-bar highlight-color="#FD9A00" index-list :sticky="false"> -->
+            <template v-for="(item, index) in addDataList" :key="item?.attentionIdDictMap?.userId">
+              <!-- <van-index-anchor index="A" /> -->
+              <van-cell center clickable @click="toggle(item)">
+                <template #icon>
+                  <div class="flex justify-start">
+                    <van-checkbox
+                      checked-color="#FD9A00"
+                      :name="item?.attentionIdDictMap?.userId"
+                      :ref="(el) => (checkboxRefs[item?.attentionIdDictMap?.userId] = el)"
+                      @click.stop="toggle(item)"
+                    />
+
+                    <div class="w-40 h-40 ml-13 mr-12 rounded-full overflow-hidden">
+                      <img
+                        v-if="item?.attentionIdDictMap?.headImageUrl"
+                        class="w-full h-full shrink-0 object-cover"
+                        :src="item?.attentionIdDictMap?.headImageUrl"
+                        alt=""
+                      />
+
+                      <img
+                        class="w-full h-full shrink-0 object-cover"
+                        src="~/assets/img/default_avatar.png"
+                        alt=""
+                      />
+                    </div>
+                  </div>
+                </template>
+                <template #title>
+                  <div class="flex items-center">
+                    <h1 class="text-xl text-black-3">
+                      {{ item?.attentionIdDictMap?.showName }}
+                    </h1>
+                    <van-tag
+                      v-if="item.fansStatus == 2"
+                      style="margin-left: 5px; padding: 3px 6px"
+                      color="#F7F8FA"
+                      text-color="#666666"
+                    >
+                      相互关注
+                    </van-tag>
+                  </div>
+                </template>
+              </van-cell>
+            </template>
+            <!-- </van-index-bar> -->
+          </van-checkbox-group>
+        </div>
+      </van-list>
+    </van-pull-refresh>
+    <div
+      class="w-full box-border p-16 pb-40 bg-white fixed bottom-0 left-0 flex justify-between items-center shadow-[0px_-4px_4px_0px_rgba(0,0,0,0.1)]"
+    >
+      <div class="shrink-0 flex justify-start items-center">
+        <div class="w-118 shrink-0 flex justify-start items-center overflow-hidden">
+          <div
+            v-for="(item, index) in checkedList.slice(0, 5)"
+            :key="index + 'avatar'"
+            :class="`w-36 h-36  ${index == 0 ? '' : '-ml-16'} shrink-0 rounded-full overflow-hidden`"
+          >
+            <img
+              v-if="item?.attentionIdDictMap?.headImageUrl"
+              class="w-full h-full object-cover"
+              :src="item?.attentionIdDictMap?.headImageUrl"
+              alt=""
+            />
+            <img
+              v-else
+              class="w-full h-full shrink-0 object-cover"
+              src="~/assets/img/default_avatar.png"
+              alt=""
+            />
+          </div>
+        </div>
+
+        <div v-if="checkedList.length > 5" class="shrink-0 w-24 h-24 ml-8">
+          <img class="w-full h-full object-cover" src="~/assets/img/chat/ellipsis.svg" alt="" />
+        </div>
+      </div>
+      <van-button
+        :disabled="checkedList.length > 0 ? false : true"
+        @click="handleCreateGroup"
+        style="width: 160px"
+        class="shrink-0"
+        block
+        size="large"
+        color="#FD9A00"
+        round
+      >
+        新建
+        <span v-if="checkedList.length">({{ checkedList.length }})</span>
+      </van-button>
+    </div>
+  </div>
+</template>
+<script setup>
+import { pinyin } from 'pinyin-pro'
+
+const route = useRoute()
+
+definePageMeta({
+  layout: false
+})
+
+onMounted(() => {
+  getList()
+})
+
+const refreshing = ref(false)
+const loading = ref(false)
+const finished = ref(false)
+
+const checked = ref([])
+const checkedList = ref([])
+const showName = ref('')
+const checkboxRefs = ref([])
+
+// 字母的数组
+const letterList = ref([])
+
+const queryParams = reactive({
+  pageNum: 1,
+  pageSize: 10,
+  flagPage: 1
+})
+
+const addDataList = ref([])
+const filterDataList = ref([])
+
+const searchText = computed(() => (showName.value ? '暂无互关好友' : '暂无搜索结果'))
+
+// 选中要邀请的人
+const toggle = (item) => {
+  let index = checkedList.value.findIndex(
+    (el) => el?.attentionIdDictMap?.userId == item?.attentionIdDictMap?.userId
+  )
+
+  if (index != -1) {
+    checkedList.value.splice(index, 1)
+  } else {
+    checkedList.value.push(item)
+  }
+  checkboxRefs.value[item?.attentionIdDictMap?.userId].toggle()
+}
+
+const search = () => {
+  finished.value = true
+  if (showName.value) {
+    addDataList.value = filterDataList.value.filter((item) =>
+      item.attentionIdDictMap.name.includes(showName.value)
+    )
+  } else {
+    addDataList.value = filterDataList.value
+  }
+}
+
+// 刷新
+const onRefresh = () => {
+  queryParams.pageNum = 1
+  addDataList.value = []
+  filterDataList.value = []
+  getList()
+}
+
+// 获取数据
+const getList = async () => {
+  try {
+    let url = `/website/tourism/fans/getFriends`
+
+    loading.value = true
+    let {
+      data: { dataList, totalCount }
+    } = await request(url, {
+      query: {
+        ...queryParams
+      }
+    })
+
+    if (Array.isArray(dataList) && dataList?.length) {
+      addDataList.value = dataList
+    } else {
+      addDataList.value = []
+    }
+
+    loading.value = false
+    refreshing.value = false
+    if (addDataList.value.length >= totalCount) {
+      finished.value = true
+    } else {
+      finished.value = false
+    }
+  } catch (err) {
+  } finally {
+    refreshing.value = false
+    loading.value = false
+  }
+}
+
+// 创建多人聊天
+async function handleCreateGroup() {
+  try {
+    showLoadingToast({
+      message: '准备开始群聊...',
+      duration: 100000
+    })
+    let { data } = request('/website/tourGroup/createGroup', {
+      method: 'post',
+      body: {
+        createType: 2,
+        ids: checked.value
+      }
+    })
+    if (data) {
+      navigateTo({
+        path: '/chat/group-chat',
+        query: data,
+        replace: true
+      })
+    }
+  } catch (error) {
+  } finally {
+    closeToast()
+  }
+}
+
+useSeoMeta({
+  title: '我的消息'
+})
+</script>
+<style lang="scss" scoped></style>

Některé soubory nejsou zobrazeny, neboť je v těchto rozdílových datech změněno mnoho souborů