lex 1 рік тому
батько
коміт
50bce0a285
100 змінених файлів з 4763 додано та 34 видалено
  1. 2 1
      .eslintrc.js
  2. 1 1
      dev-dist/sw.js
  3. 610 32
      package-lock.json
  4. 11 0
      package.json
  5. 1 0
      src/TUIKit/.eslintignore
  6. 22 0
      src/TUIKit/.eslintrc.js
  7. 330 0
      src/TUIKit/.github/README.md
  8. 117 0
      src/TUIKit/CHANGELOG.md
  9. 7 0
      src/TUIKit/TUIComponents/assets/icon/arrow-left-center.svg
  10. 7 0
      src/TUIKit/TUIComponents/assets/icon/at.svg
  11. 16 0
      src/TUIKit/TUIComponents/assets/icon/back.svg
  12. 20 0
      src/TUIKit/TUIComponents/assets/icon/c2c.svg
  13. BIN
      src/TUIKit/TUIComponents/assets/icon/call.png
  14. BIN
      src/TUIKit/TUIComponents/assets/icon/cancel.png
  15. 23 0
      src/TUIKit/TUIComponents/assets/icon/cancel.svg
  16. BIN
      src/TUIKit/TUIComponents/assets/icon/chat-setting.png
  17. BIN
      src/TUIKit/TUIComponents/assets/icon/close-image.png
  18. BIN
      src/TUIKit/TUIComponents/assets/icon/close.png
  19. 4 0
      src/TUIKit/TUIComponents/assets/icon/collapse.svg
  20. BIN
      src/TUIKit/TUIComponents/assets/icon/custom.png
  21. BIN
      src/TUIKit/TUIComponents/assets/icon/del.png
  22. 1 0
      src/TUIKit/TUIComponents/assets/icon/double-arrow.svg
  23. BIN
      src/TUIKit/TUIComponents/assets/icon/down.png
  24. BIN
      src/TUIKit/TUIComponents/assets/icon/downaload-image.png
  25. BIN
      src/TUIKit/TUIComponents/assets/icon/edit.png
  26. 27 0
      src/TUIKit/TUIComponents/assets/icon/emoj.svg
  27. BIN
      src/TUIKit/TUIComponents/assets/icon/evaluate.png
  28. 25 0
      src/TUIKit/TUIComponents/assets/icon/evaluate.svg
  29. 4 0
      src/TUIKit/TUIComponents/assets/icon/expand.svg
  30. BIN
      src/TUIKit/TUIComponents/assets/icon/face.png
  31. BIN
      src/TUIKit/TUIComponents/assets/icon/files.png
  32. BIN
      src/TUIKit/TUIComponents/assets/icon/gobackground.png
  33. 6 0
      src/TUIKit/TUIComponents/assets/icon/icon-close-h5.svg
  34. 6 0
      src/TUIKit/TUIComponents/assets/icon/icon-download-h5.svg
  35. 3 0
      src/TUIKit/TUIComponents/assets/icon/icon-download.svg
  36. BIN
      src/TUIKit/TUIComponents/assets/icon/image.png
  37. BIN
      src/TUIKit/TUIComponents/assets/icon/location.png
  38. BIN
      src/TUIKit/TUIComponents/assets/icon/meeting.png
  39. BIN
      src/TUIKit/TUIComponents/assets/icon/message.png
  40. BIN
      src/TUIKit/TUIComponents/assets/icon/more.png
  41. 30 0
      src/TUIKit/TUIComponents/assets/icon/msg-copy.svg
  42. 33 0
      src/TUIKit/TUIComponents/assets/icon/msg-del.svg
  43. 31 0
      src/TUIKit/TUIComponents/assets/icon/msg-forward.svg
  44. 30 0
      src/TUIKit/TUIComponents/assets/icon/msg-play.svg
  45. 8 0
      src/TUIKit/TUIComponents/assets/icon/msg-qnote.svg
  46. 31 0
      src/TUIKit/TUIComponents/assets/icon/msg-reply.svg
  47. 29 0
      src/TUIKit/TUIComponents/assets/icon/msg-revoke.svg
  48. 8 0
      src/TUIKit/TUIComponents/assets/icon/mute.svg
  49. BIN
      src/TUIKit/TUIComponents/assets/icon/phone.png
  50. BIN
      src/TUIKit/TUIComponents/assets/icon/public.png
  51. 6 0
      src/TUIKit/TUIComponents/assets/icon/replies.svg
  52. BIN
      src/TUIKit/TUIComponents/assets/icon/right.png
  53. BIN
      src/TUIKit/TUIComponents/assets/icon/room.png
  54. 7 0
      src/TUIKit/TUIComponents/assets/icon/rotate-left.svg
  55. 7 0
      src/TUIKit/TUIComponents/assets/icon/rotate-right.svg
  56. BIN
      src/TUIKit/TUIComponents/assets/icon/salebackground.png
  57. BIN
      src/TUIKit/TUIComponents/assets/icon/selected.png
  58. 27 0
      src/TUIKit/TUIComponents/assets/icon/selected.svg
  59. 55 0
      src/TUIKit/TUIComponents/assets/icon/star-light.svg
  60. 15 0
      src/TUIKit/TUIComponents/assets/icon/star.svg
  61. 6 0
      src/TUIKit/TUIComponents/assets/icon/startGroup.svg
  62. BIN
      src/TUIKit/TUIComponents/assets/icon/system.png
  63. BIN
      src/TUIKit/TUIComponents/assets/icon/video-call.png
  64. BIN
      src/TUIKit/TUIComponents/assets/icon/video.png
  65. BIN
      src/TUIKit/TUIComponents/assets/icon/voice-call.png
  66. BIN
      src/TUIKit/TUIComponents/assets/icon/voice.png
  67. BIN
      src/TUIKit/TUIComponents/assets/icon/words.png
  68. 25 0
      src/TUIKit/TUIComponents/assets/icon/words.svg
  69. BIN
      src/TUIKit/TUIComponents/assets/icon/work.png
  70. 9 0
      src/TUIKit/TUIComponents/assets/icon/zoom-in.svg
  71. 9 0
      src/TUIKit/TUIComponents/assets/icon/zoom-out.svg
  72. 3 0
      src/TUIKit/TUIComponents/components/dialog/index.ts
  73. 88 0
      src/TUIKit/TUIComponents/components/dialog/index.vue
  74. 34 0
      src/TUIKit/TUIComponents/components/dialog/style/color.scss
  75. 5 0
      src/TUIKit/TUIComponents/components/dialog/style/dialog.scss
  76. 46 0
      src/TUIKit/TUIComponents/components/dialog/style/h5.scss
  77. 60 0
      src/TUIKit/TUIComponents/components/dialog/style/web.scss
  78. 2 0
      src/TUIKit/TUIComponents/components/drag/index.ts
  79. 127 0
      src/TUIKit/TUIComponents/components/drag/index.vue
  80. 57 0
      src/TUIKit/TUIComponents/components/mask/mask.vue
  81. 75 0
      src/TUIKit/TUIComponents/components/message/index.ts
  82. 165 0
      src/TUIKit/TUIComponents/components/message/index.vue
  83. 67 0
      src/TUIKit/TUIComponents/components/slider/index.vue
  84. 3 0
      src/TUIKit/TUIComponents/components/transfer/index.ts
  85. 206 0
      src/TUIKit/TUIComponents/components/transfer/index.vue
  86. 63 0
      src/TUIKit/TUIComponents/components/transfer/style/color.scss
  87. 71 0
      src/TUIKit/TUIComponents/components/transfer/style/h5.scss
  88. 5 0
      src/TUIKit/TUIComponents/components/transfer/style/transfer.scss
  89. 135 0
      src/TUIKit/TUIComponents/components/transfer/style/web.scss
  90. 20 0
      src/TUIKit/TUIComponents/container/IComponentServer.ts
  91. 37 0
      src/TUIKit/TUIComponents/container/TUIChat/components/index.ts
  92. 178 0
      src/TUIKit/TUIComponents/container/TUIChat/components/index.vue
  93. 88 0
      src/TUIKit/TUIComponents/container/TUIChat/components/message-audio.vue
  94. 635 0
      src/TUIKit/TUIComponents/container/TUIChat/components/message-bubble.vue
  95. 228 0
      src/TUIKit/TUIComponents/container/TUIChat/components/message-custom.vue
  96. 285 0
      src/TUIKit/TUIComponents/container/TUIChat/components/message-emoji-react.vue
  97. 65 0
      src/TUIKit/TUIComponents/container/TUIChat/components/message-face.vue
  98. 108 0
      src/TUIKit/TUIComponents/container/TUIChat/components/message-file.vue
  99. 258 0
      src/TUIKit/TUIComponents/container/TUIChat/components/message-image.vue
  100. 40 0
      src/TUIKit/TUIComponents/container/TUIChat/components/message-location.vue

+ 2 - 1
.eslintrc.js

@@ -14,6 +14,7 @@ module.exports = {
     sourceType: 'module'
   },
   rules: {
-    '@typescript-eslint/no-explicit-any': ['off']
+    '@typescript-eslint/no-explicit-any': ['off'],
+    'no-unsafe-optional-chaining': 'off'
   }
 };

+ 1 - 1
dev-dist/sw.js

@@ -82,7 +82,7 @@ define(['./workbox-5357ef54'], (function (workbox) { 'use strict';
     "revision": "3ca0b8505b4bec776b69afdba2768812"
   }, {
     "url": "index.html",
-    "revision": "0.rcgagrcgb8"
+    "revision": "0.detutlh2tgo"
   }], {});
   workbox.cleanupOutdatedCaches();
   workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {

Різницю між файлами не показано, бо вона завелика
+ 610 - 32
package-lock.json


+ 11 - 0
package.json

@@ -23,6 +23,8 @@
     "prepare": "husky install"
   },
   "dependencies": {
+    "@tencentcloud/chat": "^3.1.1",
+    "@tencentcloud/chat-uikit-vue": "^1.4.5",
     "@vant/use": "^1.5.2",
     "@vicons/ionicons5": "^0.12.0",
     "@vueuse/core": "^10.2.0",
@@ -42,7 +44,13 @@
     "postcss-px2rem": "^0.3.0",
     "px2rem-loader": "^0.1.9",
     "query-string": "^8.1.0",
+    "rtc-ai-denoiser": "^1.1.3",
     "terser": "^5.18.2",
+    "tim-js-sdk": "^2.27.5",
+    "tim-profanity-filter-plugin": "^0.9.0",
+    "tim-upload-plugin": "^1.0.6",
+    "trtc-js-sdk": "^4.15.12",
+    "tsignaling": "^1.0.6",
     "umi-request": "^1.4.0",
     "vite-plugin-pwa": "^0.16.4",
     "vudio.js": "^1.0.3",
@@ -51,6 +59,7 @@
     "vue-router": "^4.1.6",
     "vue3-lottie": "^2.7.0",
     "vuedraggable": "^4.1.0",
+    "vuex": "^4.1.0",
     "wavesurfer.js": "^7.0.0-beta.11"
   },
   "devDependencies": {
@@ -81,6 +90,8 @@
     "plop": "^3.1.2",
     "postcss-px-to-viewport": "^1.1.1",
     "prettier": "^2.8.7",
+    "sass": "^1.64.1",
+    "sass-loader": "^10.1.1",
     "typescript": "^5.0.4",
     "unplugin-vue-components": "^0.24.1",
     "vite": "^4.2.1",

+ 1 - 0
src/TUIKit/.eslintignore

@@ -0,0 +1 @@
+TUIPlugin/TUICallKit/*

+ 22 - 0
src/TUIKit/.eslintrc.js

@@ -0,0 +1,22 @@
+module.exports = {
+  root: true,
+  env: {
+    node: true
+  },
+  extends: [
+    'plugin:vue/vue3-essential',
+    'eslint:recommended',
+    '@vue/typescript/recommended'
+  ],
+  parserOptions: {
+    ecmaVersion: 2020
+  },
+  rules: {
+    'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
+    'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
+    '@typescript-eslint/no-var-requires': 0,
+    'prefer-const': 0,
+    'vue/multi-word-component-names': 0,
+    'no-unsafe-optional-chaining': 'off'
+  }
+};

+ 330 - 0
src/TUIKit/.github/README.md

@@ -0,0 +1,330 @@
+## 关于腾讯云即时通信 IM
+
+腾讯云即时通信(Instant Messaging,IM)基于 QQ 底层 IM 能力开发,仅需植入 SDK 即可轻松集成聊天、会话、群组、资料管理能力,帮助您实现文字、图片、短语音、短视频等富媒体消息收发,全面满足通信需要。
+
+## 关于 chat-uikit-vue
+
+chat-uikit-vue 是基于腾讯云 Web IM SDK 的一款 VUE UI 组件库,它提供了一些通用的 UI 组件,包含会话、聊天、音视频通话、关系链、资料、群组等功能。基于 UI 组件您可以像搭积木一样快速搭建起自己的业务逻辑。
+chat-uikit-vue 中的组件在实现 UI 功能的同时,会调用 IM SDK 相应的接口实现 IM 相关逻辑和数据的处理,因而开发者在使用 chat-uikit-vue 时只需关注自身业务或个性化扩展即可。
+chat-uikit-vue Web 端 和 H5 端界面效果如下图所示:
+
+<img width="1015" alt="page02" src="https://user-images.githubusercontent.com/57951148/192585298-c79960ed-a6a9-4927-89b9-31c1b3f68740.png">
+<img width="2072" alt="page00" src="https://user-images.githubusercontent.com/57951148/192585375-6260280f-4a67-4b64-a908-efcedee1c253.png">
+
+本文介绍如何快速集成腾讯云 Web IM SDK 的 VUE UI 组件库。对于其他平台,请参考文档:
+
+[**chat-uikit-react**](https://github.com/TencentCloud/chat-uikit-react)
+
+[**chat-uikit-uniapp**](https://github.com/TencentCloud/chat-uikit-uniapp)
+
+[**chat-uikit-wechat**](https://github.com/TencentCloud/chat-uikit-wechat)
+
+[**chat-uikit-ios**](https://github.com/TencentCloud/chat-uikit-ios)
+
+[**chat-uikit-android**](https://github.com/TencentCloud/chat-uikit-android)
+
+[**chat-uikit-flutter**](https://github.com/TencentCloud/chat-uikit-flutter)
+
+## 发送您的第一条消息
+
+### 开发环境要求
+
+- Vue 3
+- TypeScript
+- sass(sass-loader 版本 <= 10.1.1)
+- node(12.13.0 <= node 版本 <= 17.0.0, 推荐使用 Node.js 官方 LTS 版本 16.17.0)
+- npm(版本请与 node 版本匹配)
+
+### TUIKit 源码集成 - github方式集成
+
+#### 步骤 1:创建项目
+
+TUIKit 支持使用 webpack 或 vite 创建项目工程,配置 Vue3 + TypeScript + sass。
+
+以下是使用 vue-cli 搭建项目工程示例,vite 及 create-vue 搭建示例请参考官网教程 [集成 TUIKit 基础功能](https://cloud.tencent.com/document/product/269/68493)。
+
+使用 vue-cli 方式创建项目, 配置 Vue3 + TypeScript + sass。
+如果您尚未安装 vue-cli ,可以在 terminal 或 cmd 中采用如下方式进行安装:
+
+```shell
+npm install -g @vue/cli@4.5.0 sass sass-loader@10.1.1
+```
+
+通过 vue-cli 创建项目,并选择下图中所选配置项。
+
+```shell
+vue create chat-example
+```
+
+![vue-cli-config](https://user-images.githubusercontent.com/57951148/201915919-c5359f15-d3d2-4c33-8764-f694943c956b.png)
+
+创建完成后,切换到项目所在目录
+
+```shell
+cd chat-example
+```
+
+#### 步骤 2:下载 TUIKit 组件
+
+通过 `git clone` 方式下载 TUIKit 组件及其相关依赖, 为了方便您的后续使用,建议您通过以下命令将整个 `chat-uikit-vue` 复制到您项目的 src目录下,并重命名为TUIKit:
+
+```shell
+# 项目根目录命令行执行
+git clone https://github.com/TencentCloud/chat-uikit-vue.git
+
+# 移动并重命名到src目录下
+# macOS
+mv chat-uikit-vue/TUIKit src/TUIKit
+# windows
+move chat-uikit-vue\TUIKit src\TUIKit
+
+# 安装TUIKit依赖
+npm i @tencentcloud/chat-uikit-vue --legacy-peer-deps
+```
+
+成功后目录结构如图所示:  
+<img width="300" src="https://user-images.githubusercontent.com/57951148/192585499-1a4edd85-43cc-4527-9f39-494b7d7e625a.png"/>
+
+#### 步骤 3:引入 TUIKit 组件
+
+在 main.ts 中,引入 TUIKit,并注册到 Vue 项目实例中:
+
+```javascript
+import { createApp } from 'vue';
+import App from './App.vue';
+import { TUIComponents, TUICore, genTestUserSig } from './TUIKit';
+// import TUICallKit
+import { TUICallKit } from '@tencentcloud/call-uikit-vue';
+
+const SDKAppID = 0; // Your SDKAppID
+const secretKey = ''; //Your secretKey
+const userID = ''; // User ID
+
+// init TUIKit
+const TUIKit = TUICore.init({
+  SDKAppID,
+});
+// TUIKit add TUIComponents
+TUIKit.use(TUIComponents);
+// TUIKit add TUICallKit
+TUIKit.use(TUICallKit);
+
+// login TUIKit
+TUIKit.login({
+  userID: userID,
+  userSig: genTestUserSig({
+    SDKAppID,
+    secretKey,
+    userID,
+  }).userSig, // The password with which the user logs in to IM. It is the ciphertext generated by encrypting information such as userID.For the detailed generation method, see Generating UserSig
+});
+
+createApp(App).use(TUIKit).mount('#app');
+```
+
+#### 步骤 4: 获取 SDKAppID 、密钥与 userID
+
+设置 main.ts 文件示例代码中的相关参数 SDKAppID、secretKey 以及 userID ,其中 SDKAppID 和密钥等信息,可通过 [即时通信 IM 控制台](https://console.cloud.tencent.com/im) 获取,单击目标应用卡片,进入应用的基础配置页面。例如:  
+![image](https://user-images.githubusercontent.com/57951148/192587785-6577cc5e-acf9-423c-86d0-52c67234ab1f.png)
+
+userID 信息,可通过 [即时通信 IM 控制台](https://console.cloud.tencent.com/im) 进行创建和获取,单击目标应用卡片,进入应用的账号管理页面,即可创建账号并获取 userID。例如:  
+![create user](https://user-images.githubusercontent.com/57951148/192585588-c5300d12-6bb5-45a4-831b-f7d733573840.png)
+
+#### 步骤 5:调用 TUIKit 组件
+
+在需要展示的页面,调用 TUIKit 的组件即可使用。
+例如:在 App.vue 页面中,使用 TUIConversation、TUIChat、TUISearch 快速搭建聊天界面。
+
+```javascript
+<template>
+  <div class="home-TUIKit-main">
+    <div
+      :class="env?.isH5 ? 'conversation-h5' : 'conversation'"
+      v-show="!env?.isH5 || currentModel === 'conversation'"
+    >
+      <TUISearch class="search" />
+      <TUIConversation @current="handleCurrentConversation" />
+    </div>
+    <div class="chat" v-show="!env?.isH5 || currentModel === 'message'">
+      <TUIChat>
+        <h1>欢迎使用腾讯云即时通信IM</h1>
+      </TUIChat>
+    </div>
+    <!-- TUICallKit 组件:通话 UI 组件主体 -->
+    <TUICallKit
+      :class="!showCallMini ? 'callkit-drag-container' : 'callkit-drag-container-mini'"
+      :allowedMinimized="true"
+      :allowedFullScreen="false"
+      :beforeCalling="beforeCalling"
+      :afterCalling="afterCalling"
+      :onMinimized="onMinimized"
+      :onMessageSentByMe="onMessageSentByMe"
+    />
+  </div>
+</template>
+
+<script lang="ts">
+import { defineComponent, reactive, toRefs } from "vue";
+import { TUIEnv } from "./TUIKit/TUIPlugin";
+import { handleErrorPrompts } from "./TUIKit/TUIComponents/container/utils";
+
+export default defineComponent({
+  name: "App",
+  setup() {
+    const data = reactive({
+      env: TUIEnv(),
+      currentModel: "conversation",
+      showCall: false,
+      showCallMini: false,
+    });
+    const TUIServer = (window as any)?.TUIKitTUICore?.TUIServer;
+    const handleCurrentConversation = (value: string) => {
+      data.currentModel = value ? "message" : "conversation";
+    };
+    // beforeCalling:在拨打电话前与收到通话邀请前执行
+    const beforeCalling = (type: string, error: any) => {
+      if (error) {
+        handleErrorPrompts(error, type);
+        return;
+      }
+      data.showCall = true;
+    };
+    // afterCalling:结束通话后执行
+    const afterCalling = () => {
+      data.showCall = false;
+      data.showCallMini = false;
+    };
+    // onMinimized:组件切换最小化状态时执行
+    const onMinimized = (
+      oldMinimizedStatus: boolean,
+      newMinimizedStatus: boolean
+    ) => {
+      data.showCall = !newMinimizedStatus;
+      data.showCallMini = newMinimizedStatus;
+    };
+    // onMessageSentByMe:在整个通话过程内发送消息时执行
+    const onMessageSentByMe = async (message: any) => {
+      TUIServer?.TUIChat?.handleMessageSentByMeToView(message);
+      return;
+    };
+    return {
+      ...toRefs(data),
+      handleCurrentConversation,
+      beforeCalling,
+      afterCalling,
+      onMinimized,
+      onMessageSentByMe,
+    };
+  },
+});
+</script>
+<style scoped>
+.home-TUIKit-main {
+  display: flex;
+  height: 100vh;
+  overflow: hidden;
+}
+.search {
+  padding: 12px;
+}
+.conversation {
+  min-width: 285px;
+  flex: 0 0 24%;
+  border-right: 1px solid #f4f5f9;
+}
+.conversation-h5 {
+  flex: 1;
+  border-right: 1px solid #f4f5f9;
+}
+.chat {
+  flex: 1;
+  height: 100%;
+  position: relative;
+}
+.callkit-drag-container {
+  position: fixed;
+  left: calc(50% - 25rem);
+  top: calc(50% - 18rem);
+  width: 50rem;
+  height: 36rem;
+  border-radius: 16px;
+  box-shadow: rgba(0, 0, 0, 0.16) 0px 3px 6px, rgba(0, 0, 0, 0.23) 0px 3px 6px;
+}
+.callkit-drag-container-mini {
+  position: fixed;
+  width: 168px;
+  height: 56px;
+  right: 10px;
+  top: 70px;
+}
+</style>
+
+```
+
+#### 步骤 6:启动项目
+在您项目的根目录执行:
+```javascript
+npm run serve
+```
+
+#### 步骤 7:发送您的第一条消息
+
+![send your first message](https://user-images.githubusercontent.com/57951148/192585549-2cc65785-0d6d-4d48-a0ce-0abe0b927bf4.png)
+
+#### 步骤 8: 拨打您的第一通电话
+自 @tencentcloud/chat-uikit-vue v1.4.0 版本起自动接入音视频通话功能,无需手动集成。
+如果您是 v1.4.0 以下版本,可以通过接入 call-uikit-vue 体验通话功能。详情请参考 [音视频通话 ( Web & H5 )](https://cloud.tencent.com/document/product/269/79861) 
+ <img width="1015" alt="page05" src="https://user-images.githubusercontent.com/57951148/196082955-e046f0b1-bba2-491d-91b3-f30f2c6f4aae.png">
+
+### 常见问题
+#### 1. TUIKit 与 Demo 有何区别?
+
+<table style="text-align:center; vertical-align:middle; width:1000px">
+  <tr>
+    <th style="text-align:center;" width="500px">TUIKit 运行效果</th>
+    <th style="text-align:center;" width="500px">Demo 运行效果</th>
+  </tr>
+  <tr>
+    <td><img style="width:500px" src="https://user-images.githubusercontent.com/57951148/225872424-c530e7ef-593d-472a-a77d-420d18bafefa.png"/></td>
+    <td><img style="width:500px" src="https://user-images.githubusercontent.com/57951148/225871366-f24b0abe-2829-4886-83fe-eb129338380a.png"/></td>
+   </tr>
+</table>
+
+TUIKit 是基于腾讯云 Web IM SDK 的一款 VUE UI 组件库,它提供了一些通用的 UI 组件,包含会话、聊天、音视频通话、关系链、资料、群组等功能。通过以上“TUIKit 源码集成”教程,您可以快速接入并体验 TUIKit 中的基础功能,并可以直接将 TUIKit 集成到您的现有项目中进行使用。
+
+Demo 是基于 TUIKit 搭建的一套完整的 即时通信含 UI 解决方案,他是一个基于 VUE3 + TS + TUIKit 的完整项目,其 views/Home.vue 文件中展示了丰富的 TUIKit 组合使用方案供您参考使用。
+ 
+
+
+#### 2. 什么是 UserSig?
+
+UserSig 是用户登录即时通信 IM 的密码,其本质是对 UserID 等信息加密后得到的密文。
+
+#### 3. 如何生成 UserSig?
+
+UserSig 签发方式是将 UserSig 的计算代码集成到您的服务端,并提供面向项目的接口,在需要 UserSig 时由您的项目向业务服务器发起请求获取动态 UserSig。更多详情请参见 [服务端生成 UserSig](https://cloud.tencent.com/document/product/269/32688#GeneratingdynamicUserSig)。
+
+> !
+>
+> 本文示例代码采用的获取 UserSig 的方案是在客户端代码中配置 SECRETKEY,该方法中 SECRETKEY 很容易被反编译逆向破解,一旦您的密钥泄露,攻击者就可以盗用您的腾讯云流量,因此**该方法仅适合本地跑通功能调试**。 正确的 UserSig 签发方式请参见上文。
+
+#### 4. Component name "XXXX" should always be multi-word
+
+- IM TUIKit web 所使用的 ESLint 版本为 v6.7.2 ,对于模块名的驼峰式格式并不进行严格校验
+- 如果您出现此问题,您可以在 .eslintrc.js 文件中进行如下配置:
+
+```javascript
+module.exports = {
+  ...
+  rules: {
+    ...
+    'vue/multi-word-component-names': 'warn',
+  },
+};
+```
+### 相关文档
+- [快速跑通 Demo](https://github.com/TencentCloud/chat-uikit-vue/tree/main/Demo)
+- [@tencentcloud/chat-uikit-vue npm仓库](https://www.npmjs.com/package/@tencentcloud/chat-uikit-vue)
+- [SDK API手册](https://web.sdk.qcloud.com/im/doc/zh-cn/SDK.html)
+- [SDK 更新日志](https://cloud.tencent.com/document/product/269/38492)
+- [音视频通话](https://cloud.tencent.com/document/product/269/79861) 

+ 117 - 0
src/TUIKit/CHANGELOG.md

@@ -0,0 +1,117 @@
+## 1.4.5 (2023-07-26)
+### 新增
+- 升级全新 V3 @tencentcloud/chat
+### 修复
+- 修复 "创建群聊" 消息 解析问题
+- 修复已知问题,提升稳定性
+
+## 1.4.4 (2023-06-16)
+### 修复
+- 修复 群评价消息 解析失败问题
+- 修复 @ 功能搜索选中 问题
+- 修复已知问题,提升稳定性
+
+## 1.4.3 (2023-05-10)
+### 修复
+- 修复 会话列表 消息在线状态 失效问题
+
+## 1.4.2 (2023-04-26)
+### 新增
+- 支持 创建群聊消息提示 解析显示
+
+
+## 1.4.1 (2023-03-31)
+### 新增
+- 支持 消息记录列表 显示消息时间
+### 修复
+- 修复 样式污染问题
+- 修复 H5 系统键盘覆盖输入框 问题
+- 修复 H5 大表情样式 问题
+
+## 1.4.0 (2023-03-24)
+### 新增
+- 支持 vite & webpack 接入,详见[集成 TUIKit](https://cloud.tencent.com/document/product/269/68493)
+- 支持 音视频通话(TUICallKit) 功能自动集成
+### 修复
+- 修复已知问题,提升稳定性
+
+## 1.3.6 (2023-03-21)
+### 新增
+- 优化“音视频通话”集成方式, 详见[使用音视频通话](https://cloud.tencent.com/document/product/269/79861)
+### 修复
+- 修复已知问题,提升稳定性
+
+## 1.3.5 (2023-03-16)
+### 新增
+- 支持消息输入区多类型消息混合输入
+- 支持消息输入区图片文件拖拽上传
+- 发布[ demo 源码](https://github.com/TencentCloud/chat-uikit-vue/)
+### 修复
+- 修复已知问题,提升稳定性
+
+## 1.3.4 (2023-03-03)
+### 修复
+- 修复安卓特定版本浏览器图片发送变形问题
+- 修复已知问题,提升稳定性
+
+## 1.3.3 (2023-02-17)
+### 新增
+- 新增消息通知插件 TUINotification,支持 Web 端离线消息通知功能。
+### 修复
+- 修复消息重发失败问题,提升稳定性
+
+## 1.3.2 (2023-02-10)
+### 新增
+- 新增图片预览功能:支持缩放、旋转、切换图片,并在 H5 环境下支持手势操作
+- 改进媒体类消息上屏逻辑,加入骨架屏占位功能
+### 修复
+- 修复已知问题,提升稳定性
+
+# 1.3.1 (2023-01-12)
+### 新增
+- 支持本地审核功能,使用本地审核功能请点击 [控制台](https://console.cloud.tencent.com/im/local-audit-setting) 开启,详情参见 [内容审核](https://cloud.tencent.com/document/product/269/79139)。
+- 支持群聊点击群成员列表头像跳转至用户信息界面
+- 支持接口开关直接控制 消息已读回执、对方正在输入、用户在线状态、消息表情评论 等功能
+### 修复
+- 修复已知问题,提升稳定性
+
+## 1.3.0 (2023-01-03)
+### 新增
+- 支持消息回复功能
+- 支持表情回应功能
+- 支持C2C会话单条消息已读 
+### 修复
+- 修复已知问题,提升稳定性
+
+## 1.2.5 (2022-12-05)
+### 修复
+- 修复已知问题,提升稳定性
+
+## 1.2.4 (2022-11-18)
+### 修复
+- 修复已知问题,提升稳定性
+
+## 1.2.3 (2022-11-16)
+### 新增
+- 支持 github 仓库形式接入源码
+### 修复
+- 修复已知问题,提升稳定性
+
+
+## 1.2.2 (2022-11-10)
+
+### 新增
+- 支持集成 [音视频通话](https://cloud.tencent.com/document/product/269/79861)
+### 修复
+- 修复已知问题,提升稳定性
+
+## 1.0.0 (2022-09-15)
+
+### 新增
+- [TUIKit界面库 - Web](https://cloud.tencent.com/document/product/269/79737)
+- [TUIKit界面库 - H5](https://cloud.tencent.com/document/product/269/79738)
+- [集成基础功能 - Web & H5](https://cloud.tencent.com/document/product/269/68493)
+- [设置界面风格 - Web](https://cloud.tencent.com/document/product/269/79113)
+- [设置界面风格 - H5](https://cloud.tencent.com/document/product/269/79836)
+- [添加自定义消息 - Web](https://cloud.tencent.com/document/product/269/79115)
+- [添加自定义消息 - H5](https://cloud.tencent.com/document/product/269/79837)

+ 7 - 0
src/TUIKit/TUIComponents/assets/icon/arrow-left-center.svg

@@ -0,0 +1,7 @@
+<?xml version="1.0" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg class="icon" width="32px" height="32.00px" viewBox="0 0 1024 1024" version="1.1"
+  xmlns="http://www.w3.org/2000/svg">
+  <path fill="#444444"
+    d="M384 512L731.733333 202.666667c17.066667-14.933333 19.2-42.666667 4.266667-59.733334-14.933333-17.066667-42.666667-19.2-59.733333-4.266666l-384 341.333333c-10.666667 8.533333-14.933333 19.2-14.933334 32s4.266667 23.466667 14.933334 32l384 341.333333c8.533333 6.4 19.2 10.666667 27.733333 10.666667 12.8 0 23.466667-4.266667 32-14.933333 14.933333-17.066667 14.933333-44.8-4.266667-59.733334L384 512z" />
+</svg>

Різницю між файлами не показано, бо вона завелика
+ 7 - 0
src/TUIKit/TUIComponents/assets/icon/at.svg


+ 16 - 0
src/TUIKit/TUIComponents/assets/icon/back.svg

@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="48px" height="48px" viewBox="0 0 48 48" version="1.1">
+  <title>ic_back_white</title>
+  <g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+    <g id="02.-查看信息-示例好友" transform="translate(-32.000000, -200.000000)">
+      <g id="编组-6" transform="translate(0.000000, 176.000000)">
+        <g id="ic_back_white" transform="translate(32.000000, 24.000000)">
+          <g id="ic_back_black" fill="#444444" fill-rule="nonzero">
+            <path d="M9.51231071,24 L23.5198286,38.7290411 C23.785378,39.0082689 23.7753168,39.4496082 23.4973187,39.7164448 L21.6255915,41.5130226 C21.3466827,41.7807332 20.9035602,41.7716553 20.6358496,41.4927466 C20.6351032,41.491969 20.6343586,41.4911896 20.6336157,41.4904085 L4.45876399,24.4823948 C4.2017544,24.2121467 4.2017544,23.7878533 4.45876399,23.5176052 L20.6336157,6.50959152 C20.900035,6.22944906 21.3431109,6.21832422 21.6232533,6.48474351 C21.6240344,6.48548634 21.6248138,6.48623097 21.6255915,6.48697741 L23.4973187,8.28355524 C23.7753168,8.55039175 23.785378,8.99173114 23.5198286,9.27095893 L9.51231071,24 L9.51231071,24 Z" id="Path-2"></path>
+          </g>
+          <rect id="矩形" x="0" y="0" width="48" height="48"></rect>
+        </g>
+      </g>
+    </g>
+  </g>
+</svg>

Різницю між файлами не показано, бо вона завелика
+ 20 - 0
src/TUIKit/TUIComponents/assets/icon/c2c.svg


BIN
src/TUIKit/TUIComponents/assets/icon/call.png


BIN
src/TUIKit/TUIComponents/assets/icon/cancel.png


+ 23 - 0
src/TUIKit/TUIComponents/assets/icon/cancel.svg

@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16px" height="16px" viewBox="0 0 16 16" version="1.1">
+  <title>清除</title>
+  <g id="页面-2备份" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+    <g id="创建群聊" transform="translate(-1243.000000, -413.000000)">
+      <g id="Group-1364" transform="translate(650.000000, 343.000000)">
+        <g id="Group-1363" transform="translate(290.000000, 46.000000)">
+          <g id="Group-1358" transform="translate(0.000000, 24.000000)">
+            <g id="清除" transform="translate(303.000000, 0.000000)">
+              <path d="M0,0 L16,0 L16,16 L0,16 L0,0 Z" id="矩形"></path>
+              <path d="M1,8 C1,11.8659999 4.13400006,15 8,15 C11.8659999,15 15,11.8659999 15,8 C15,4.13400006 11.8659999,1 8,1 L8,1 C4.13400006,1 1,4.13400006 1,8 Z" fill="#999999"></path>
+              <g id="编组" transform="translate(8.000000, 8.000000) rotate(-315.000000) translate(-8.000000, -8.000000) translate(4.000000, 4.000000)" fill="#FFFFFF">
+                <rect id="矩形备份" transform="translate(4.000000, 4.000000) rotate(-90.000000) translate(-4.000000, -4.000000) " x="1.8189894e-12" y="3.5" width="8" height="1" rx="0.5"></rect>
+                <rect id="矩形备份" transform="translate(4.000000, 4.000000) rotate(-360.000000) translate(-4.000000, -4.000000) " x="0" y="3.5" width="8" height="1" rx="0.5"></rect>
+              </g>
+              <rect id="矩形" x="0" y="0" width="16" height="16"></rect>
+            </g>
+          </g>
+        </g>
+      </g>
+    </g>
+  </g>
+</svg>

BIN
src/TUIKit/TUIComponents/assets/icon/chat-setting.png


BIN
src/TUIKit/TUIComponents/assets/icon/close-image.png


BIN
src/TUIKit/TUIComponents/assets/icon/close.png


+ 4 - 0
src/TUIKit/TUIComponents/assets/icon/collapse.svg

@@ -0,0 +1,4 @@
+<svg width="17" height="18" viewBox="0 0 17 18" fill="none" xmlns="http://www.w3.org/2000/svg">
+<rect x="0.5" y="-0.5" width="15.8" height="15.8" rx="7.9" transform="matrix(1 0 0 -1 0 16.3999)" stroke="#666666"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M4.15745 10.4828C4.00124 10.3266 4.00124 10.0733 4.15745 9.91711L8.11724 5.95731C8.27345 5.8011 8.52672 5.8011 8.68293 5.95731L8.82435 6.09873C8.82645 6.10083 8.82852 6.10295 8.83057 6.10509L12.6427 9.91725C12.7989 10.0735 12.7989 10.3267 12.6427 10.4829L12.5013 10.6244C12.3451 10.7806 12.0918 10.7806 11.9356 10.6244L8.40002 7.08875L4.86455 10.6242C4.70834 10.7804 4.45508 10.7804 4.29887 10.6242L4.15745 10.4828Z" fill="#666666"/>
+</svg>

BIN
src/TUIKit/TUIComponents/assets/icon/custom.png


BIN
src/TUIKit/TUIComponents/assets/icon/del.png


+ 1 - 0
src/TUIKit/TUIComponents/assets/icon/double-arrow.svg

@@ -0,0 +1 @@
+<svg t="1660053550964" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3181" width="200" height="200"><path d="M95.658667 461.354667a71.637333 71.637333 0 0 0 0 101.290666l365.696 365.696a71.637333 71.637333 0 0 0 101.290666 0l365.696-365.696a71.637333 71.637333 0 1 0-101.248-101.290666L512 776.448l-315.093333-315.093333a71.594667 71.594667 0 0 0-101.248 0z" p-id="3182" fill="#bfbfbf"></path><path d="M95.658667 95.658667a71.637333 71.637333 0 0 0 0 101.248l365.696 365.738666a71.594667 71.594667 0 0 0 101.290666 0l365.696-365.738666a71.637333 71.637333 0 0 0-101.248-101.248L512 410.709333 196.906667 95.658667a71.637333 71.637333 0 0 0-101.248 0z" p-id="3183" fill="#bfbfbf"></path></svg>

BIN
src/TUIKit/TUIComponents/assets/icon/down.png


BIN
src/TUIKit/TUIComponents/assets/icon/downaload-image.png


BIN
src/TUIKit/TUIComponents/assets/icon/edit.png


+ 27 - 0
src/TUIKit/TUIComponents/assets/icon/emoj.svg

@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16px" height="16px" viewBox="0 0 16 16" version="1.1">
+  <title>ic/消息备份 8</title>
+  <defs>
+    <circle id="path-1" cx="8" cy="8" r="8"></circle>
+    <mask id="mask-2" maskContentUnits="userSpaceOnUse" maskUnits="objectBoundingBox" x="0" y="0" width="16" height="16" fill="white">
+      <use xlink:href="#path-1"></use>
+    </mask>
+  </defs>
+  <g id="new" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" opacity="0.795168922">
+    <g id="消息列表" transform="translate(-816.000000, -700.000000)">
+      <g id="输入框" transform="translate(796.000000, 686.000000)">
+        <g id="编组-11" transform="translate(20.000000, 11.000000)">
+          <g transform="translate(0.000000, 3.000000)" id="ic/消息备份-8">
+            <g id="编组-8备份">
+              <path d="M0,0 L16,0 L16,16 L0,16 L0,0 Z" id="矩形备份-3"></path>
+            </g>
+            <path d="M0,0 L16,0 L16,16 L0,16 L0,0 Z" id="矩形"></path>
+            <use id="椭圆形" stroke="#232832" mask="url(#mask-2)" stroke-width="2" stroke-dasharray="0,0" xlink:href="#path-1"></use>
+            <path d="M6,6 C6,6.55228475 5.55228475,7 5,7 C4.44771525,7 4,6.55228475 4,6 C4,5.44771525 4.44771525,5 5,5 C5.55228475,5 6,5.44771525 6,6 Z" id="矩形" fill="#232832"></path>
+            <path d="M12,6 C12,6.55228475 11.5522847,7 11,7 C10.4477153,7 10,6.55228475 10,6 C10,5.44771525 10.4477153,5 11,5 C11.5522847,5 12,5.44771525 12,6 Z" id="矩形备份" fill="#232832"></path>
+          </g>
+        </g>
+      </g>
+    </g>
+  </g>
+</svg>

BIN
src/TUIKit/TUIComponents/assets/icon/evaluate.png


+ 25 - 0
src/TUIKit/TUIComponents/assets/icon/evaluate.svg

@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16px" height="16px" viewBox="0 0 16 16" version="1.1">
+  <title>编组 9备份 4</title>
+  <defs>
+    <rect id="path-1" x="1" y="1" width="14" height="14" rx="2.25"></rect>
+    <mask id="mask-2" maskContentUnits="userSpaceOnUse" maskUnits="objectBoundingBox" x="0" y="0" width="14" height="14" fill="white">
+      <use xlink:href="#path-1"></use>
+    </mask>
+  </defs>
+  <g id="new" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" opacity="0.795168922">
+    <g id="消息列表" transform="translate(-1040.000000, -700.000000)">
+      <g id="输入框" transform="translate(796.000000, 686.000000)">
+        <g id="编组-11" transform="translate(20.000000, 11.000000)">
+          <g transform="translate(0.000000, 3.000000)" id="编组-9备份-4">
+            <g transform="translate(224.000000, 0.000000)">
+              <rect id="矩形" x="0" y="0" width="16" height="16"></rect>
+              <use id="矩形" stroke="#232832" mask="url(#mask-2)" stroke-width="2" stroke-dasharray="0,0" transform="translate(8.000000, 8.000000) rotate(-90.000000) translate(-8.000000, -8.000000) " xlink:href="#path-1"></use>
+              <path d="M7.90693177,10.0489289 L6.00755342,11.0474912 C5.9097845,11.0988913 5.78885907,11.061302 5.73745891,10.9635331 C5.7169911,10.924601 5.70992816,10.8800074 5.71736353,10.8366559 L6.08011252,8.72166698 C6.09124011,8.65678817 6.06973041,8.59058813 6.02259319,8.5446407 L4.48596383,7.04679726 C4.40686711,6.96969701 4.40524865,6.84307436 4.48234889,6.76397764 C4.51305061,6.73248092 4.55327909,6.71198348 4.5968065,6.70565858 L6.72037606,6.39708586 C6.78551809,6.38762017 6.84183121,6.34670629 6.87096361,6.2876776 L7.82065278,4.36339715 C7.86953724,4.26434642 7.98946241,4.22367862 8.08851314,4.27256308 C8.12795566,4.29202912 8.15988117,4.32395463 8.17934722,4.36339715 L9.12903639,6.2876776 C9.15816879,6.34670629 9.21448191,6.38762017 9.27962394,6.39708586 L11.4031935,6.70565858 C11.5125025,6.7215421 11.5882388,6.82303067 11.5723552,6.93233965 C11.5660303,6.97586705 11.5455329,7.01609553 11.5140362,7.04679726 L9.97740681,8.5446407 C9.93026959,8.59058813 9.90875989,8.65678817 9.91988748,8.72166698 L10.2826365,10.8366559 C10.3013087,10.9455231 10.2281911,11.0489143 10.1193238,11.0675865 C10.0759723,11.0750219 10.0313787,11.067959 9.99244658,11.0474912 L8.09306823,10.0489289 C8.03480342,10.0182972 7.96519658,10.0182972 7.90693177,10.0489289 Z" id="星形" stroke="#232832"></path>
+            </g>
+          </g>
+        </g>
+      </g>
+    </g>
+  </g>
+</svg>

+ 4 - 0
src/TUIKit/TUIComponents/assets/icon/expand.svg

@@ -0,0 +1,4 @@
+<svg width="17" height="18" viewBox="0 0 17 18" fill="none" xmlns="http://www.w3.org/2000/svg">
+<rect x="0.5" y="-0.5" width="15.8" height="15.8" rx="7.9" transform="matrix(1 0 0 -1 0 16.3999)" stroke="#666666"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M12.6427 7.24255C12.7989 7.39876 12.7989 7.65202 12.6427 7.80823L8.68293 11.768C8.52672 11.9242 8.27345 11.9242 8.11724 11.768L7.97582 11.6266C7.97408 11.6249 7.97236 11.6231 7.97067 11.6214L4.15744 7.80814C4.00123 7.65193 4.00123 7.39867 4.15744 7.24246L4.29887 7.10103C4.45508 6.94482 4.70834 6.94482 4.86455 7.10103L8.40013 10.6366L11.9356 7.10113C12.0918 6.94492 12.3451 6.94492 12.5013 7.10113L12.6427 7.24255Z" fill="#666666"/>
+</svg>

BIN
src/TUIKit/TUIComponents/assets/icon/face.png


BIN
src/TUIKit/TUIComponents/assets/icon/files.png


BIN
src/TUIKit/TUIComponents/assets/icon/gobackground.png


+ 6 - 0
src/TUIKit/TUIComponents/assets/icon/icon-close-h5.svg

@@ -0,0 +1,6 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 62 62" class="design-iconfont">
+  <g fill="none" fill-rule="evenodd">
+    <path fill="#4D4D4D" d="M31 0A31 31 0 1 0 31 62A31 31 0 1 0 31 0Z"/>
+    <path d="M20.768687,18.3379271 L31,28.569 L41.231313,18.3379271 C41.5917969,17.9774431 42.159028,17.9497136 42.5513192,18.2547385 L42.6455265,18.3379271 L43.6620729,19.3544735 C44.0525972,19.7449978 44.0525972,20.3781627 43.6620729,20.768687 L43.6620729,20.768687 L33.431,31 L43.6620729,41.231313 C44.0525972,41.6218373 44.0525972,42.2550022 43.6620729,42.6455265 L42.6455265,43.6620729 C42.2550022,44.0525972 41.6218373,44.0525972 41.231313,43.6620729 L31,33.431 L20.768687,43.6620729 C20.4082031,44.0225569 19.840972,44.0502864 19.4486808,43.7452615 L19.3544735,43.6620729 L18.3379271,42.6455265 C17.9474028,42.2550022 17.9474028,41.6218373 18.3379271,41.231313 L18.3379271,41.231313 L28.569,31 L18.3379271,20.768687 C17.9474028,20.3781627 17.9474028,19.7449978 18.3379271,19.3544735 L19.3544735,18.3379271 C19.7449978,17.9474028 20.3781627,17.9474028 20.768687,18.3379271 Z" fill="#FFF" fill-rule="nonzero"/>
+  </g>
+</svg>

+ 6 - 0
src/TUIKit/TUIComponents/assets/icon/icon-download-h5.svg

@@ -0,0 +1,6 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 62 62" class="design-iconfont">
+  <g fill="none" fill-rule="evenodd">
+    <path fill="#4D4D4D" d="M31 0A31 31 0 1 0 31 62A31 31 0 1 0 31 0Z"/>
+    <path d="M14.7359124,0 C15.2881971,-1.01453063e-16 15.7359124,0.44771525 15.7359124,1 L15.735,21.344 L23.7580265,13.6968792 C24.1578097,13.3158576 24.790775,13.3310181 25.1718581,13.7307426 L26.5518275,15.1783906 C26.9328955,15.5781475 26.9177451,16.2111312 26.5179882,16.5921992 L14.4478609,28.0980396 C14.0646983,28.463289 13.4632705,28.466657 13.0760413,28.1057217 L0.731506115,16.5994137 C0.327506443,16.2228467 0.305267874,15.5900724 0.68183488,15.1860727 L2.04550464,13.7230605 C2.42208199,13.3190823 3.05484394,13.2968371 3.45886079,13.673373 L11.735,21.388 L11.7359124,1 C11.7359124,0.44771525 12.1836276,1.01453063e-16 12.7359124,0 L14.7359124,0 Z" transform="translate(17 17)" fill="#FFF" fill-rule="nonzero"/>
+  </g>
+</svg>

+ 3 - 0
src/TUIKit/TUIComponents/assets/icon/icon-download.svg

@@ -0,0 +1,3 @@
+<svg width="22" height="20" viewBox="0 0 22 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M3.09661 19.8415C1.38654 19.8415 0 18.4764 0 16.7919V12.9796C0 12.5587 0.346831 12.2173 0.774001 12.2173H2.3226C2.75004 12.2173 3.0966 12.5587 3.0966 12.9796V15.267C3.0966 16.1093 3.78974 16.7919 4.6449 16.7919H17.0317C17.8869 16.7919 18.58 16.1093 18.58 15.267V12.9796C18.58 12.5587 18.9266 12.2173 19.3543 12.2173H20.9026C21.3301 12.2173 21.6766 12.5587 21.6766 12.9796V16.7919C21.6766 18.4764 20.2904 19.8415 18.58 19.8415H18.58H3.09661ZM5.67942 4.88325L9.2903 8.43944V0.762639C9.2903 0.341674 9.63693 0 10.0643 0H11.6126C12.0401 0 12.3869 0.341674 12.3869 0.762639V8.43944L15.9978 4.88325C16.311 4.57485 16.8189 4.57485 17.1321 4.88325L18.2668 6.00088C18.58 6.30935 18.58 6.80951 18.2668 7.11823L11.8728 13.4154C11.8452 13.4426 11.3477 13.7421 10.8471 13.7423C10.3411 13.7425 9.83204 13.4429 9.80436 13.4154L3.41009 7.11823C3.09687 6.80949 3.09687 6.30935 3.41009 6.00088L4.54488 4.88325H4.54487C4.85807 4.57485 5.3662 4.57485 5.67942 4.88325Z" fill="white"/>
+</svg>

BIN
src/TUIKit/TUIComponents/assets/icon/image.png


BIN
src/TUIKit/TUIComponents/assets/icon/location.png


BIN
src/TUIKit/TUIComponents/assets/icon/meeting.png


BIN
src/TUIKit/TUIComponents/assets/icon/message.png


BIN
src/TUIKit/TUIComponents/assets/icon/more.png


+ 30 - 0
src/TUIKit/TUIComponents/assets/icon/msg-copy.svg

@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="40px" height="40px" viewBox="0 0 40 40" version="1.1">
+  <title>编组 14</title>
+  <defs>
+    <path d="M467,0 C474.731986,-2.71135202e-14 481,6.2680135 481,14 L481,247 C481,254.731986 474.731986,261 467,261 L353.036,261 L340.862492,273.204941 C339.302377,274.769018 336.769719,274.77223 335.205642,273.212116 C335.203247,273.209727 335.200856,273.207335 335.198467,273.204941 L323.024,261 L14,261 C6.2680135,261 2.72325209e-15,254.731986 0,247 L0,14 C8.29461588e-16,6.2680135 6.2680135,3.19669972e-15 14,0 L467,0 Z" id="path-1"></path>
+    <filter x="-10.1%" y="-14.0%" width="120.2%" height="135.4%" filterUnits="objectBoundingBox" id="filter-2">
+      <feOffset dx="0" dy="10" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
+      <feGaussianBlur stdDeviation="14.5" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
+      <feColorMatrix values="0 0 0 0 0   0 0 0 0 0   0 0 0 0 0  0 0 0 0.06 0" type="matrix" in="shadowBlurOuter1"></feColorMatrix>
+    </filter>
+  </defs>
+  <g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+    <g id="消息状态" transform="translate(-541.000000, -196.000000)">
+      <g id="编组-11" transform="translate(499.000000, 163.000000)">
+        <g id="形状结合">
+          <use fill="black" fill-opacity="1" filter="url(#filter-2)" xlink:href="#path-1"></use>
+          <use fill="#FFFFFF" fill-rule="evenodd" xlink:href="#path-1"></use>
+        </g>
+        <g id="编组-19" transform="translate(8.000000, 1.000000)" fill-rule="nonzero">
+          <g id="编组-17" transform="translate(34.000000, 32.000000)">
+            <g id="编组-14" transform="translate(0.000000, 0.249660)">
+              <rect id="矩形" stroke="#444444" stroke-width="4" x="6" y="11.7115453" width="20.4869565" height="24.5849258" rx="1"></rect>
+              <path d="M36,2.01560374 L36,28.7262543 C36,29.0023967 35.7761424,29.2262543 35.5,29.2262543 L32.4652074,29.2262543 C32.1890657,29.2262525 31.9652084,29.002396 31.9652057,28.7262543 L31.9651258,6.04681123 L31.9651258,6.04681123 L12.2913043,6.04719144 C12.015162,6.04720643 11.7913,5.82335314 11.7912947,5.54721076 C11.7912947,5.54720754 11.7912947,5.54720432 11.7913043,5.5472011 L11.7913043,2.51560374 C11.7913043,2.23946137 12.015162,2.01560374 12.2913043,2.01560374 L36,2.01560374 L36,2.01560374 Z" id="路径-3" fill="#444444"></path>
+            </g>
+          </g>
+        </g>
+      </g>
+    </g>
+  </g>
+</svg>

+ 33 - 0
src/TUIKit/TUIComponents/assets/icon/msg-del.svg

@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="40px" height="40px" viewBox="0 0 40 40" version="1.1">
+  <title>矩形</title>
+  <defs>
+    <path d="M467,0 C474.731986,-2.71135202e-14 481,6.2680135 481,14 L481,247 C481,254.731986 474.731986,261 467,261 L353.036,261 L340.862492,273.204941 C339.302377,274.769018 336.769719,274.77223 335.205642,273.212116 C335.203247,273.209727 335.200856,273.207335 335.198467,273.204941 L323.024,261 L14,261 C6.2680135,261 2.72325209e-15,254.731986 0,247 L0,14 C8.29461588e-16,6.2680135 6.2680135,3.19669972e-15 14,0 L467,0 Z" id="path-1"></path>
+    <filter x="-10.1%" y="-14.0%" width="120.2%" height="135.4%" filterUnits="objectBoundingBox" id="filter-2">
+      <feOffset dx="0" dy="10" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
+      <feGaussianBlur stdDeviation="14.5" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
+      <feColorMatrix values="0 0 0 0 0   0 0 0 0 0   0 0 0 0 0  0 0 0 0.06 0" type="matrix" in="shadowBlurOuter1"></feColorMatrix>
+    </filter>
+  </defs>
+  <g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+    <g id="消息状态" transform="translate(-779.000000, -326.000000)">
+      <g id="编组-11" transform="translate(499.000000, 163.000000)">
+        <g id="形状结合">
+          <use fill="black" fill-opacity="1" filter="url(#filter-2)" xlink:href="#path-1"></use>
+          <use fill="#FFFFFF" fill-rule="evenodd" xlink:href="#path-1"></use>
+        </g>
+        <g id="编组-13备份" transform="translate(246.000000, 131.583780)">
+          <g id="编组-17" transform="translate(34.000000, 31.416220)">
+            <g id="编组-18" transform="translate(4.000000, 2.847939)">
+              <path d="M27,8.04349833 L27,34.26099 L5,34.26099 L5,8.04349833 L27,8.04349833 Z" id="路径-5" stroke="#444444" stroke-width="4"></path>
+              <rect id="矩形" fill="#444444" x="9.14285714" y="0" width="14" height="4.02899889"></rect>
+              <rect id="矩形" fill="#444444" x="0" y="6.04349833" width="32" height="4.02899889"></rect>
+              <path d="M14,15.1087458 L14,27.1957425 L10,27.1957425 L10,15.1087458 L14,15.1087458 Z M22,15.1087458 L22,27.1957425 L18,27.1957425 L18,15.1087458 L22,15.1087458 Z" id="形状结合" fill="#444444"></path>
+            </g>
+          </g>
+        </g>
+        <g id="编组-19" transform="translate(8.000000, 1.000000)"></g>
+      </g>
+    </g>
+  </g>
+</svg>

+ 31 - 0
src/TUIKit/TUIComponents/assets/icon/msg-forward.svg

@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="40px" height="40px" viewBox="0 0 40 40" version="1.1">
+  <title>编组</title>
+  <defs>
+    <path d="M467,0 C474.731986,-2.71135202e-14 481,6.2680135 481,14 L481,247 C481,254.731986 474.731986,261 467,261 L353.036,261 L340.862492,273.204941 C339.302377,274.769018 336.769719,274.77223 335.205642,273.212116 C335.203247,273.209727 335.200856,273.207335 335.198467,273.204941 L323.024,261 L14,261 C6.2680135,261 2.72325209e-15,254.731986 0,247 L0,14 C8.29461588e-16,6.2680135 6.2680135,3.19669972e-15 14,0 L467,0 Z" id="path-1"></path>
+    <filter x="-10.1%" y="-14.0%" width="120.2%" height="135.4%" filterUnits="objectBoundingBox" id="filter-2">
+      <feOffset dx="0" dy="10" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
+      <feGaussianBlur stdDeviation="14.5" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
+      <feColorMatrix values="0 0 0 0 0   0 0 0 0 0   0 0 0 0 0  0 0 0 0.06 0" type="matrix" in="shadowBlurOuter1"></feColorMatrix>
+    </filter>
+  </defs>
+  <g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+    <g id="消息状态" transform="translate(-660.000000, -196.000000)">
+      <g id="编组-11" transform="translate(499.000000, 163.000000)">
+        <g id="形状结合">
+          <use fill="black" fill-opacity="1" filter="url(#filter-2)" xlink:href="#path-1"></use>
+          <use fill="#FFFFFF" fill-rule="evenodd" xlink:href="#path-1"></use>
+        </g>
+        <g id="编组-19" transform="translate(8.000000, 1.000000)" fill-rule="nonzero" stroke="#444444" stroke-width="4">
+          <g id="编组-13" transform="translate(119.000000, 0.000000)">
+            <g id="编组-17" transform="translate(34.000000, 32.000000)">
+              <g id="编组" transform="translate(0.000000, 0.249660)">
+                <path d="M23.1265517,6.28055724 L35.7271185,19.5748426 L23.0577232,32.8831329 L23.0577232,25.1147287 C22.2832234,25.0144246 21.6206186,24.9731478 21.1119625,24.9593482 C19.8469875,24.9250298 17.622079,25.0087745 15.0567889,25.7301995 C10.5501298,27.0035633 6.83444935,29.6335962 4.00998374,33.593477 C3.98525766,32.7188606 3.9976824,31.6416134 4.10230378,30.4471653 C4.26498285,28.6221312 4.60551504,26.8911737 5.12965716,25.3080362 C5.77312173,23.3656805 6.68732788,21.6451437 7.85433875,20.1962611 C11.1297915,16.1439451 16.3122766,14.2888953 23.1265517,14.4503809 L23.1265517,14.4503809 L23.1265517,6.28055724 Z" id="路径"></path>
+              </g>
+            </g>
+          </g>
+        </g>
+      </g>
+    </g>
+  </g>
+</svg>

+ 30 - 0
src/TUIKit/TUIComponents/assets/icon/msg-play.svg

@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="130px" height="130px" viewBox="0 0 130 130" version="1.1">
+  <title>编组</title>
+  <defs>
+    <filter x="-20.0%" y="-20.0%" width="140.0%" height="140.0%" filterUnits="objectBoundingBox" id="filter-1">
+      <feOffset dx="0" dy="0" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
+      <feGaussianBlur stdDeviation="5" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
+      <feColorMatrix values="0 0 0 0 0   0 0 0 0 0   0 0 0 0 0  0 0 0 0.15 0" type="matrix" in="shadowBlurOuter1" result="shadowMatrixOuter1"></feColorMatrix>
+      <feMerge>
+        <feMergeNode in="shadowMatrixOuter1"></feMergeNode>
+        <feMergeNode in="SourceGraphic"></feMergeNode>
+      </feMerge>
+    </filter>
+  </defs>
+  <g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+    <g id="消息状态" transform="translate(-1210.000000, -966.000000)">
+      <g id="编组-3" transform="translate(1055.000000, 783.000000)">
+        <g id="编组-17" transform="translate(60.000000, 37.996517)">
+          <g id="编组-14" transform="translate(0.000000, 0.003483)">
+            <g id="编组" transform="translate(110.000000, 160.000000)" filter="url(#filter-1)">
+              <rect id="矩形" x="0" y="0" width="100" height="100"></rect>
+              <path d="M67.4424727,46.9165576 L41.2606545,30.5526788 C40.1396848,29.8520727 38.7265939,29.8147394 37.5704727,30.4557091 C36.4143515,31.0966788 35.6970182,32.3143758 35.6970182,33.6363152 L35.6970182,66.3635879 C35.6970182,67.6855273 36.4143515,68.9032242 37.5704727,69.5441939 C38.1198061,69.8486788 38.7268364,69.9999515 39.3331394,69.9999515 C40.0029576,69.9999515 40.6720485,69.8152242 41.2604121,69.4472242 L67.4422303,53.0838303 C68.505503,52.4193455 69.1513213,51.2540121 69.1513213,50.0001939 C69.1513213,48.7463758 68.5057455,47.5810424 67.4424727,46.9165576" id="Fill-1" fill="#FFFFFF"></path>
+              <path d="M50,10 C27.944,10 10,27.944 10,50 C10,72.0557576 27.944,90 50,90 C72.056,90 90,72.0557576 90,50 C90,27.944 72.056,10 50,10" id="路径" stroke="#FFFFFF" stroke-width="6"></path>
+            </g>
+          </g>
+        </g>
+      </g>
+    </g>
+  </g>
+</svg>

+ 8 - 0
src/TUIKit/TUIComponents/assets/icon/msg-qnote.svg

@@ -0,0 +1,8 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 41" class="design-iconfont">
+  <g fill="none" fill-rule="evenodd">
+    <path d="M34,2 L34,28.6209709 L20.7153357,28.6209709 L17.9993455,31.4022694 L15.2847485,28.6219998 L2,28.6219998 L2,2 L34,2 Z" transform="translate(2 3.273066)" fill-rule="nonzero" stroke="#444" stroke-width="4"/>
+    <path fill="#444" d="M11 16.12483H15V20.15603749H11z" transform="translate(0 .24966)"/>
+    <path fill="#444" d="M18 16.12483H22V20.15603749H18z" transform="translate(0 .24966)"/>
+    <path fill="#444" d="M25 16.12483H29V20.15603749H25z" transform="translate(0 .24966)"/>
+  </g>
+</svg>

+ 31 - 0
src/TUIKit/TUIComponents/assets/icon/msg-reply.svg

@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="40px" height="40px" viewBox="0 0 40 40" version="1.1">
+  <title>编组 12</title>
+  <defs>
+    <path d="M467,0 C474.731986,-2.71135202e-14 481,6.2680135 481,14 L481,247 C481,254.731986 474.731986,261 467,261 L353.036,261 L340.862492,273.204941 C339.302377,274.769018 336.769719,274.77223 335.205642,273.212116 C335.203247,273.209727 335.200856,273.207335 335.198467,273.204941 L323.024,261 L14,261 C6.2680135,261 2.72325209e-15,254.731986 0,247 L0,14 C8.29461588e-16,6.2680135 6.2680135,3.19669972e-15 14,0 L467,0 Z" id="path-1"></path>
+    <filter x="-10.1%" y="-14.0%" width="120.2%" height="135.4%" filterUnits="objectBoundingBox" id="filter-2">
+      <feOffset dx="0" dy="10" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
+      <feGaussianBlur stdDeviation="14.5" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
+      <feColorMatrix values="0 0 0 0 0   0 0 0 0 0   0 0 0 0 0  0 0 0 0.06 0" type="matrix" in="shadowBlurOuter1"></feColorMatrix>
+    </filter>
+  </defs>
+  <g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+    <g id="消息状态" transform="translate(-541.000000, -328.000000)">
+      <g id="编组-11" transform="translate(499.000000, 163.000000)">
+        <g id="形状结合">
+          <use fill="black" fill-opacity="1" filter="url(#filter-2)" xlink:href="#path-1"></use>
+          <use fill="#FFFFFF" fill-rule="evenodd" xlink:href="#path-1"></use>
+        </g>
+        <g id="编辑功能" transform="translate(8.000000, 131.583780)" fill-rule="nonzero">
+          <g id="编组-17" transform="translate(34.000000, 32.416220)">
+            <g id="编组" transform="translate(0.000000, 3.000000)">
+              <path d="M30,2 L30,24.7410578 L11.7126857,24.7410578 L8.99940621,27.5321142 L6.28736501,24.7420913 L2,24.7420913 L2,2 L30,2 Z" id="路径" stroke="#444444" stroke-width="4"></path>
+              <path d="M38,12.0541319 C38.5522847,12.0541319 39,12.5038667 39,13.0586428 L39,13.0586428 L39,32.7691572 C39,33.3239333 38.5522847,33.7736682 38,33.7736682 L38,33.7736682 L33.5577151,33.7736682 L30.6995611,36.7141728 C30.3044253,37.101771 29.671305,37.0942155 29.2854474,36.6972972 L29.2854474,36.6972972 L26.4422849,33.7726347 L15,33.7726347 C14.4477153,33.7726347 14,33.3228998 14,32.7681237 L14,32.7681237 L14,26.7401319 L18,26.7401319 L18,29.7536155 L28.1270436,29.7545907 L30.001,31.6812721 L31.8726235,29.7556243 L35,29.75462 L35,16.0721758 L32,16.0721319 L32,12.0541319 Z" id="形状结合" fill="#444444"></path>
+            </g>
+          </g>
+        </g>
+        <g id="编组-19" transform="translate(8.000000, 1.000000)"></g>
+      </g>
+    </g>
+  </g>
+</svg>

+ 29 - 0
src/TUIKit/TUIComponents/assets/icon/msg-revoke.svg

@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="40px" height="40px" viewBox="0 0 40 40" version="1.1">
+  <title>矩形</title>
+  <defs>
+    <path d="M467,0 C474.731986,-2.71135202e-14 481,6.2680135 481,14 L481,247 C481,254.731986 474.731986,261 467,261 L353.036,261 L340.862492,273.204941 C339.302377,274.769018 336.769719,274.77223 335.205642,273.212116 C335.203247,273.209727 335.200856,273.207335 335.198467,273.204941 L323.024,261 L14,261 C6.2680135,261 2.72325209e-15,254.731986 0,247 L0,14 C8.29461588e-16,6.2680135 6.2680135,3.19669972e-15 14,0 L467,0 Z" id="path-1"></path>
+    <filter x="-10.1%" y="-14.0%" width="120.2%" height="135.4%" filterUnits="objectBoundingBox" id="filter-2">
+      <feOffset dx="0" dy="10" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
+      <feGaussianBlur stdDeviation="14.5" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
+      <feColorMatrix values="0 0 0 0 0   0 0 0 0 0   0 0 0 0 0  0 0 0 0.06 0" type="matrix" in="shadowBlurOuter1"></feColorMatrix>
+    </filter>
+  </defs>
+  <g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+    <g id="消息状态" transform="translate(-660.000000, -327.000000)">
+      <g id="编组-11" transform="translate(499.000000, 163.000000)">
+        <g id="形状结合">
+          <use fill="black" fill-opacity="1" filter="url(#filter-2)" xlink:href="#path-1"></use>
+          <use fill="#FFFFFF" fill-rule="evenodd" xlink:href="#path-1"></use>
+        </g>
+        <g id="编组-19" transform="translate(8.000000, 1.000000)" fill="#444444" fill-rule="nonzero">
+          <g id="编组-13" transform="translate(119.000000, 131.000000)">
+            <g id="编组-21" transform="translate(34.000000, 32.000000)">
+              <path d="M9.94902685,18.825399 C9.94902685,19.1015414 9.72516923,19.325399 9.44902685,19.325399 C9.31552791,19.325399 9.18757266,19.2720125 9.09366105,19.1771306 L2.13687841,12.1484693 C1.94403281,11.9536312 1.94403281,11.6398441 2.13687841,11.445006 L9.09366105,4.41634471 C9.28791708,4.2200816 9.60449539,4.21845454 9.8007585,4.41271056 C9.89564042,4.50662217 9.94902685,4.63457741 9.94902685,4.76807636 L9.94824421,9.73507632 L22.6994692,9.73558854 C31.0812313,9.73558854 38,15.6504194 38,23.1330579 C38,30.5145797 31.2669616,36.3703807 23.0384554,36.5272976 L22.6994692,36.5305273 L10.4390092,36.5305273 C9.88672441,36.5305273 9.43900916,36.082812 9.43900916,35.5305273 L9.43900916,33.408229 C9.43900916,32.8559443 9.88672441,32.408229 10.4390092,32.408229 L22.6994692,32.408229 C28.9648068,32.408229 33.9198585,28.1721729 33.9198585,23.1330579 C33.9198585,18.1779281 29.1285988,13.9993117 23.0116273,13.861401 L22.6994692,13.8578868 L9.94824421,13.8570763 L9.94902685,18.825399 Z" id="路径"></path>
+            </g>
+          </g>
+        </g>
+      </g>
+    </g>
+  </g>
+</svg>

+ 8 - 0
src/TUIKit/TUIComponents/assets/icon/mute.svg

@@ -0,0 +1,8 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" class="design-iconfont">
+  <g fill="none" fill-rule="evenodd">
+    <path d="M4.13744611,6.61713753 L20.551,23 L0,23 L0,21 L3,21 L3,11 C3,9.40883362 3.41291862,7.91410954 4.13744611,6.61713753 Z M21.481,21 L24,21 L24,23 L23.485,23 L21.481,21 Z M12,2 C16.9705627,2 21,6.02943725 21,11 L21,20.52 L5.36684476,4.91705738 C7.01203082,3.12402075 9.37475086,2 12,2 Z" fill="#CCC" transform="translate(4 3)"/>
+    <path fill="#CCC" fill-rule="nonzero" d="M13 0L13 4 11 4 11 0z" transform="translate(4 3)"/>
+    <path stroke="#CCC" d="M8.5 24.5H15.5V25.5H8.5z" transform="translate(4 3)"/>
+    <path stroke="#CCC" stroke-width="2" transform="matrix(-1 0 0 1 31.089472 3)" d="M25.4499982 0.1546001L1.63947409 23.8855564"/>
+  </g>
+</svg>

BIN
src/TUIKit/TUIComponents/assets/icon/phone.png


BIN
src/TUIKit/TUIComponents/assets/icon/public.png


+ 6 - 0
src/TUIKit/TUIComponents/assets/icon/replies.svg

@@ -0,0 +1,6 @@
+<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <circle cx="6" cy="6" r="6" fill="#006EFF"/>
+  <circle cx="3" cy="6" r="1" fill="white"/>
+  <circle cx="6" cy="6" r="1" fill="white"/>
+  <circle cx="9" cy="6" r="1" fill="white"/>
+</svg>

BIN
src/TUIKit/TUIComponents/assets/icon/right.png


BIN
src/TUIKit/TUIComponents/assets/icon/room.png


+ 7 - 0
src/TUIKit/TUIComponents/assets/icon/rotate-left.svg

@@ -0,0 +1,7 @@
+<?xml version="1.0" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg class="icon" width="200px" height="200.00px" viewBox="0 0 1024 1024" version="1.1"
+  xmlns="http://www.w3.org/2000/svg">
+  <path fill="#444444"
+    d="M672 418H144c-17.7 0-32 14.3-32 32v414c0 17.7 14.3 32 32 32h528c17.7 0 32-14.3 32-32V450c0-17.7-14.3-32-32-32z m-44 402H188V494h440v326zM819.3 328.5c-78.8-100.7-196-153.6-314.6-154.2l-0.2-64c0-6.5-7.6-10.1-12.6-6.1l-128 101c-4 3.1-3.9 9.1 0 12.3L492 318.6c5.1 4 12.7 0.4 12.6-6.1v-63.9c12.9 0.1 25.9 0.9 38.8 2.5 42.1 5.2 82.1 18.2 119 38.7 38.1 21.2 71.2 49.7 98.4 84.3 27.1 34.7 46.7 73.7 58.1 115.8 11 40.7 14 82.7 8.9 124.8-0.7 5.4-1.4 10.8-2.4 16.1h74.9c14.8-103.6-11.3-213-81-302.3z" />
+</svg>

+ 7 - 0
src/TUIKit/TUIComponents/assets/icon/rotate-right.svg

@@ -0,0 +1,7 @@
+<?xml version="1.0" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg class="icon" width="200px" height="200.00px" viewBox="0 0 1024 1024" version="1.1"
+  xmlns="http://www.w3.org/2000/svg">
+  <path fill="#444444"
+    d="M480.5 251.2c13-1.6 25.9-2.4 38.8-2.5v63.9c0 6.5 7.5 10.1 12.6 6.1L660 217.6c4-3.2 4-9.2 0-12.3l-128-101c-5.1-4-12.6-0.4-12.6 6.1l-0.2 64c-118.6 0.5-235.8 53.4-314.6 154.2-69.6 89.2-95.7 198.6-81.1 302.4h74.9c-0.9-5.3-1.7-10.7-2.4-16.1-5.1-42.1-2.1-84.1 8.9-124.8 11.4-42.2 31-81.1 58.1-115.8 27.2-34.7 60.3-63.2 98.4-84.3 37-20.6 76.9-33.6 119.1-38.8zM880 418H352c-17.7 0-32 14.3-32 32v414c0 17.7 14.3 32 32 32h528c17.7 0 32-14.3 32-32V450c0-17.7-14.3-32-32-32z m-44 402H396V494h440v326z" />
+</svg>

BIN
src/TUIKit/TUIComponents/assets/icon/salebackground.png


BIN
src/TUIKit/TUIComponents/assets/icon/selected.png


+ 27 - 0
src/TUIKit/TUIComponents/assets/icon/selected.svg

@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16px" height="16px" viewBox="0 0 16 16" version="1.1">
+  <title>编组 14</title>
+  <defs>
+    <filter x="-10.6%" y="-5.4%" width="121.2%" height="110.9%" filterUnits="objectBoundingBox" id="filter-1">
+      <feOffset dx="0" dy="7" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
+      <feGaussianBlur stdDeviation="10" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
+      <feColorMatrix values="0 0 0 0 0   0 0 0 0 0   0 0 0 0 0  0 0 0 0.1 0" type="matrix" in="shadowBlurOuter1" result="shadowMatrixOuter1"></feColorMatrix>
+      <feMerge>
+        <feMergeNode in="shadowMatrixOuter1"></feMergeNode>
+        <feMergeNode in="SourceGraphic"></feMergeNode>
+      </feMerge>
+    </filter>
+  </defs>
+  <g id="new" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+    <g id="自定义消息" transform="translate(-458.000000, -318.000000)">
+      <g id="编组-32" filter="url(#filter-1)" transform="translate(44.000000, 60.000000)">
+        <g id="编组-24" transform="translate(30.000000, 250.000000)">
+          <g id="编组-14" transform="translate(384.000000, 8.000000)">
+            <circle id="椭圆形" fill="#006EFF" fill-rule="nonzero" cx="8" cy="8" r="8"></circle>
+            <polyline id="路径-4" stroke="#FFFFFF" stroke-width="2" transform="translate(8.042641, 6.242641) rotate(-315.000000) translate(-8.042641, -6.242641) " points="6.04264069 10.2426407 10.0426407 10.2426407 10.0426407 2.24264069"></polyline>
+          </g>
+        </g>
+      </g>
+    </g>
+  </g>
+</svg>

+ 55 - 0
src/TUIKit/TUIComponents/assets/icon/star-light.svg

@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="34px" height="33px" viewBox="0 0 34 33" version="1.1">
+  <title>编组 10</title>
+  <defs>
+    <filter x="-27.3%" y="-28.6%" width="154.5%" height="157.1%" filterUnits="objectBoundingBox" id="filter-1">
+      <feGaussianBlur stdDeviation="2" in="SourceGraphic"></feGaussianBlur>
+    </filter>
+    <linearGradient x1="72.4956041%" y1="27.6077921%" x2="39.6032421%" y2="74.5944688%" id="linearGradient-2">
+      <stop stop-color="#FFF235" offset="0%"></stop>
+      <stop stop-color="#F9C600" offset="100%"></stop>
+    </linearGradient>
+    <path d="M23.276778,8.12077882 L16.0081718,7.05943334 L12.7576045,0.434939378 C12.4730744,-0.144979793 11.5274548,-0.144979793 11.2429246,0.434939378 L7.99235731,7.05943334 L0.72375111,8.12077882 C0.405448806,8.16747802 0.141181906,8.39163419 0.0415541289,8.69899984 C-0.0580736481,9.00636549 0.024668065,9.34344882 0.255162837,9.56930313 L5.51432744,14.7248949 L4.27235744,22.006574 C4.21832203,22.3249777 4.34834472,22.6467776 4.60839011,22.8369707 C4.8692798,23.028862 5.2137555,23.0526361 5.49744137,22.9015005 L12.0002646,19.4644393 L18.5022435,22.9015005 C18.6255117,22.9668794 18.7606003,22.9991443 18.8948445,22.9991443 C19.0696152,22.9991443 19.244386,22.9448034 19.3912947,22.8369707 C19.6513401,22.6467776 19.7813628,22.3249777 19.7273274,22.006574 L18.4853574,14.7248949 L23.744522,9.56930313 C23.9750168,9.34344882 24.0577585,9.00636549 23.958975,8.69899984 C23.8593472,8.39163419 23.5950803,8.16747802 23.276778,8.12077882 Z" id="path-3"></path>
+    <filter x="-15.1%" y="-10.7%" width="128.1%" height="131.2%" filterUnits="objectBoundingBox" id="filter-5">
+      <feMorphology radius="0.2" operator="dilate" in="SourceAlpha" result="shadowSpreadOuter1"></feMorphology>
+      <feOffset dx="0" dy="1" in="shadowSpreadOuter1" result="shadowOffsetOuter1"></feOffset>
+      <feGaussianBlur stdDeviation="1" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
+      <feComposite in="shadowBlurOuter1" in2="SourceAlpha" operator="out" result="shadowBlurOuter1"></feComposite>
+      <feColorMatrix values="0 0 0 0 1   0 0 0 0 1   0 0 0 0 1  0 0 0 0.495153147 0" type="matrix" in="shadowBlurOuter1"></feColorMatrix>
+    </filter>
+    <filter x="-25.5%" y="-21.5%" width="149.0%" height="153.0%" filterUnits="objectBoundingBox" id="filter-6">
+      <feGaussianBlur stdDeviation="2" in="SourceAlpha" result="shadowBlurInner1"></feGaussianBlur>
+      <feOffset dx="0" dy="-2" in="shadowBlurInner1" result="shadowOffsetInner1"></feOffset>
+      <feComposite in="shadowOffsetInner1" in2="SourceAlpha" operator="arithmetic" k2="-1" k3="1" result="shadowInnerInner1"></feComposite>
+      <feColorMatrix values="0 0 0 0 1   0 0 0 0 0.735756187   0 0 0 0 0  0 0 0 0.782696064 0" type="matrix" in="shadowInnerInner1" result="shadowMatrixInner1"></feColorMatrix>
+      <feGaussianBlur stdDeviation="2.5" in="SourceAlpha" result="shadowBlurInner2"></feGaussianBlur>
+      <feOffset dx="0" dy="3" in="shadowBlurInner2" result="shadowOffsetInner2"></feOffset>
+      <feComposite in="shadowOffsetInner2" in2="SourceAlpha" operator="arithmetic" k2="-1" k3="1" result="shadowInnerInner2"></feComposite>
+      <feColorMatrix values="0 0 0 0 1   0 0 0 0 0.977846179   0 0 0 0 0.736284656  0 0 0 0.709552898 0" type="matrix" in="shadowInnerInner2" result="shadowMatrixInner2"></feColorMatrix>
+      <feMerge>
+        <feMergeNode in="shadowMatrixInner1"></feMergeNode>
+        <feMergeNode in="shadowMatrixInner2"></feMergeNode>
+      </feMerge>
+    </filter>
+  </defs>
+  <g id="页面-2备份" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+    <g id="评价" transform="translate(-1023.000000, -440.000000)">
+      <g id="编组-12" transform="translate(954.000000, 401.000000)">
+        <g id="编组-25" transform="translate(20.000000, 41.000000)">
+          <g id="编组-10" transform="translate(54.000000, 0.000000)">
+            <path d="M22.3370465,11.4146241 L15.6741575,10.4455696 L12.6944708,4.39711856 C12.4336515,3.86762715 11.5668336,3.86762715 11.3060142,4.39711856 L8.32632754,10.4455696 L1.66343852,11.4146241 C1.37166141,11.4572625 1.12941675,11.6619269 1.03809128,11.9425651 C0.946765823,12.2232033 1.02261239,12.530975 1.23389927,12.7371898 L6.05480015,17.4444693 L4.91632765,24.0929589 C4.8667952,24.3836753 4.98598266,24.6774926 5.2243576,24.8511472 C5.46350648,25.0263523 5.77927587,25.0480591 6.03932126,24.9100657 L12.0002425,21.7718794 L17.9603899,24.9100657 C18.0733858,24.9697595 18.1972169,24.9992187 18.3202741,24.9992187 C18.4804806,24.9992187 18.6406872,24.9496031 18.7753535,24.8511472 C19.0137284,24.6774926 19.1329159,24.3836753 19.0833835,24.0929589 L17.944911,17.4444693 L22.7658118,12.7371898 C22.9770987,12.530975 23.0529453,12.2232033 22.9623938,11.9425651 C22.8710683,11.6619269 22.6288237,11.4572625 22.3370465,11.4146241 Z" id="路径" fill-opacity="0.6957231" fill="#FFCA23" fill-rule="nonzero" filter="url(#filter-1)"></path>
+            <mask id="mask-4" fill="white">
+              <use xlink:href="#path-3"></use>
+            </mask>
+            <g id="路径" fill-rule="nonzero">
+              <use fill="black" fill-opacity="1" filter="url(#filter-5)" xlink:href="#path-3"></use>
+              <use fill="url(#linearGradient-2)" xlink:href="#path-3"></use>
+              <use fill="black" fill-opacity="1" filter="url(#filter-6)" xlink:href="#path-3"></use>
+              <path stroke-opacity="0.8" stroke="#FFFFFF" stroke-width="0.2" d="M11.2266591,0.241080187 L16.0645854,6.96661028 L23.454788,8.045711 L18.5910086,14.7495608 L19.8315027,22.0225866 C19.2622565,23.1360849 19.0671242,23.0979502 18.8707721,23.099179 L18.8707721,23.099179 L12.0009684,19.5771763 L5.39709804,23.0676459 L5.41424065,14.766815 L0.0672527664,9.52512944 L7.92123191,6.97737585 L11.2266591,0.241080187 Z"></path>
+            </g>
+          </g>
+        </g>
+      </g>
+    </g>
+  </g>
+</svg>

+ 15 - 0
src/TUIKit/TUIComponents/assets/icon/star.svg

@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="24px" height="23px" viewBox="0 0 24 23" version="1.1">
+  <title>路径</title>
+  <g id="页面-2备份" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+    <g id="评价" transform="translate(-1145.000000, -442.000000)" fill="#E5E7E9" fill-rule="nonzero">
+      <g id="编组-12" transform="translate(954.000000, 401.000000)">
+        <g id="编组-25" transform="translate(20.000000, 41.000000)">
+          <g id="编组-9" transform="translate(54.000000, 0.000000)">
+            <path d="M140.958975,8.69899984 C140.859347,8.39163419 140.59508,8.16747802 140.276778,8.12077882 L133.008172,7.05943334 L129.757605,0.434939378 C129.473074,-0.144979793 128.527455,-0.144979793 128.242925,0.434939378 L124.992357,7.05943334 L117.723751,8.12077882 C117.405449,8.16747802 117.141182,8.39163419 117.041554,8.69899984 C116.941926,9.00636549 117.024668,9.34344882 117.255163,9.56930313 L122.514327,14.7248949 L121.272357,22.006574 C121.218322,22.3249777 121.348345,22.6467776 121.60839,22.8369707 C121.86928,23.028862 122.213755,23.0526361 122.497441,22.9015005 L129.000265,19.4644393 L135.502243,22.9015005 C135.625512,22.9668794 135.7606,22.9991443 135.894844,22.9991443 C136.069615,22.9991443 136.244386,22.9448034 136.391295,22.8369707 C136.65134,22.6467776 136.781363,22.3249777 136.727327,22.006574 L135.485357,14.7248949 L140.744522,9.56930313 C140.975017,9.34344882 141.057758,9.00636549 140.958975,8.69899984 L140.958975,8.69899984 Z" id="路径"></path>
+          </g>
+        </g>
+      </g>
+    </g>
+  </g>
+</svg>

+ 6 - 0
src/TUIKit/TUIComponents/assets/icon/startGroup.svg

@@ -0,0 +1,6 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14" fill="none">
+  <circle r="2.61111" transform="matrix(1 0 0 -1 8.55545 3.88889)" stroke="#4C5059"></circle>
+  <circle r="2.61111" transform="matrix(1 0 0 -1 4.66678 3.88889)" fill="#F4F5F9" stroke="#4C5059"></circle>
+  <path d="M5.16675 11.7778C5.16675 9.84484 6.73375 8.27783 8.66675 8.27783H10.0001C11.9331 8.27783 13.5001 9.84484 13.5001 11.7778V12.7223H5.16675V11.7778Z" stroke="#4C5059"></path>
+  <path d="M0.5 11.7778C0.5 9.84484 2.067 8.27783 4 8.27783H5.33333C7.26633 8.27783 8.83333 9.84484 8.83333 11.7778V12.7223H0.5V11.7778Z" fill="#F4F5F9" stroke="#4C5059"></path>
+</svg>

BIN
src/TUIKit/TUIComponents/assets/icon/system.png


BIN
src/TUIKit/TUIComponents/assets/icon/video-call.png


BIN
src/TUIKit/TUIComponents/assets/icon/video.png


BIN
src/TUIKit/TUIComponents/assets/icon/voice-call.png


BIN
src/TUIKit/TUIComponents/assets/icon/voice.png


BIN
src/TUIKit/TUIComponents/assets/icon/words.png


+ 25 - 0
src/TUIKit/TUIComponents/assets/icon/words.svg

@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16px" height="16px" viewBox="0 0 16 16" version="1.1">
+  <title>编组 9备份 5</title>
+  <defs>
+    <path d="M3.25,1 L12.75,1 C13.9926407,1 15,2.00735931 15,3.25 L15,8.80223083 L15,8.80223083 L15,10.6730769 C15,11.9157176 13.9926407,12.9230769 12.75,12.9230769 L9.60349397,12.9230769 L9.60349397,12.9230769 L8.41095925,14.4677057 C8.24220503,14.6862843 7.9282097,14.726675 7.70963106,14.5579208 C7.6710256,14.5281153 7.63698877,14.4928218 7.60860204,14.4531614 L6.51344888,12.9230769 L6.51344888,12.9230769 L3.25,12.9230769 C2.00735931,12.9230769 1,11.9157176 1,10.6730769 L1,3.25 C1,2.00735931 2.00735931,1 3.25,1 Z" id="path-1"></path>
+    <mask id="mask-2" maskContentUnits="userSpaceOnUse" maskUnits="objectBoundingBox" x="0" y="0" width="14" height="14" fill="white">
+      <use xlink:href="#path-1"></use>
+    </mask>
+  </defs>
+  <g id="new" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" opacity="0.795168922">
+    <g id="消息列表" transform="translate(-1076.000000, -700.000000)">
+      <g id="输入框" transform="translate(796.000000, 686.000000)">
+        <g id="编组-11" transform="translate(20.000000, 11.000000)">
+          <g transform="translate(0.000000, 3.000000)" id="编组-9备份-5">
+            <g transform="translate(260.000000, 0.000000)">
+              <rect id="矩形" x="0" y="0" width="16" height="16"></rect>
+              <use id="矩形" stroke="#232832" mask="url(#mask-2)" stroke-width="2" stroke-dasharray="0,0" xlink:href="#path-1"></use>
+              <polyline id="路径-10" stroke="#232832" stroke-linecap="round" stroke-linejoin="round" points="9.29062678 3.74278634 5.97267016 6.33120496 9.88720003 7.13345401 6.6167373 9.74278634"></polyline>
+            </g>
+          </g>
+        </g>
+      </g>
+    </g>
+  </g>
+</svg>

BIN
src/TUIKit/TUIComponents/assets/icon/work.png


+ 9 - 0
src/TUIKit/TUIComponents/assets/icon/zoom-in.svg

@@ -0,0 +1,9 @@
+<?xml version="1.0" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg class="icon" width="200px" height="200.00px" viewBox="0 0 1024 1024" version="1.1"
+  xmlns="http://www.w3.org/2000/svg">
+  <path fill="#444444"
+    d="M945.066667 898.133333l-189.866667-189.866666c55.466667-64 87.466667-149.333333 87.466667-241.066667 0-204.8-168.533333-373.333333-373.333334-373.333333S96 264.533333 96 469.333333 264.533333 842.666667 469.333333 842.666667c91.733333 0 174.933333-34.133333 241.066667-87.466667l189.866667 189.866667c6.4 6.4 14.933333 8.533333 23.466666 8.533333s17.066667-2.133333 23.466667-8.533333c8.533333-12.8 8.533333-34.133333-2.133333-46.933334zM469.333333 778.666667C298.666667 778.666667 160 640 160 469.333333S298.666667 160 469.333333 160 778.666667 298.666667 778.666667 469.333333 640 778.666667 469.333333 778.666667z" />
+  <path fill="#444444"
+    d="M597.333333 437.333333h-96V341.333333c0-17.066667-14.933333-32-32-32s-32 14.933333-32 32v96H341.333333c-17.066667 0-32 14.933333-32 32s14.933333 32 32 32h96V597.333333c0 17.066667 14.933333 32 32 32s32-14.933333 32-32v-96H597.333333c17.066667 0 32-14.933333 32-32s-14.933333-32-32-32z" />
+</svg>

+ 9 - 0
src/TUIKit/TUIComponents/assets/icon/zoom-out.svg

@@ -0,0 +1,9 @@
+<?xml version="1.0" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg class="icon" width="200px" height="200.00px" viewBox="0 0 1024 1024" version="1.1"
+  xmlns="http://www.w3.org/2000/svg">
+  <path fill="#444444"
+    d="M945.066667 898.133333l-189.866667-189.866666c55.466667-64 87.466667-149.333333 87.466667-241.066667 0-204.8-168.533333-373.333333-373.333334-373.333333S96 264.533333 96 469.333333 264.533333 842.666667 469.333333 842.666667c91.733333 0 174.933333-34.133333 241.066667-87.466667l189.866667 189.866667c6.4 6.4 14.933333 8.533333 23.466666 8.533333s17.066667-2.133333 23.466667-8.533333c8.533333-12.8 8.533333-34.133333-2.133333-46.933334zM469.333333 778.666667C298.666667 778.666667 160 640 160 469.333333S298.666667 160 469.333333 160 778.666667 298.666667 778.666667 469.333333 640 778.666667 469.333333 778.666667z" />
+  <path fill="#444444"
+    d="M597.333333 437.333333H341.333333c-17.066667 0-32 14.933333-32 32s14.933333 32 32 32h256c17.066667 0 32-14.933333 32-32s-14.933333-32-32-32z" />
+</svg>

+ 3 - 0
src/TUIKit/TUIComponents/components/dialog/index.ts

@@ -0,0 +1,3 @@
+import Dialog from './index.vue';
+
+export default Dialog;

+ 88 - 0
src/TUIKit/TUIComponents/components/dialog/index.vue

@@ -0,0 +1,88 @@
+<template>
+  <div class="dialog" :class="[isH5 ? 'dialog-h5' : '', center ? 'center' : '']" v-if="show" @click.self="toggleView">
+    <main class="dialog-main" :style="!background && {'background': 'none'}">
+      <header v-if="isHeaderShow">
+        <h1>{{title}}</h1>
+        <i class="icon icon-close" @click="toggleView"></i>
+      </header>
+      <div class="dialog-main-content">
+        <slot />
+      </div>
+      <footer v-if="isFooterShow">
+        <button class="btn btn-cancel" @click="toggleView">{{$t('component.取消')}}</button>
+        <button class="btn btn-default" @click="submit">{{$t('component.确定')}}</button>
+      </footer>
+    </main>
+  </div>
+</template>
+
+<script lang="ts">
+import { defineComponent, reactive, watchEffect, toRefs } from 'vue';
+
+export default defineComponent({
+  props: {
+    show: {
+      type: Boolean,
+      default: () => false,
+    },
+    isHeaderShow: {
+      type: Boolean,
+      default: () => true,
+    },
+    isFooterShow: {
+      type: Boolean,
+      default: () => true,
+    },
+    background: {
+      type: Boolean,
+      default: () => true,
+    },
+    title: {
+      type: String,
+      default: () => '',
+    },
+    isH5: {
+      type: Boolean,
+      default: () => false,
+    },
+    center: {
+      type: Boolean,
+      default: () => false,
+    },
+  },
+  setup(props:any, ctx:any) {
+    const data = reactive({
+      show: false,
+      isHeaderShow: true,
+      isFooterShow: true,
+      background: true,
+      title: '',
+    });
+
+    watchEffect(() => {
+      data.show = props.show;
+      data.title = props.title;
+      data.isHeaderShow = props.isHeaderShow;
+      data.isFooterShow = props.isFooterShow;
+      data.background = props.background;
+    });
+
+    const toggleView = () => {
+      data.show = !data.show;
+      ctx.emit('update:show', data.show);
+    };
+
+    const submit = () => {
+      ctx.emit('submit');
+      toggleView();
+    };
+
+    return {
+      ...toRefs(data),
+      toggleView,
+      submit,
+    };
+  },
+});
+</script>
+<style lang="scss" scoped src="./style/dialog.scss"></style>

+ 34 - 0
src/TUIKit/TUIComponents/components/dialog/style/color.scss

@@ -0,0 +1,34 @@
+.dialog {
+  background: rgba(0 ,0, 0, .3);
+  header {
+    h1 {
+      font-family: PingFangSC-Medium;
+      font-weight: 500;
+      color: #333333;
+    }
+  }
+  &-main {
+    background: #FFFFFF;
+    header {
+      font-weight: 500;
+      color: #333333;
+    }
+    &-content {
+      font-weight: 400;
+      color: #333333;
+    }
+  }
+}
+.btn {
+  font-weight: 400;
+  color: #FFFFFF;
+  letter-spacing: 0;
+  &-cancel {
+    border: 1px solid #dddddd;
+    color: #666666;
+  }
+  &-default {
+    background: #006EFF;
+    border: 1px solid #006EFF;
+  }
+}

+ 5 - 0
src/TUIKit/TUIComponents/components/dialog/style/dialog.scss

@@ -0,0 +1,5 @@
+@import "./color.scss";
+@import "./web.scss";
+@import "./h5.scss";
+@import url('../../../styles/common.scss');
+@import url('../../../styles/icon.scss');

+ 46 - 0
src/TUIKit/TUIComponents/components/dialog/style/h5.scss

@@ -0,0 +1,46 @@
+.dialog-h5 {
+  height: 100%;
+  top: 0;
+  align-items: inherit;
+
+  .dialog {
+    &-main {
+      min-width: 0;
+      min-height: 0;
+      width: 100%;
+      padding: 0;
+      display: flex;
+      flex-direction: column;
+      overflow: hidden;
+      &-content {
+        flex: 1;
+        padding: 0;
+        min-width: 0;
+        min-height: 0;
+        overflow: hidden;
+      }
+
+      footer {
+        border-top: 1px solid #DDDDDD;
+
+        .btn {
+          flex: 1;
+          margin: 0;
+          background: none;
+          border-right: 1px solid #DDDDDD;
+
+          &-default {
+            color: #FF584C;
+            border: none;
+          }
+        }
+      }
+    }
+  }
+}
+
+.center {
+  align-items: center;
+  padding: 20px;
+  box-sizing: border-box;
+}

+ 60 - 0
src/TUIKit/TUIComponents/components/dialog/style/web.scss

@@ -0,0 +1,60 @@
+.dialog {
+  position: fixed;
+  width: 100%;
+  height: 100%;
+  left: 0;
+  top: 0;
+  z-index: 6;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+
+  header {
+    h1 {
+      font-size: 16px;
+      line-height: 30px;
+    }
+  }
+
+  &-main {
+    min-width: 368px;
+    border-radius: 10px;
+    padding: 20px 30px;
+
+    header {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      font-size: 16px;
+      line-height: 30px;
+    }
+
+    &-content {
+      padding: 20px 0 40px;
+      font-size: 14px;
+    }
+
+    footer {
+      display: flex;
+      justify-content: flex-end;
+    }
+  }
+}
+
+.btn {
+  padding: 8px 20px;
+  margin: 0 6px;
+  border-radius: 4px;
+  border: none;
+  font-size: 14px;
+  text-align: center;
+  line-height: 20px;
+
+  &:disabled {
+    opacity: 0.3;
+  }
+
+  &:last-child {
+    margin-right: 0;
+  }
+}

+ 2 - 0
src/TUIKit/TUIComponents/components/drag/index.ts

@@ -0,0 +1,2 @@
+import Drag from './index.vue';
+export default Drag;

+ 127 - 0
src/TUIKit/TUIComponents/components/drag/index.vue

@@ -0,0 +1,127 @@
+<template>
+  <div class="drag-container" v-show="show">
+    <slot />
+  </div>
+</template>
+
+<script lang="ts">
+import {
+  defineComponent,
+  reactive,
+  toRefs,
+  onMounted,
+  watch,
+  watchEffect,
+} from "vue";
+export default defineComponent({
+  props: {
+    show: {
+      type: Boolean,
+      default: () => false,
+    },
+    domClassName: {
+      type: String,
+      default: "",
+    },
+  },
+  setup(props: any, ctx: any) {
+    const data = reactive({
+      show: false,
+      domClassName: "",
+      startPosition: {
+        left: "",
+        top: "",
+        cssText: "",
+      },
+    });
+
+    watchEffect(() => {
+      data.show = props.show;
+      data.domClassName = props.domClassName;
+    });
+
+    onMounted(() => {
+      const dragDom = document.getElementsByClassName(
+        props.domClassName ? props.domClassName : "drag-container"
+      )[0] as HTMLElement;
+      if (!dragDom) return;
+      let isDrag = false;
+      watch(
+        () => data.show,
+        (newVal, oldVal) => {
+          data.show = newVal;
+          if (newVal === oldVal) return;
+          if (data.show === true) {
+            dragDom.style.left = data.startPosition?.left;
+            dragDom.style.top = data.startPosition?.top;
+            dragDom.style.cssText = data.startPosition?.cssText;
+          }
+        }
+      );
+      const mouseDown = (e: MouseEvent) => {
+        isDrag = true;
+        const X = e.clientX - dragDom.offsetLeft;
+        const Y = e.clientY - dragDom.offsetTop;
+        const move = (e: MouseEvent) => {
+          e.preventDefault();
+          if (isDrag) {
+            dragDom.style.left = `${e.clientX - X}px`;
+            dragDom.style.top = `${e.clientY - Y}px`;
+          }
+        };
+        document.addEventListener("mousemove", throttle(move, 20), false);
+        document.addEventListener("mouseup", () => {
+          isDrag = false;
+          document.removeEventListener("mousemove", move);
+        });
+      };
+      dragDom.addEventListener("mousedown", mouseDown);
+    });
+
+    function throttle(
+      fn: { (e: MouseEvent): void; apply?: any },
+      timer: number
+    ) {
+      let initTime = 0;
+      return function (...args: any) {
+        const nowTime = +new Date();
+        if (nowTime - initTime > timer) {
+          initTime = nowTime;
+          fn.apply(ctx, args);
+        }
+      };
+    }
+
+    const positionReset = () => {
+      const dragDom = document.getElementsByClassName(
+        props.domClassName ? props.domClassName : "drag-container"
+      )[0] as HTMLElement;
+      data.startPosition = {
+        left: "",
+        top: "",
+        cssText: "",
+      };
+      dragDom.style.left = data.startPosition?.left;
+      dragDom.style.top = data.startPosition?.top;
+      dragDom.style.cssText = data.startPosition?.cssText;
+    };
+
+    ctx.expose({
+      positionReset,
+    });
+
+    return {
+      ...toRefs(data),
+      throttle,
+    };
+  },
+});
+</script>
+<style lang="scss" scoped>
+@import url("../../styles/common.scss");
+@import url("../../styles/icon.scss");
+.drag-container {
+  position: fixed;
+  z-index: 100;
+}
+</style>

+ 57 - 0
src/TUIKit/TUIComponents/components/mask/mask.vue

@@ -0,0 +1,57 @@
+<template>
+  <div class="mask" @click.self="toggleView" v-if="show">
+    <slot />
+  </div>
+</template>
+
+<script lang="ts">
+import { defineComponent, reactive, watchEffect, toRefs } from 'vue';
+
+export default defineComponent({
+  props: {
+    show: {
+      type: Boolean,
+      default: () => false,
+    },
+  },
+  setup(props:any, ctx:any) {
+    const data = reactive({
+      show: false,
+    });
+
+    watchEffect(() => {
+      data.show = props.show;
+    });
+
+    const toggleView = () => {
+      data.show = !data.show;
+      ctx.emit('update:show', data.show);
+    };
+
+    return {
+      ...toRefs(data),
+      toggleView,
+    };
+  },
+});
+</script>
+
+<style lang="scss" scoped>
+@import url('../../styles/common.scss');
+@import url('../../styles/icon.scss');
+.mask {
+  position: fixed;
+  width: 100vw;
+  height: 100vh;
+  left: 0;
+  top: 0;
+  z-index: 99;
+  background: rgba(#000000, 0.5);
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  main {
+    position: relative;
+  }
+}
+</style>

+ 75 - 0
src/TUIKit/TUIComponents/components/message/index.ts

@@ -0,0 +1,75 @@
+import { createVNode, render, ComponentPublicInstance, VNode } from 'vue';
+import MessageConstructor from './index.vue';
+
+const instances:any = [];
+let seed = 1;
+
+const appendTo: HTMLElement | null = document.body;
+
+const message = function (options:any) {
+  const tempVm: any = instances.find((item:any) => `${item.vm.props?.message ?? ''}`
+      === `${(options as any).message ?? ''}`);
+  if (tempVm) {
+    tempVm.vm.component!.props.repeatNum += 1;
+    return {
+      close: () => ((
+          vm.component!.proxy as ComponentPublicInstance<{
+            visible: boolean
+          }>
+      ).visible = false),
+    };
+  }
+  let verticalOffset = options.offset || 20;
+  instances.forEach(({ vm }:any) => {
+    verticalOffset += (vm.el?.offsetHeight || 0) + 20;
+  });
+  verticalOffset += 20;
+  const id = `message_${seed += 1}`;
+  const userOnClose = options.onClose;
+  const props:any = {
+    zIndex: 20 + seed,
+    offset: verticalOffset,
+    id,
+    ...options,
+    onClose: () => {
+      close(id, userOnClose);
+    },
+  };
+  const vm = createVNode(
+    MessageConstructor,
+    props,
+  );
+  const container = document.createElement('div');
+  vm.props!.onDestroy = () => {
+    render(null, container);
+  };
+  render(vm, container);
+  instances.push({ vm });
+  appendTo.appendChild(container.firstElementChild!);
+  return {
+    close: () => ((
+        vm.component!.proxy as ComponentPublicInstance<{
+          visible: boolean
+        }>
+    ).visible = false),
+  };
+};
+
+export function close(id: string, userOnClose?: (vm: VNode) => void): void {
+  const idx = instances.findIndex(({ vm }: any) => id === vm.component!.props.id);
+  if (idx === -1) return;
+  const { vm } = instances[idx];
+  if (!vm) return;
+  userOnClose?.(vm);
+  const removedHeight = vm.el!.offsetHeight;
+  instances.splice(idx, 1);
+  // adjust other instances vertical offset
+  const len = instances.length;
+  if (len < 1) return;
+  for (let i = idx; i < len; i++) {
+    const pos = Number.parseInt(instances[i].vm.el!.style.top, 10) - removedHeight - 16;
+    instances[i].vm.component!.props.offset = pos;
+  }
+}
+
+export default message;

+ 165 - 0
src/TUIKit/TUIComponents/components/message/index.vue

@@ -0,0 +1,165 @@
+<template>
+  <transition name="fade" @before-leave="onClose" @after-leave="$emit('destroy')">
+    <div class="message" :class="[handleStyle(type), isH5 && 'message-h5']" :style="customStyle" v-show="visible">
+      <p v-if="!isH5">{{ message }}</p>
+      <span v-if="isH5">{{ message }}</span>
+    </div>
+  </transition>
+</template>
+<script lang="ts">
+import { useTimeoutFn } from '@vueuse/core';
+import { computed, CSSProperties, defineComponent, onMounted, ref, watch } from 'vue';
+export default defineComponent({
+  name: 'TUIMessage',
+  props: {
+    message: {
+      type: String,
+      default: '',
+    },
+    duration: {
+      type: Number,
+      default: 3000,
+    },
+    repeatNum: {
+      type: Number,
+      default: 1,
+    },
+    id: {
+      type: String,
+      default: '',
+    },
+    onClose: {
+      type: Function,
+      required: false,
+    },
+    offset: {
+      type: Number,
+      default: 20,
+    },
+    zIndex: {
+      type: Number,
+      default: 0,
+    },
+    isH5: {
+      type: Boolean,
+      default: false,
+    },
+    type: {
+      type: String,
+      default: '',
+    },
+  },
+  setup(props) {
+    const visible = ref(false);
+
+    let stopTimer: (() => void) | undefined = undefined;
+
+    function startTimer() {
+      if (props.duration > 0) {
+        ({ stop: stopTimer } = useTimeoutFn(() => {
+          if (visible.value) close();
+        }, props.duration));
+      }
+    }
+
+    function clearTimer() {
+      stopTimer?.();
+    }
+
+    function close() {
+      visible.value = false;
+    }
+
+    watch(
+      () => props.repeatNum,
+      () => {
+        clearTimer();
+        startTimer();
+      }
+    );
+
+    const customStyle = computed<CSSProperties>(() => ({
+      top: `${props.offset}px`,
+      zIndex: props.zIndex,
+    }));
+
+    onMounted(() => {
+      startTimer();
+      visible.value = true;
+    });
+
+    const handleStyle = (type?: string) => {
+      if (type && (type === 'error' || type === 'success' || type === 'warning')) return type;
+      return 'normal';
+    };
+
+    return {
+      visible,
+      customStyle,
+      handleStyle,
+    };
+  },
+});
+</script>
+<style lang="scss" scoped>
+@import url('../../styles/common.scss');
+@import url('../../styles/icon.scss');
+.message {
+  position: fixed;
+  left: 0;
+  right: 0;
+  margin: 0 auto;
+  max-width: 450px;
+  width: fit-content;
+  justify-content: center;
+  align-items: center;
+  p {
+    box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.2);
+    border-radius: 3px;
+    padding: 10px 15px;
+    width: fit-content;
+    word-break: break-all;
+  }
+}
+.message-h5 {
+  position: absolute;
+  top: 300px !important;
+  margin: 0 auto;
+  width: fit-content;
+  max-width: 80%;
+  width: fit-content;
+  justify-content: center;
+  align-items: center;
+  border-radius: 5px;
+  padding: 10px 15px;
+  span {
+    font-family: PingFangSC-Regular;
+    font-weight: 400;
+    font-size: 14px;
+    letter-spacing: 0;
+    text-align: center;
+    word-break: break-all;
+  }
+}
+
+.success {
+  border: 1px solid #e4f2da;
+  background: #f2f9ec;
+  color: #7ebf50;
+}
+.error {
+  border: 1px solid #fde2e2;
+  background: #fef0f0;
+  color: #f46c6e;
+}
+.normal {
+  border: 1px solid #e9e9eb;
+  background: #f4f4f5;
+  color: #909398;
+}
+.warning {
+  border: 1px solid #faf0e2;
+  background: #fdf8f1;
+  color: #e4b877;
+}
+</style>

+ 67 - 0
src/TUIKit/TUIComponents/components/slider/index.vue

@@ -0,0 +1,67 @@
+<template>
+  <div class="slider-box" :class="[open && 'slider-open']" @click="toggle">
+    <span class="slider-block"></span>
+  </div>
+</template>
+
+<script lang="ts">
+import { defineComponent, reactive, watchEffect, toRefs } from 'vue';
+
+export default defineComponent({
+  props: {
+    open: {
+      type: Boolean,
+      default: () => false,
+    },
+  },
+  setup(props:any, ctx:any) {
+    const data = reactive({
+      open: false,
+    });
+
+    watchEffect(() => {
+      data.open = props.open;
+    });
+
+
+    const toggle = () => {
+      data.open = !data.open;
+      ctx.emit('change', data.open);
+    };
+
+    return {
+      ...toRefs(data),
+      toggle,
+    };
+  },
+});
+</script>
+
+<style lang="scss" scoped>
+@import url('../../styles/common.scss');
+@import url('../../styles/icon.scss');
+.slider {
+  &-box {
+    display: flex;
+    align-items: center;
+    width: 34px;
+    height: 20px;
+    border-radius: 10px;
+    background: #E1E1E3;
+  }
+  &-open {
+    background: #006EFF !important;
+    justify-content: flex-end;
+  }
+  &-block {
+    display: inline-block;
+    width: 16px;
+    height: 16px;
+    border-radius: 8px;
+    margin: 0 2px;
+    background: #FFFFFF;
+    border: 0 solid rgba(0,0,0,0.85);
+    box-shadow: 0 2px 4px 0 #D1D1D1;
+  }
+}
+</style>

+ 3 - 0
src/TUIKit/TUIComponents/components/transfer/index.ts

@@ -0,0 +1,3 @@
+import Transfer from './index.vue';
+
+export default Transfer;

+ 206 - 0
src/TUIKit/TUIComponents/components/transfer/index.vue

@@ -0,0 +1,206 @@
+<template>
+  <div class="transfer" :class="[isH5 ? 'transfer-h5' : '']">
+    <header class="transfer-h5-header" v-if="isH5">
+      <i class="icon icon-back" @click="cancel"></i>
+      <span class="title">{{ title }}</span>
+    </header>
+    <main class="main">
+      <div class="left">
+        <header v-if="isSearch">
+          <input
+            type="text"
+            @keyup.enter="handleInput"
+            :placeholder="$t('component.请输入userID')"
+            enterkeyhint="search"
+          />
+        </header>
+        <main>
+          <ul class="list">
+            <li class="list-item" @click="selectedAll" v-if="optional.length > 1 && !isRadio">
+              <i
+                class="icon"
+                :class="[selectedList.length === optional.length ? 'icon-selected' : 'icon-unselected']"
+              ></i>
+              <span class="all">{{ $t('component.全选') }}</span>
+            </li>
+            <li class="list-item" v-for="(item, index) in list" :key="index" @click="selected(item)">
+              <i
+                class="icon"
+                :class="[
+                  item?.isDisabled && 'disabled',
+                  selectedList.indexOf(item) > -1 ? 'icon-selected' : 'icon-unselected',
+                ]"
+              ></i>
+              <template v-if="!isCustomItem">
+                <img
+                  class="avatar"
+                  :src="item?.avatar || 'https://web.sdk.qcloud.com/component/TUIKit/assets/avatar_21.png'"
+                  onerror="this.src='https://web.sdk.qcloud.com/component/TUIKit/assets/avatar_21.png'"
+                />
+                <span class="name">{{ item?.nick || item?.userID }}</span>
+                <span v-if="item?.isDisabled">({{ $t('component.已在群聊中') }})</span>
+              </template>
+              <template v-else>
+                <slot name="left" :data="item" />
+              </template>
+            </li>
+          </ul>
+        </main>
+      </div>
+      <div class="right">
+        <header v-if="!isH5">{{ title }}</header>
+        <ul class="list" v-show="resultShow">
+          <p v-if="selectedList.length > 0 && !isH5">
+            {{ $t('component.已选中') }}{{ selectedList.length }}{{ $t('component.人') }}
+          </p>
+          <li class="list-item space-between" v-for="(item, index) in selectedList" :key="index">
+            <aside>
+              <template v-if="!isCustomItem">
+                <img
+                  class="avatar"
+                  :src="item?.avatar || 'https://web.sdk.qcloud.com/component/TUIKit/assets/avatar_21.png'"
+                  onerror="this.src='https://web.sdk.qcloud.com/component/TUIKit/assets/avatar_21.png'"
+                />
+                <span v-if="!isH5">{{ item.nick || item.userID }}</span>
+              </template>
+              <template v-else>
+                <slot name="right" :data="item" />
+              </template>
+            </aside>
+            <i class="icon icon-cancel" @click="selected(item)" v-if="!isH5"></i>
+          </li>
+        </ul>
+        <footer>
+          <button class="btn btn-cancel" @click="cancel">{{ $t('component.取消') }}</button>
+          <button v-if="selectedList.length > 0" class="btn" @click="submit">{{ $t('component.完成') }}</button>
+          <button v-else class="btn btn-no" @click="submit">{{ $t('component.完成') }}</button>
+        </footer>
+      </div>
+    </main>
+  </div>
+</template>
+
+<script lang="ts">
+import { defineComponent, reactive, watchEffect, toRefs, computed } from 'vue';
+
+export default defineComponent({
+  props: {
+    list: {
+      type: Array,
+      default: () => [],
+    },
+    selectedList: {
+      type: Array,
+      default: () => [],
+    },
+    isSearch: {
+      type: Boolean,
+      default: () => true,
+    },
+    isRadio: {
+      type: Boolean,
+      default: () => false,
+    },
+    isCustomItem: {
+      type: Boolean,
+      default: () => false,
+    },
+    title: {
+      type: String,
+      default: () => '',
+    },
+    type: {
+      type: String,
+      default: () => '',
+    },
+    isH5: {
+      type: Boolean,
+      default: () => false,
+    },
+    resultShow: {
+      type: Boolean,
+      default: () => true,
+    },
+  },
+  setup(props: any, ctx: any) {
+    const data = reactive({
+      type: '',
+      list: [],
+      selectedList: [],
+      isSearch: true,
+      isCustomItem: false,
+      title: '',
+    });
+
+    watchEffect(() => {
+      if (props.isCustomItem) {
+        for (let index = 0; index < props.list.length; index++) {
+          if (props.list[index].conversationID.indexOf('@TIM#SYSTEM') > -1) {
+            // eslint-disable-next-line vue/no-mutating-props
+            props.list.splice(index, 1);
+          }
+          data.list = props.list;
+        }
+      } else {
+        data.list = props.list;
+      }
+      data.selectedList = props.selectedList;
+      data.isSearch = props.isSearch;
+      data.isCustomItem = props.isCustomItem;
+      data.title = props.title;
+      data.type = props.type;
+    });
+
+    // 可选项
+    const optional = computed(() => data.list.filter((item: any) => !item.isDisabled));
+
+    const handleInput = (e: any) => {
+      ctx.emit('search', e.target.value);
+    };
+
+    const selected = (item: any) => {
+      if (item.isDisabled) {
+        return;
+      }
+      let list: any = data.selectedList;
+      const index: number = list.indexOf(item);
+      if (index > -1) {
+        return data.selectedList.splice(index, 1);
+      }
+      if (props.isRadio) {
+        list = [];
+      }
+      list.push(item);
+      data.selectedList = list;
+    };
+
+    const selectedAll = () => {
+      if (data.selectedList.length === optional.value.length) {
+        data.selectedList = [];
+      } else {
+        data.selectedList = [...optional.value];
+      }
+    };
+
+    const submit = () => {
+      ctx.emit('submit', data.selectedList);
+    };
+
+    const cancel = () => {
+      ctx.emit('cancel');
+    };
+
+    return {
+      ...toRefs(data),
+      optional,
+      handleInput,
+      selected,
+      selectedAll,
+      submit,
+      cancel,
+    };
+  },
+});
+</script>
+
+<style lang="scss" scoped src="./style/transfer.scss"></style>

+ 63 - 0
src/TUIKit/TUIComponents/components/transfer/style/color.scss

@@ -0,0 +1,63 @@
+.main {
+  background: #FFFFFF;
+  border: 1px solid #E0E0E0;
+  box-shadow: 0 -4px 12px 0 rgba(0,0,0,0.06);
+  .left {
+    border-right: 1px solid #E8E8E9;
+  }
+  header {
+    font-weight: 500;
+    color: #000000;
+    letter-spacing: 0;
+    input {
+      background: #FFFFFF;
+      border: 1px solid #DEE0E3;
+      font-weight: 500;
+      color: #8F959E;
+      letter-spacing: 0;
+    }
+  }
+  .list {
+    p {
+      font-weight: 500;
+      color: #8F959E;
+      letter-spacing: 0;
+    }
+    &-item {
+      .disabled {
+        background: #eeeeee;
+      }
+    }
+  }
+}
+.avatar {
+  background: #F4F5F9;
+  color: #000000;
+}
+.btn {
+  background: #3370FF;
+  border: 0 solid #2F80ED;
+  font-weight: 400;
+  color: #FFFFFF;
+  &-cancel {
+    background: #FFFFFF;
+    border: 1px solid #DDDDDD;
+    color: #828282;
+  }
+}
+.btn-no {
+  background: #e8e8e9;
+  border: 1px solid #DDDDDD;
+  font-weight: 400;
+  color: #FFFFFF;
+}
+
+.transfer-h5-header {
+  background: #FFFFFF;
+  .title {
+    font-family: PingFangSC-Medium;
+    font-weight: 500;
+    color: #000000;
+    letter-spacing: 0;
+  }
+}

+ 71 - 0
src/TUIKit/TUIComponents/components/transfer/style/h5.scss

@@ -0,0 +1,71 @@
+.transfer-h5 {
+  width: 100%;
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+  &-header {
+    position: relative;
+    display: flex;
+    justify-content: center;
+    font-size: 18px;
+    padding: 16px 18px;
+    .icon {
+      position: absolute;
+      left: 18px;
+      top: 18px;
+    }
+  }
+  .main {
+    flex: 1;
+    flex-direction: column;
+    width: auto;
+    height: auto;
+    border-radius: 0;
+    border: none;
+    box-shadow: none;
+    max-height: calc( 100% - 54px);
+    padding: 0;
+    .avatar {
+      border-radius: 0;
+    }
+    .left {
+      padding: 0;
+      flex: 1;
+      border: none;
+      display: flex;
+      flex-direction: column;
+      header {
+        position: sticky;
+        top: 0;
+        padding: 0 18px;
+        input {
+          border-radius: 5px;
+          font-size: 14px;
+        }
+      }
+      .list {
+        padding: 0 18px;
+      }
+    }
+    .right {
+      flex: 0;
+      padding: 0;
+      flex-direction: row;
+      align-items: center;
+      box-shadow: inset 0 1px 0 0 #EEEEEE;
+      padding: 0 18px 8px;
+      .list {
+        flex-direction: row;
+        width: 0;
+      }
+      footer {
+        padding: 6px 0;
+        display: flex;
+        align-items: center;
+        .btn {
+          font-size: 14px;
+        }
+      }
+    }
+  }
+}

+ 5 - 0
src/TUIKit/TUIComponents/components/transfer/style/transfer.scss

@@ -0,0 +1,5 @@
+@import "./color.scss";
+@import "./web.scss";
+@import "./h5.scss";
+@import url('../../../styles/common.scss');
+@import url('../../../styles/icon.scss');

+ 135 - 0
src/TUIKit/TUIComponents/components/transfer/style/web.scss

@@ -0,0 +1,135 @@
+.main {
+  box-sizing: border-box;
+  width: 620px;
+  height: 394px;
+  display: flex;
+  border-radius: 8px;
+  padding: 20px 0;
+
+  .right {
+    padding: 0 20px;
+    flex: 1;
+
+    ul {
+      padding-right: 20px;
+    }
+  }
+
+  .left {
+    flex: 1;
+    overflow-y: hidden;
+    display: flex;
+    flex-direction: column;
+
+    header {
+      padding: 0 20px;
+    }
+
+    main {
+      flex: 1;
+      overflow-y: auto;
+      padding: 0 20px;
+    }
+  }
+
+  .right {
+    display: flex;
+    flex-direction: column;
+
+    footer {
+      align-self: flex-end;
+
+      .btn-cancel {
+        margin-right: 12px;
+      }
+    }
+
+    .list {
+      overflow-y: auto;
+    }
+  }
+
+  header {
+    font-size: 14px;
+    line-height: 14px;
+    padding-bottom: 20px;
+
+    input {
+      box-sizing: border-box;
+      width: 100%;
+      border-radius: 30px;
+      font-size: 10px;
+      line-height: 14px;
+      padding: 9px 12px;
+    }
+  }
+
+  .list {
+    flex: 1;
+    display: flex;
+    flex-direction: column;
+
+    p {
+      font-size: 10px;
+      line-height: 14px;
+    }
+
+    &-item {
+      padding: 6px 0;
+      display: flex;
+      align-items: center;
+      font-size: 14px;
+
+      aside {
+        display: flex;
+        align-items: center;
+      }
+
+      .avatar {
+        margin: 0 5px 0 8px;
+        border-radius: 50%;
+      }
+
+      .name {
+        overflow: hidden;
+        text-overflow: ellipsis;
+        white-space: nowrap;
+        flex: 1;
+      }
+    }
+  }
+}
+
+img,
+.avatar {
+  width: 36px;
+  height: 36px;
+  border-radius: 4px;
+  font-size: 12px;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
+
+.btn {
+  padding: 4px 28px;
+  font-size: 12px;
+  line-height: 24px;
+  border-radius: 4px;
+}
+
+.btn-no {
+  padding: 4px 28px;
+  font-size: 12px;
+  line-height: 24px;
+  border-radius: 4px;
+}
+
+.space-between {
+  justify-content: space-between;
+}
+
+.all {
+  padding-left: 8px;
+  font-size: 14px;
+}

+ 20 - 0
src/TUIKit/TUIComponents/container/IComponentServer.ts

@@ -0,0 +1,20 @@
+/* eslint-disable @typescript-eslint/no-empty-function */
+/**
+   * class IComponentServer
+   *
+   * IComponentServer 组件 server 基类
+   */
+export default class IComponentServer {
+  /**
+     * 组件销毁
+     */
+  public destroyed() {}
+
+  /**
+     * 数据监听回调
+     *
+     * @param {any} oldValue 旧数据
+     * @param {any} newValue 新数据
+     */
+  protected updateStore(oldValue:any, newValue:any) {}
+}

+ 37 - 0
src/TUIKit/TUIComponents/container/TUIChat/components/index.ts

@@ -0,0 +1,37 @@
+import MessageText from './message-text.vue';
+import MessageImage from './message-image.vue';
+import MessageVideo from './message-video.vue';
+import MessageAudio from './message-audio.vue';
+import MessageFile from './message-file.vue';
+import MessageFace from './message-face.vue';
+import MessageLocation from './message-location.vue';
+import MessageMerger from './message-merger.vue';
+import MessageCustom from './message-custom.vue';
+import MessageTip from './message-tip.vue';
+import MessageBubble from './message-bubble.vue';
+import MessageRevoked from './message-revoked.vue';
+import MessageSystem from './message-system.vue';
+import MessageTool from './message-tool.vue';
+import MessageEmojiReact from './message-emoji-react.vue';
+import MessageItem from './index.vue';
+import MessageTimestamp from './message-timestamp.vue'
+
+export {
+  MessageText,
+  MessageImage,
+  MessageVideo,
+  MessageAudio,
+  MessageFile,
+  MessageFace,
+  MessageLocation,
+  MessageMerger,
+  MessageCustom,
+  MessageTip,
+  MessageTimestamp,
+  MessageBubble,
+  MessageRevoked,
+  MessageSystem,
+  MessageTool,
+  MessageEmojiReact,
+  MessageItem
+};

+ 178 - 0
src/TUIKit/TUIComponents/container/TUIChat/components/index.vue

@@ -0,0 +1,178 @@
+<template>
+  <div class="message-item">
+    <MessageTip v-if="isMessageTip(message)" :data="handleTipMessageShowContext(message)" />
+    <MessageBubble
+      v-else-if="!message.isRevoked"
+      :data="message"
+      :isH5="env.isH5"
+      :messagesList="messageList"
+      :needGroupReceipt="displayGroupMessageReadReceipt"
+      :needReplies="true"
+      :needEmojiReact="displayEmojiReactions"
+      @jumpID="jumpID"
+      @resendMessage="resendMessage"
+      @showReadReceiptDialog="showReadReceiptDialog"
+      @showRepliesDialog="showRepliesDialog"
+    >
+      <MessageText v-if="message.type === types.MSG_TEXT" :data="handleTextMessageShowContext(message)" />
+      <MessageImage
+        v-if="message.type === types.MSG_IMAGE"
+        :isH5="env.isH5"
+        :data="handleImageMessageShowContext(message)"
+        @uploading="uploading"
+        @previewImage="previewImage"
+      />
+      <MessageVideo
+        v-if="message.type === types.MSG_VIDEO"
+        :isH5="env.isH5"
+        :data="handleVideoMessageShowContext(message)"
+        @uploading="uploading"
+      />
+      <MessageAudio v-if="message.type === types.MSG_AUDIO" :data="handleAudioMessageShowContext(message)" />
+      <MessageFile v-if="message.type === types.MSG_FILE" :data="handleFileMessageShowContext(message)" />
+      <MessageFace v-if="message.type === types.MSG_FACE" :data="handleFaceMessageShowContext(message)" :isH5="env.isH5"/>
+      <MessageLocation v-if="message.type === types.MSG_LOCATION" :data="handleLocationMessageShowContext(message)" />
+      <MessageCustom v-if="message.type === types.MSG_CUSTOM" :data="handleCustomMessageShowContext(message)" />
+      <MessageMerger v-if="message.type === types.MSG_MERGER" :data="handleMergerMessageShowContext(message)" />
+      <template #dialog>
+        <MessageTool :message="message" :needEmojiReact="displayEmojiReactions" @handleMessage="handleMessage" />
+        <MessageEmojiReact v-if="!env?.isH5 && displayEmojiReactions" :message="message" type="dropdown" />
+      </template>
+    </MessageBubble>
+    <MessageRevoked v-else :isEdit="message.type === types.MSG_TEXT" :data="message" @edit="handleEdit(message)" />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { toRefs, defineProps, defineEmits } from 'vue';
+import MessageBubble from './message-bubble.vue';
+import MessageText from './message-text.vue';
+import MessageImage from './message-image.vue';
+import MessageVideo from './message-video.vue';
+import MessageAudio from './message-audio.vue';
+import MessageFile from './message-file.vue';
+import MessageFace from './message-face.vue';
+import MessageLocation from './message-location.vue';
+import MessageMerger from './message-merger.vue';
+import MessageCustom from './message-custom.vue';
+import MessageTip from './message-tip.vue';
+import MessageTool from './message-tool.vue';
+import MessageEmojiReact from './message-emoji-react.vue';
+import MessageRevoked from './message-revoked.vue';
+
+import {
+  handleTextMessageShowContext,
+  handleImageMessageShowContext,
+  handleVideoMessageShowContext,
+  handleAudioMessageShowContext,
+  handleFileMessageShowContext,
+  handleFaceMessageShowContext,
+  handleLocationMessageShowContext,
+  handleMergerMessageShowContext,
+  handleTipMessageShowContext,
+  handleCustomMessageShowContext,
+  isMessageTip,
+} from '../utils/utils';
+import { Message } from '@tencentcloud/chat';
+import constant from '../../constant';
+
+const props = defineProps({
+  message: {
+    type: Object,
+    default: () => ({}),
+  },
+  beforeMessage: {
+    type: Object,
+    default: () => ({}),
+  },
+  types: {
+    type: Object,
+    default: () => ({}),
+  },
+  env: {
+    type: Object,
+    default: () => ({}),
+  },
+  messageList: {
+    type: Array,
+    default: () => [],
+  },
+  // 开关控制位
+  displayGroupMessageReadReceipt: {
+    type: Boolean,
+    default: true,
+  },
+  displayEmojiReactions: {
+    type: Boolean,
+    default: true,
+  },
+});
+
+const emits = defineEmits(['handleEditor', 'showDialog', 'uploading', 'jumpID','resendMessage']);
+const { message, types, env, messageList, displayGroupMessageReadReceipt, displayEmojiReactions } = toRefs(props);
+
+const handleEdit = (message: any) => {
+  emits('handleEditor', message, 'reedit');
+};
+
+const handleMessage = (message: Message, type: string) => {
+  if (!message || !type) {
+    return;
+  }
+  switch (type) {
+    case constant.handleMessage.forward:
+      emits('showDialog', message, constant.handleMessage.forward);
+      break;
+    case constant.handleMessage.reference:
+      emits('handleEditor', message, constant.handleMessage.reference);
+      break;
+    case constant.handleMessage.reply:
+      emits('handleEditor', message, constant.handleMessage.reply);
+      break;
+    default:
+      break;
+  }
+};
+
+const previewImage = (message: Message) => {
+  if (message) {
+    emits('showDialog', message, 'previewImage');
+  }
+};
+
+const showReadReceiptDialog = (message: Message) => {
+  if (message) {
+    emits('showDialog', message, 'receipt');
+  }
+};
+
+const showRepliesDialog = (message: Message) => {
+  if (message) {
+    emits('showDialog', message, 'replies');
+  }
+};
+
+const jumpID = (messageID: string) => {
+  if (messageID) {
+    emits('jumpID', messageID);
+  }
+};
+
+const uploading = () => {
+  emits('uploading');
+};
+
+const resendMessage = (message: Message) => {
+  if (message) {
+    emits('resendMessage', message);
+  }
+}
+</script>
+<style lang="scss" scoped>
+@import url('../../../styles/common.scss');
+@import url('../../../styles/icon.scss');
+.message-item {
+  display: flex;
+  flex-direction: column;
+}
+</style>

+ 88 - 0
src/TUIKit/TUIComponents/container/TUIChat/components/message-audio.vue

@@ -0,0 +1,88 @@
+<template>
+  <div
+    class="message-audio"
+    :class="[data.message.flow === 'out' && 'reserve']"
+    @click.stop="play"
+    :style="`width: ${data?.second * 10 + 40}px`"
+  >
+    <i class="icon icon-voice" :class="[data.message.flow === 'out' && 'icon-reserve']"></i>
+    <label>{{ data.second }}s</label>
+    <audio ref="audio" :src="data.url"></audio>
+  </div>
+</template>
+
+<script lang="ts">
+import { defineComponent, watch, reactive, toRefs, ref } from 'vue';
+
+export default defineComponent({
+  props: {
+    data: {
+      type: Object,
+      default: () => ({}),
+    },
+  },
+  setup(props: any, ctx: any) {
+    const data = reactive({
+      data: {},
+      show: false,
+    });
+
+    const audio = ref(null);
+
+    watch(
+      () => props.data,
+      () => {
+        data.data = props.data;
+      },
+      { deep: true, immediate: true }
+    );
+
+    const play = () => {
+      const audios = document.getElementsByTagName('audio');
+      for (const audio of audios) {
+        if (!audio.paused) {
+          audio.pause();
+          audio.load();
+        }
+      }
+      const audioPlayer: any = audio.value;
+      if (audioPlayer.paused) {
+        audioPlayer.play();
+        data.show = true;
+      } else {
+        audioPlayer.pause();
+        audioPlayer.load();
+        data.show = false;
+      }
+    };
+
+    return {
+      ...toRefs(data),
+      audio,
+      play,
+    };
+  },
+});
+</script>
+<style lang="scss" scoped>
+@import url('../../../styles/common.scss');
+@import url('../../../styles/icon.scss');
+.message-audio {
+  display: flex;
+  align-items: center;
+  position: relative;
+  cursor: pointer;
+  max-width: 100%;
+  overflow: hidden;
+  .icon {
+    margin: 0 4px;
+  }
+  audio {
+    width: 0;
+    height: 0;
+  }
+}
+.reserve {
+  flex-direction: row-reverse;
+}
+</style>

+ 635 - 0
src/TUIKit/TUIComponents/container/TUIChat/components/message-bubble.vue

@@ -0,0 +1,635 @@
+<template>
+  <div class="message-bubble" :class="[message.flow === 'in' ? '' : 'reverse']" ref="htmlRefHook">
+    <img
+      class="avatar"
+      :src="message?.avatar || 'https://web.sdk.qcloud.com/component/TUIKit/assets/avatar_21.png'"
+      onerror="this.src='https://web.sdk.qcloud.com/component/TUIKit/assets/avatar_21.png'"
+    />
+    <main class="message-area">
+      <label class="name" v-if="message.flow === 'in' && message.conversationType === 'GROUP'">
+        {{ message.nameCard || message.nick || message.from }}
+      </label>
+      <div :class="handleImageOrVideoBubbleStyle(message)" @click.prevent.right="toggleDialog">
+        <div
+          class="message-replie-area"
+          :class="[message?.flow === 'in' ? '' : 'message-replies-area-reverse']"
+          v-if="message?.cloudCustomData && referenceMessage && referenceMessage?.messageRootID"
+          @click="showRepliesDialog(message, false)"
+        >
+          <MessageReference
+            :message="message"
+            :referenceMessage="referenceMessage"
+            :referenceForShow="referenceForShow"
+            :url="url"
+            :face="face"
+            :allMessageID="allMessageID"
+            type="reply"
+          />
+        </div>
+        <slot />
+        <div v-if="dropdown" ref="dropdownRef" class="dropdown-inner">
+          <div class="dialog" :class="[message.flow === 'in' ? '' : 'dialog-right']" @click="dropdown = false">
+            <slot name="dialog" />
+          </div>
+        </div>
+        <MessageEmojiReact :message="message" type="content" v-if="needEmojiReact && isEmojiReactionInMessage(message)" />
+      </div>
+    </main>
+    <label class="message-label fail" v-if="message.status === 'fail'" @click="resendMessage(message)">!</label>
+    <label
+      class="message-label"
+      :class="readReceiptStyle(message)"
+      v-if="showReadReceiptTag(message)"
+      @click="showReadReceiptDialog(message)"
+    >
+      <span>{{ readReceiptCont(message) }}</span>
+    </label>
+  </div>
+  <div
+    class="message-reference-area"
+    :class="[message.flow === 'in' ? '' : 'message-reference-area-reverse']"
+    v-if="message?.cloudCustomData && referenceMessage && !referenceMessage?.messageRootID"
+    @click="jumpToAim(referenceMessage)"
+  >
+    <MessageReference
+      :message="message"
+      :referenceMessage="referenceMessage"
+      :referenceForShow="referenceForShow"
+      :url="url"
+      :face="face"
+      :allMessageID="allMessageID"
+      type="reference"
+    />
+  </div>
+  <label
+    class="message-replies"
+    :class="[message.flow === 'in' ? '' : 'message-replies-reverse']"
+    v-if="replies?.length"
+    @click="showRepliesDialog(message, true)"
+  >
+    <i class="icon icon-msg-replies"></i>
+    <span>{{ replies?.length + $t('TUIChat.条回复') }}</span>
+  </label>
+</template>
+
+<script lang="ts">
+import { decodeText } from '../utils/decodeText';
+import constant from '../../constant';
+import { defineComponent, watchEffect, reactive, toRefs, ref, nextTick, watch } from 'vue';
+import { onClickOutside, onLongPress, useElementBounding } from '@vueuse/core';
+import { deepCopy, JSONToObject } from '../utils/utils';
+import { handleErrorPrompts } from '../../utils';
+import TUIChat from '../index.vue';
+import MessageReference from './message-reference.vue';
+import { Message } from '../interface';
+import { TUIEnv } from '../../../../TUIPlugin';
+import MessageEmojiReact from './message-emoji-react.vue';
+import TIM from '../../../../TUICore/tim/index';
+
+const messageBubble = defineComponent({
+  props: {
+    data: {
+      type: Object,
+      default: () => ({}),
+    },
+    messagesList: {
+      type: Array,
+      default: () => [],
+    },
+    isH5: {
+      type: Boolean,
+      default: false,
+    },
+    needGroupReceipt: {
+      type: Boolean,
+      default: false,
+    },
+    needReplies: {
+      type: Boolean,
+      default: true,
+    },
+    flow: {
+      type: String,
+      default: '',
+    },
+    needEmojiReact: {
+      type: Boolean,
+      default: false,
+    },
+  },
+  emits: ['jumpID', 'resendMessage', 'showReadReceiptDialog', 'showRepliesDialog', 'dropDownOpen'],
+  components: {
+    MessageReference,
+    MessageEmojiReact,
+  },
+  setup(props: any, ctx: any) {
+    const { t } = (window as any).TUIKitTUICore.config.i18n.useI18n();
+    const { TUIServer } = TUIChat;
+    const data = reactive({
+      env: TUIEnv(),
+      message: {} as Message,
+      messagesList: [],
+      show: false,
+      type: {},
+      referenceMessage: {},
+      referenceForShow: {},
+      allMessageID: '',
+      needGroupReceipt: false,
+      needReplies: true,
+      replies: [],
+      face: [],
+      url: '',
+      needEmojiReact: false,
+    });
+
+    watchEffect(() => {
+      data.type = constant;
+      data.messagesList = props.messagesList;
+      data.needEmojiReact = props.needEmojiReact;
+      data.message = deepCopy(
+        data.messagesList?.find((item: any) => (item as any)?.ID === props.message?.ID) || props.data
+      );
+      data.needGroupReceipt = props.needGroupReceipt;
+      data.needReplies = props.needReplies;
+      if ((data.message as any).cloudCustomData) {
+        const messageIDList: any[] = [];
+        const cloudCustomData = JSONToObject((data.message as any).cloudCustomData);
+        data.replies = cloudCustomData?.messageReplies?.replies || [];
+        data.referenceMessage = cloudCustomData.messageReply ? cloudCustomData.messageReply : '';
+        for (let index = 0; index < (data.messagesList as any).length; index++) {
+          // To determine whether the referenced message is still in the message list, the corresponding field of the referenced message is displayed if it is in the message list. Otherwise, messageabstract/messagesender is displayed
+          messageIDList.push((data.messagesList as any)[index].ID);
+          (data as any).allMessageID = JSON.stringify(messageIDList);
+          if ((data.messagesList as any)[index].ID === (data.referenceMessage as any)?.messageID) {
+            data.referenceForShow = (data.messagesList as any)[index];
+            if ((data.referenceMessage as any).messageType === constant.typeText) {
+              (data as any).face = decodeText((data.referenceForShow as any).payload);
+            }
+            if ((data.referenceMessage as any).messageType === constant.typeFace) {
+              (data as any).url = `https://web.sdk.qcloud.com/im/assets/face-elem/${
+                (data.referenceForShow as any).payload.data
+              }@2x.png`;
+            }
+          }
+        }
+      } else {
+        data.replies = [];
+      }
+    });
+
+    const htmlRefHook = ref<HTMLElement | null>(null);
+    const dropdown = ref(false);
+    const dropdownRef = ref(null);
+
+    const toggleDialog = (e: any) => {
+      dropdown.value = !dropdown.value;
+      if (dropdown.value) {
+        ctx.emit('dropDownOpen', dropdownRef);
+        nextTick(() => {
+          const dialogDom = (dropdownRef as any)?.value?.children[0];
+          const dialogElement = document.getElementsByClassName('dialog-item')[0] as HTMLElement;
+          const parentDom = (dropdownRef as any)?.value?.offsetParent;
+          const parentBound = useElementBounding(parentDom);
+          const messageListDom = document.getElementById('messageEle') as HTMLElement;
+          const messageListBound = useElementBounding(messageListDom);
+          const leftRange = messageListBound?.left?.value;
+          const rightRange =
+            messageListBound?.left?.value + (messageListDom as any).clientWidth - dialogDom.clientWidth + 76;
+          const topRange = messageListBound?.top?.value;
+          const bottomRange =
+            messageListBound?.top?.value + (messageListDom as any).clientHeight - dialogDom.clientHeight;
+          const { clientX, clientY } = e;
+          if (data?.env?.isH5) {
+            if (parentBound?.top?.value <= dialogElement?.clientHeight) {
+              dialogDom.style.bottom = `-${dialogElement?.clientHeight}px`;
+            } else {
+              if (data?.message?.flow === 'in') {
+                dialogDom.style.top = `-${dialogElement?.clientHeight - 20}px`;
+              } else {
+                dialogDom.style.top = `-${dialogElement?.clientHeight}px`;
+              }
+            }
+            const centerWidth = parentBound?.left?.value + parentBound?.width?.value / 2;
+            if (
+              centerWidth > dialogElement.clientWidth / 2 &&
+              centerWidth < messageListDom?.clientWidth - dialogElement.clientWidth / 2
+            ) {
+              dialogDom.style.left = 'calc(50% - 135px)';
+            } else if (centerWidth <= dialogElement.clientWidth / 2) {
+              dialogDom.style.left = '-20px';
+            } else {
+              dialogDom.style.left = `-${dialogElement.clientWidth / 2 + 10}px`;
+            }
+            return;
+          }
+          switch (true) {
+            case clientX > leftRange && clientX < rightRange:
+              dialogDom.style.left = `${Math.max(e.clientX - parentBound?.left?.value - 76, -40)}px`;
+              break;
+            case clientX <= leftRange:
+              dialogDom.style.left = '20px';
+              break;
+            case clientX >= rightRange:
+              dialogDom.style.right = `${Math.max(
+                parentBound?.left?.value + parentDom?.clientWidth - e.clientX - 256,
+                -10
+              )}px`;
+              break;
+          }
+          switch (true) {
+            case clientY > topRange && clientY < bottomRange:
+              dialogDom.style.top = `${e.clientY - parentBound?.top?.value}px`;
+              dialogDom.style.cssText = dialogDom.style.cssText.replace('align-items:end;', '');
+              break;
+            case clientY <= topRange:
+              dialogDom.style.top = '0px';
+              dialogDom.style.cssText = dialogDom.style.cssText.replace('align-items:end;', '');
+              break;
+            case clientY >= bottomRange:
+              dialogDom.style.bottom = `${parentBound?.top?.value + parentDom?.clientHeight - e.clientY}px`;
+              dialogDom.style.cssText += 'align-items:end;';
+              break;
+          }
+        });
+      }
+    };
+
+    const jumpToAim = (message: any) => {
+      if (
+        (data.referenceMessage as any)?.messageID &&
+        data.allMessageID.includes((data.referenceMessage as any)?.messageID)
+      ) {
+        ctx.emit('jumpID', (data.referenceMessage as any).messageID);
+      } else {
+        const message = t('TUIChat.无法定位到原消息');
+        handleErrorPrompts(message, props);
+      }
+    };
+
+    onClickOutside(dropdownRef, () => {
+      dropdown.value = false;
+    });
+
+    const toggleDialogH5 = (e: any) => {
+      if (data?.env?.isH5) toggleDialog(e);
+      return;
+    };
+
+    onLongPress(htmlRefHook, toggleDialogH5);
+
+    const resendMessage = (message: any) => {
+      ctx.emit('resendMessage', message);
+    };
+
+    const showReadReceiptTag = (message: any) => {
+      if (message.flow === 'out' && message.status === 'success' && message.needReadReceipt) {
+        return true;
+      }
+      return false;
+    };
+
+    const readReceiptStyle = (message: any) => {
+      if (
+        message?.readReceiptInfo?.isPeerRead ||
+        (message?.readReceiptInfo?.isPeerRead === undefined && message?.isPeerRead) ||
+        message?.readReceiptInfo?.unreadCount === 0
+      ) {
+        return '';
+      }
+      return 'unRead';
+    };
+
+    const readReceiptCont = (message: any) => {
+      switch (message.conversationType) {
+        case TUIServer.TUICore.TIM.TYPES.CONV_C2C:
+          if (
+            message?.readReceiptInfo?.isPeerRead ||
+            (message?.readReceiptInfo?.isPeerRead === undefined && message?.isPeerRead)
+          ) {
+            return t('TUIChat.已读');
+          }
+          return t('TUIChat.未读');
+        case TUIServer.TUICore.TIM.TYPES.CONV_GROUP:
+          if (message.readReceiptInfo.unreadCount === 0) {
+            return t('TUIChat.全部已读');
+          }
+          if (
+            message.readReceiptInfo.readCount === 0 ||
+            (message.readReceiptInfo.unreadCount === undefined && message.readReceiptInfo.readCount === undefined)
+          ) {
+            return t('TUIChat.未读');
+          }
+          return `${message.readReceiptInfo.readCount + t('TUIChat.人已读')}`;
+        default:
+          return '';
+      }
+    };
+
+    const showReadReceiptDialog = (message: any) => {
+      ctx.emit('showReadReceiptDialog', message, 'receipt');
+    };
+
+    const showRepliesDialog = (message: any, isRoot: boolean) => {
+      if (isRoot) {
+        ctx.emit('showRepliesDialog', message, 'replies');
+        return;
+      }
+      if ((data.referenceMessage as any)?.messageRootID) {
+        const message = data.messagesList?.find(
+          (item: Message) => item.ID === (data.referenceMessage as any)?.messageRootID
+        );
+        if (message) {
+          ctx.emit('showRepliesDialog', message, 'replies');
+          return;
+        } else {
+          const message = t('TUIChat.无法定位到原消息');
+          handleErrorPrompts(message, props);
+        }
+      }
+    };
+
+    const handleImageOrVideoBubbleStyle = (message: Message) => {
+      const classNameList = ['content'];
+      if (!message) return classNameList;
+      classNameList.push(`content-${data.message.flow}`);
+      if (data.message.type === TIM.TYPES.MSG_IMAGE && !isEmojiReactionInMessage(message)) {
+        classNameList.push('content-image');
+      }
+      if (data.message.type === TIM.TYPES.MSG_VIDEO && !isEmojiReactionInMessage(message)) {
+        classNameList.push('content-video');
+      }
+      return classNameList;
+    };
+
+    const isEmojiReactionInMessage = (message: Message) => {
+      try {
+        if (!message?.cloudCustomData) return;
+        const reactList = JSONToObject(message?.cloudCustomData)?.messageReact?.reacts;
+        if (!reactList || Object.keys(reactList).length === 0) return false;
+        return true;
+      } catch (err) {
+        console.warn(err);
+        return false;
+      }
+    };
+
+    return {
+      ...toRefs(data),
+      toggleDialog,
+      htmlRefHook,
+      jumpToAim,
+      dropdown,
+      dropdownRef,
+      resendMessage,
+      showReadReceiptTag,
+      readReceiptStyle,
+      readReceiptCont,
+      showReadReceiptDialog,
+      showRepliesDialog,
+      handleImageOrVideoBubbleStyle,
+      isEmojiReactionInMessage,
+      TIM,
+    };
+  },
+});
+export default messageBubble;
+</script>
+<style lang="scss" scoped>
+@import url('../../../styles/common.scss');
+@import url('../../../styles/icon.scss');
+.reverse {
+  flex-direction: row-reverse;
+  justify-content: flex-start;
+}
+.avatar {
+  width: 36px;
+  height: 36px;
+  border-radius: 5px;
+}
+.message-bubble {
+  width: 100%;
+  display: flex;
+  padding-bottom: 5px;
+}
+.line-left {
+  border: 1px solid rgba(0, 110, 255, 0.5);
+}
+.message-reference-area {
+  display: flex;
+  background: #f2f2f2;
+  border-radius: 0.5rem;
+  border-radius: 0.63rem;
+  align-self: start;
+  margin-left: 44px;
+  margin-right: 8px;
+  &-show {
+    width: 100%;
+    display: flex;
+    flex-direction: inherit;
+    justify-content: center;
+    padding: 6px;
+    p {
+      font-family: PingFangSC-Regular;
+      font-weight: 400;
+      font-size: 0.88rem;
+      color: #999999;
+      letter-spacing: 0;
+      word-break: keep-all;
+      padding-right: 5px;
+    }
+    span {
+      height: 1.25rem;
+      font-family: PingFangSC-Regular;
+      font-weight: 400;
+      font-size: 0.88rem;
+      color: #999999;
+      letter-spacing: 0;
+      display: inline-block;
+    }
+  }
+}
+.message-replies {
+  display: flex;
+  align-self: start;
+  margin-left: 44px;
+  margin-right: 8px;
+  padding: 2px;
+  color: #999999;
+  font-size: 10px;
+  i {
+    margin: 4px;
+  }
+  span {
+    line-height: 20px;
+  }
+}
+.message-reference-area-reverse,
+.message-replies-reverse {
+  align-self: end;
+  margin-right: 44px;
+  margin-left: 8px;
+}
+.message-img {
+  max-width: min(calc(100vw - 180px), 300px);
+  max-height: min(calc(100vw - 180px), 300px);
+}
+.message-video-cover {
+  display: inline-block;
+  position: relative;
+  &::before {
+    position: absolute;
+    z-index: 1;
+    content: '';
+    width: 0px;
+    height: 0px;
+    border: 15px solid transparent;
+    border-left: 20px solid #ffffff;
+    top: 0;
+    left: 0;
+    bottom: 0;
+    right: 0;
+    margin: auto;
+  }
+}
+.message-videoimg {
+  max-width: min(calc(100vw - 160px), 300px);
+  max-height: min(calc(100vw - 160px), 300px);
+}
+.face-box {
+  display: flex;
+  align-items: center;
+}
+.text-img {
+  width: 20px;
+  height: 20px;
+}
+.message-audio {
+  padding-left: 10px;
+  display: flex;
+  align-items: center;
+  position: relative;
+  .icon {
+    margin: 0 4px;
+  }
+  audio {
+    width: 0;
+    height: 0;
+  }
+}
+.reserve {
+  flex-direction: row-reverse;
+}
+.message-area {
+  max-width: calc(100% - 54px);
+  position: relative;
+  display: flex;
+  flex-direction: column;
+  padding: 0 8px;
+  .name {
+    padding-bottom: 4px;
+    font-weight: 400;
+    font-size: 0.8rem;
+    color: #999999;
+    letter-spacing: 0;
+  }
+  .reference-content {
+    padding: 12px;
+    font-weight: 400;
+    font-size: 14px;
+    color: burlywood;
+    letter-spacing: 0;
+    word-wrap: break-word;
+    word-break: break-all;
+    animation: reference 800ms;
+  }
+  @-webkit-keyframes reference {
+    from {
+      opacity: 1;
+    }
+    50% {
+      background-color: #ff9c19;
+    }
+    to {
+      opacity: 1;
+    }
+  }
+  @keyframes reference {
+    from {
+      opacity: 1;
+    }
+    50% {
+      background-color: #ff9c19;
+    }
+    to {
+      opacity: 1;
+    }
+  }
+  .content {
+    padding: 12px;
+    font-weight: 400;
+    font-size: 14px;
+    color: #000000;
+    letter-spacing: 0;
+    word-wrap: break-word;
+    word-break: break-all;
+    width: fit-content;
+    &-in {
+      background: #fbfbfb;
+      border-radius: 0px 10px 10px 10px;
+    }
+    &-out {
+      background: #dceafd;
+      border-radius: 10px 0px 10px 10px;
+    }
+    &-image {
+      padding: 0px;
+      height: fit-content;
+      border-radius: 10px 0px 10px 10px;
+    }
+    &-video {
+      padding: 0px;
+      height: fit-content;
+      background: transparent;
+      border-radius: 10px;
+    }
+  }
+}
+.message-label {
+  align-self: flex-end;
+  font-family: PingFangSC-Regular;
+  font-weight: 400;
+  font-size: 12px;
+  color: #b6b8ba;
+  word-break: keep-all;
+}
+.fail {
+  width: 15px;
+  height: 15px;
+  border-radius: 15px;
+  background: red;
+  color: #ffffff;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
+.unRead {
+  color: #679ce1;
+}
+.dropdown-inner {
+  position: absolute;
+  width: 100%;
+  height: 100%;
+  left: 0;
+  top: 0;
+}
+.dialog {
+  position: absolute;
+  z-index: 1;
+  display: flex;
+  flex-direction: row;
+  width: fit-content;
+  &-right {
+    right: 0;
+  }
+}
+</style>

+ 228 - 0
src/TUIKit/TUIComponents/container/TUIChat/components/message-custom.vue

@@ -0,0 +1,228 @@
+<template>
+  <div class="custom">
+    <template v-if="isCustom === constant.typeService">
+      <div>
+        <h1>
+          <label>{{ extension.title }}</label>
+          <a
+            v-if="extension.hyperlinks_text"
+            :href="extension.hyperlinks_text.value"
+            target="view_window"
+          >
+            {{ extension.hyperlinks_text.key }}
+          </a>
+        </h1>
+        <ul v-if="extension.item && extension.item.length > 0">
+          <li v-for="(item, index) in extension.item" :key="index">
+            <a v-if="isUrl(item.value)" :href="item.value" target="view_window">
+              {{ item.key }}
+            </a>
+            <p v-else>{{ item.key }}</p>
+          </li>
+        </ul>
+        <article>{{ extension.description }}</article>
+      </div>
+    </template>
+    <template v-else-if="isCustom.businessID === constant.typeEvaluate">
+      <div class="evaluate">
+        <h1>{{ $t('message.custom.对本次服务评价') }}</h1>
+        <ul>
+          <li
+            class="evaluate-list-item"
+            v-for="(item, index) in ~~isCustom.score"
+            :key="index"
+          >
+            <i class="icon icon-star-light"></i>
+          </li>
+        </ul>
+        <article>{{ isCustom.comment }}</article>
+      </div>
+    </template>
+    <template v-else-if="isCustom.businessID === constant.typeOrder">
+      <div class="order" @click="openLink(isCustom.link)">
+        <img :src="isCustom.imageUrl" alt="" />
+        <main>
+          <h1>{{ isCustom.title }}</h1>
+          <p>{{ isCustom.description }}</p>
+          <span>{{ isCustom.price }}</span>
+        </main>
+      </div>
+    </template>
+    <template v-else-if="isCustom.businessID === constant.typeTextLink">
+      <div class="textLink">
+        <p>{{ isCustom.text }}</p>
+        <a :href="isCustom.link" target="view_window">
+          {{ $t('message.custom.查看详情>>') }}
+        </a>
+      </div>
+    </template>
+    <template v-else-if="isCustom.businessID === constant.TYPE_CALL_MESSAGE">
+      <div
+        class="call"
+        @click="handleCallAgain"
+        :class="`call-${data?.message?.conversationType}`"
+      >
+        <i class="icon" :class="handleCallMessageIcon()"></i>
+        <span>{{ data.custom }}</span>
+      </div>
+    </template>
+    <template v-else>
+      <span v-html="data.custom"></span>
+    </template>
+  </div>
+</template>
+
+<script lang="ts">
+import { defineComponent, watchEffect, reactive, toRefs } from 'vue';
+import { isUrl, JSONToObject } from '../utils/utils';
+import constant from '../../constant';
+import { useStore } from 'vuex';
+
+export default defineComponent({
+  props: {
+    data: {
+      type: Object,
+      default: () => ({})
+    }
+  },
+  setup(props: any, ctx: any) {
+    const VuexStore =
+      ((window as any)?.TUIKitTUICore?.isOfficial && useStore && useStore()) ||
+      {};
+    const data = reactive({
+      data: {} as any,
+      extension: {},
+      isCustom: '',
+      constant: constant
+    });
+    watchEffect(() => {
+      data.data = props.data;
+      const {
+        message: { payload }
+      } = props.data;
+      data.isCustom = payload.data || ' ';
+      data.isCustom = JSONToObject(payload.data);
+      if (payload.data === constant.typeService) {
+        data.extension = JSONToObject(payload.extension);
+      }
+    });
+    const openLink = (url: any) => {
+      window.open(url);
+    };
+    const handleCallMessageIcon = () => {
+      const callType = JSON.parse(
+        JSON.parse(data?.data?.message?.payload?.data)?.data
+      )?.call_type;
+      let className = '';
+      switch (callType) {
+        case 1:
+          className = 'icon-call-voice';
+          break;
+        case 2:
+          className = 'icon-call-video';
+          break;
+        default:
+          break;
+      }
+      return className;
+    };
+
+    const handleCallAgain = async () => {
+      const callType = JSON.parse(
+        JSON.parse(props?.data?.message?.payload?.data)?.data
+      )?.call_type;
+      switch (data?.data?.message?.conversationType) {
+        case (window as any).TUIKitTUICore.TIM.TYPES.CONV_C2C:
+          // eslint-disable-next-line no-case-declarations, no-unsafe-optional-chaining
+          const { flow, to, from } = data?.data?.message;
+          if (to === from) break;
+          try {
+            await (window as any)?.TUIKitTUICore?.TUIServer?.TUICallKit?.call({
+              userID: flow === 'out' ? to : from,
+              type: callType
+            });
+            (window as any)?.TUIKitTUICore?.isOfficial &&
+              VuexStore?.commit &&
+              VuexStore?.commit('handleTask', 6);
+          } catch (error) {
+            console.warn(error);
+          }
+          break;
+        case (window as any).TUIKitTUICore.TIM.TYPES.CONV_GROUP:
+          break;
+        default:
+          break;
+      }
+    };
+
+    return {
+      ...toRefs(data),
+      isUrl,
+      openLink,
+      handleCallMessageIcon,
+      handleCallAgain
+    };
+  }
+});
+</script>
+<style lang="scss" scoped>
+@import url('../../../styles/common.scss');
+@import url('../../../styles/icon.scss');
+a {
+  color: #679ce1;
+}
+.custom {
+  font-size: 14px;
+  h1 {
+    font-size: 14px;
+    color: #000000;
+  }
+  h1,
+  a,
+  p {
+    font-size: 14px;
+  }
+  .evaluate {
+    ul {
+      display: flex;
+      padding-top: 10px;
+    }
+  }
+  .order {
+    display: flex;
+    main {
+      padding-left: 5px;
+      p {
+        font-family: PingFangSC-Regular;
+        width: 145px;
+        line-height: 17px;
+        font-size: 14px;
+        color: #999999;
+        letter-spacing: 0;
+        margin-bottom: 6px;
+        word-break: break-word;
+      }
+      span {
+        font-family: PingFangSC-Regular;
+        line-height: 25px;
+        color: #ff7201;
+      }
+    }
+    img {
+      width: 67px;
+      height: 67px;
+    }
+  }
+  .call {
+    display: flex;
+    flex-direction: row;
+    align-items: center;
+    &-C2C {
+      cursor: pointer;
+    }
+    &-GROUP {
+      cursor: default;
+    }
+  }
+}
+</style>

+ 285 - 0
src/TUIKit/TUIComponents/container/TUIChat/components/message-emoji-react.vue

@@ -0,0 +1,285 @@
+<template>
+  <div class="dialog-emoji" :class="env?.isH5?'dialog-emoji-h5':''" v-if="type === 'dropdown'">
+    <div class="face-list collapse">
+      <ul class="face-list-collapse">
+        <li
+          class="face-list-item"
+          v-for="(childrenItem, childrenIndex) in emojiCollapseList"
+          :key="childrenIndex"
+          @click.stop="select(childrenItem)"
+        >
+          <img :src="emojiUrl + emojiMap[childrenItem]" />
+        </li>
+      </ul>
+      <div class="face-list-button" @click.stop="isCollapse = !isCollapse">
+        <i class="icon" :class="[isCollapse ? 'icon-expand' : 'icon-collapse']"></i>
+      </div>
+    </div>
+    <ul class="face-list face-list-expand" v-show="!isCollapse">
+      <li
+        class="face-list-item"
+        v-for="(childrenItem, childrenIndex) in emojiExpandList"
+        :key="childrenIndex"
+        @click.stop="select(childrenItem)"
+      >
+        <img :src="emojiUrl + emojiMap[childrenItem]" />
+      </li>
+    </ul>
+  </div>
+  <div v-if="type === 'content'" class="emoji-content">
+    <ul class="emoji-react" ref="container">
+      <li
+        class="emoji-react-item"
+        v-for="(val, key) in handleEmojiReact(message)"
+        :key="key"
+        v-show="val && val?.length"
+        @click.stop="select(key)"
+      >
+        <img :src="emojiUrl + emojiMap[key]" />
+        <div class="emoji-react-item-content">
+          <span>{{ handleEmojiReactItem(val) }}</span>
+        </div>
+      </li>
+    </ul>
+  </div>
+</template>
+<script lang="ts">
+import { defineComponent, watch, reactive, toRefs, computed, ref, onMounted, nextTick } from 'vue';
+import TIM from '../../../../TUICore/tim';
+import { emojiUrl, emojiMap, emojiName } from '../utils/emojiMap';
+import { JSONToObject } from '../utils/utils';
+import TUIChat from '../index.vue';
+import { Message } from '@tencentcloud/chat';
+export default defineComponent({
+  props: {
+    message: {
+      type: Object,
+      default: () => ({}),
+    },
+    emojiList: {
+      type: Array,
+      default: () => [],
+    },
+    type: {
+      type: String,
+      default: '',
+    },
+  },
+  emits: ['handleCollapse'],
+  setup(props: any, ctx: any) {
+    const { TUIServer } = TUIChat;
+    const data = reactive({
+      message: {} as Message,
+      emojiUrl,
+      emojiMap,
+      emojiName,
+      isCollapse: true,
+      types: TIM.TYPES,
+      env: TUIServer.TUICore.TUIEnv,
+      type: props.type,
+      emojiReacts: {},
+      allMemberList: [],
+    });
+
+    const container = ref({} as HTMLElement);
+
+    const emojiCollapseList = computed(() => {
+      return data.emojiName.slice(0, 6);
+    });
+
+    const emojiExpandList = computed(() => {
+      return data.emojiName.slice(6);
+    });
+
+    const select = (item: any) => {
+      TUIServer.emojiReact(data.message, item);
+    };
+
+    const handleEmojiReact = (message: any) => {
+      try {
+        if (!message?.cloudCustomData) return;
+        const reactList = JSONToObject(message?.cloudCustomData)?.messageReact?.reacts;
+        if (!reactList) return;
+        data.emojiReacts = reactList;
+        return reactList;
+      } catch (err) {
+        console.warn(err);
+      }
+    };
+
+    const handleEmojiReactItem = (userIDList: any) => {
+      let res = '';
+      userIDList?.forEach((item: any, index: any) => {
+        const memberInfo = data.allMemberList?.find((member: any) => member.userID === item);
+        res = res + (memberInfo ? (memberInfo as any)?.nick : item) + ', ';
+      });
+      if (res.length) res = res.substring(0, res.lastIndexOf(','));
+      return res;
+    };
+
+    const handleAllMemberList = (message: any) => {
+      const TUIChatStore = TUIServer?.currentStore;
+      switch (message?.conversationType) {
+        case TIM.TYPES.CONV_C2C:
+          data.allMemberList = [
+            {
+              userID: TUIChatStore?.conversation?.userProfile?.userID,
+              nick: TUIChatStore?.conversation?.userProfile?.nick,
+            },
+            {
+              userID: TUIChatStore?.selfInfo?.userID,
+              nick: TUIChatStore?.selfInfo?.nick,
+            },
+          ] as any;
+          break;
+        case TIM.TYPES.CONV_GROUP:
+          data.allMemberList = TUIChatStore?.allMemberList?.memberList;
+          break;
+        default:
+          break;
+      }
+    };
+
+    watch(
+      () => props.message,
+      (newVal: any, oldVal: any) => {
+        data.message = props.message;
+        if (newVal?.conversationID !== oldVal?.conversationID) {
+          handleAllMemberList(newVal);
+        }
+      },
+      { deep: true, immediate: true }
+    );
+
+    watch(
+      ()=>data.isCollapse,
+      (newVal, oldVal) => {
+        if (newVal === oldVal) return;
+        if (!data?.env?.isH5) return;
+        ctx.emit('handleCollapse', newVal);
+      }
+    )
+
+    return {
+      ...toRefs(data),
+      emojiCollapseList,
+      emojiExpandList,
+      select,
+      handleEmojiReact,
+      handleEmojiReactItem,
+      container,
+    };
+  },
+});
+</script>
+<style lang="scss" scoped>
+@import url('../../../styles/common.scss');
+@import url('../../../styles/icon.scss');
+.dialog-emoji {
+  margin-left: 2px;
+  background: #ffffff;
+  border: 1px solid #e0e0e0;
+  box-shadow: 0 4px 12px 0 rgba(0, 0, 0, 0.06);
+  width: 242px;
+  min-height: 28px;
+  height: fit-content;
+  padding: 2px 6px;
+  word-break: keep-all;
+  top: 30px;
+  border-radius: 8px;
+  &-h5{
+    margin: 0 5px;
+    border: none;
+    border-bottom: 1px solid #e0e0e0;
+    box-shadow: none;
+    border-radius: 10px 10px 0 0;
+  }
+  .collapse {
+    padding: 2px 0px;
+  }
+  .face-list {
+    display: flex;
+    overflow: hidden;
+    flex-wrap: nowrap;
+    flex-direction: row;
+    &-collapse {
+      display: flex;
+      overflow: hidden;
+      flex-wrap: nowrap;
+      justify-content: space-between;
+      align-items: center;
+      flex: 1;
+    }
+    &-expand {
+      border-top: 1px solid #e0e0e0;
+      display: flex;
+      overflow-x: hidden;
+      flex-wrap: wrap;
+      max-height: 90px;
+      overflow-y: auto;
+    }
+    &-button {
+      padding: 5px 7px;
+      height: 20px;
+      line-height: 20px;
+      text-align: center;
+      align-items: center;
+      i {
+        text-align: center;
+      }
+    }
+    li {
+      margin: 4px;
+      display: flex;
+      flex-direction: row;
+      align-items: center;
+      flex-direction: row;
+      img {
+        width: 22px;
+        height: 22px;
+      }
+    }
+  }
+}
+.emoji-content {
+  .emoji-react {
+    margin-top: 3px;
+    display: flex;
+    flex-direction: row;
+    flex-wrap: wrap;
+    align-items: center;
+    &-item {
+      width: fit-content;
+      height: 20px;
+      background: rgba(0, 0, 0, 0.05);
+      border-radius: 12px;
+      padding: 2px 6px;
+      margin: 2px;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      max-width: 200px;
+      display: flex;
+      img {
+        width: 16px;
+        height: 16px;
+        padding: 2px;
+      }
+      &-content {
+        overflow: hidden;
+        text-overflow: ellipsis;
+        font-size: 10px;
+        color: #999999;
+        align-self: center;
+        span {
+          margin: 2px;
+          font-size: 10px;
+          color: #999999;
+          overflow: hidden;
+          text-overflow: ellipsis;
+          white-space: nowrap;
+        }
+      }
+    }
+  }
+}
+</style>

+ 65 - 0
src/TUIKit/TUIComponents/container/TUIChat/components/message-face.vue

@@ -0,0 +1,65 @@
+<template>
+  <div class="message-image" ref="skeleton">
+    <img class="message-img" :src="data.url" :style="
+        isH5
+          ? {
+              maxWidth: data.width ? data.width + 'px' : 'calc(100vw - 180px)',
+              maxHeight: data.height ? data.height + 'px' : 'calc(100vw - 180px)',
+            }
+          : {}
+      "/>
+  </div>
+</template>
+
+<script lang="ts">
+import { defineComponent, watchEffect, reactive, toRefs, nextTick, ref } from 'vue';
+import { handleSkeletonSize } from '../utils/utils';
+
+export default defineComponent({
+  props: {
+    data: {
+      type: Object,
+      default: () => ({}),
+    },
+    isH5: {
+      type: Boolean,
+      default: false,
+    },
+  },
+  setup(props: any, ctx: any) {
+    const data = reactive({
+      data: {},
+    });
+
+    const skeleton: any = ref();
+
+    watchEffect(() => {
+      data.data = props.data;
+      if (!data.data) return;
+      nextTick(() => {
+        const containerWidth = document.getElementById('messageEle')?.clientWidth || 0;
+        const max = props.isH5 ? Math.min(containerWidth - 172, 300) : 300;
+        const size = handleSkeletonSize(240, 240, max, max);
+        skeleton?.value?.style && (skeleton.value.style.width = `${size.width}px`);
+        skeleton?.value?.style && (skeleton.value.style.height = `${size.height}px`);
+      });
+    });
+    return {
+      ...toRefs(data),
+      skeleton,
+    };
+  },
+});
+</script>
+<style lang="scss" scoped>
+@import url('../../../styles/common.scss');
+@import url('../../../styles/icon.scss');
+.text-img {
+  width: 20px;
+  height: 20px;
+}
+.message-img {
+  max-width: min(calc(100vw - 180px), 300px);
+  max-height: min(calc(100vw - 180px), 300px);
+}
+</style>

+ 108 - 0
src/TUIKit/TUIComponents/container/TUIChat/components/message-file.vue

@@ -0,0 +1,108 @@
+<template>
+  <div class="message-file">
+    <div class="box" @click="download" :title="$t('TUIChat.单击下载')">
+      <i class="icon icon-files"></i>
+      <div class="message-file-content">
+        <label>{{ data.name }}</label>
+        <span>{{ data.size }}</span>
+      </div>
+    </div>
+    <progress v-if="data.progress" :value="data.progress" max="1"></progress>
+  </div>
+</template>
+
+<script lang="ts">
+import { defineComponent, watchEffect, reactive, toRefs } from 'vue';
+
+export default defineComponent({
+  props: {
+    data: {
+      type: Object,
+      default: () => ({}),
+    },
+  },
+  setup(props: any, ctx: any) {
+    const data = reactive({
+      data: {},
+    });
+
+    watchEffect(() => {
+      data.data = props.data;
+    });
+
+    const download = () => {
+      const file: any = data.data;
+      const option: any = {
+        mode: 'cors',
+        headers: new Headers({
+          'Content-Type': 'application/x-www-form-urlencoded',
+        }),
+      };
+      // 浏览器支持fetch则用blob下载,避免浏览器点击a标签,跳转到新页面预览的行为
+      // If the browser supports fetch, use blob to download, so as to avoid the browser clicking the a tag and jumping to the preview of the new page
+      if ((window as any).fetch) {
+        fetch(file.url, option)
+          .then((res) => res.blob())
+          .then((blob) => {
+            const a = document.createElement('a');
+            const url = window.URL.createObjectURL(blob);
+            a.href = url;
+            a.download = file.name;
+            a.click();
+          });
+      } else {
+        const a = document.createElement('a');
+        a.href = file.url;
+        a.target = '_blank';
+        a.download = file.name;
+        a.click();
+      }
+    };
+
+    return {
+      ...toRefs(data),
+      download,
+    };
+  },
+});
+</script>
+<style lang="scss" scoped>
+@import url('../../../styles/common.scss');
+@import url('../../../styles/icon.scss');
+.message-file {
+  display: flex;
+  flex-direction: column;
+  .box {
+    flex: 1;
+    display: flex;
+    cursor: pointer;
+    .message-file-content {
+      flex: 1;
+      display: flex;
+      flex-direction: column;
+    }
+  }
+  progress {
+    width: 100%;
+    color: #006eff;
+    appearance: none;
+    border-radius: 0.25rem;
+    background: rgba(#ffffff, 1);
+    width: 100%;
+    height: 0.5rem;
+    &::-webkit-progress-value {
+      background-color: #006eff;
+      border-radius: 0.25rem;
+    }
+    &::-webkit-progress-bar {
+      border-radius: 0.25rem;
+      background: rgba(#ffffff, 1);
+    }
+    &::-moz-progress-bar {
+      color: #006eff;
+      background: #006eff;
+      border-radius: 0.25rem;
+    }
+  }
+}
+</style>

+ 258 - 0
src/TUIKit/TUIComponents/container/TUIChat/components/message-image.vue

@@ -0,0 +1,258 @@
+<template>
+  <div class="message-image" @click.self="toggleShow" ref="skeleton">
+    <img
+      class="message-img"
+      :src="data.url"
+      :width="data.width"
+      :height="data.height"
+      :style="
+        isH5
+          ? {
+              maxWidth: data.width ? data.width + 'px' : 'calc(100vw - 180px)',
+              maxHeight: data.height
+                ? data.height + 'px'
+                : 'calc(100vw - 180px)'
+            }
+          : {}
+      "
+    />
+    <div class="progress" v-if="data.progress">
+      <progress :value="data.progress" max="1"></progress>
+    </div>
+    <div class="dialog" v-if="show" @click.self="toggleShow">
+      <header v-if="!isH5">
+        <i class="icon icon-close" @click.stop="toggleShow"></i>
+      </header>
+      <div
+        class="dialog-box"
+        :class="[isH5 ? 'dialog-box-h5' : '']"
+        @click.self="toggleShow"
+      >
+        <img
+          :class="[isWidth ? 'isWidth' : 'isHeight']"
+          :src="data.message.payload.imageInfoArray[0].url"
+          @click.self="toggleShow"
+        />
+        <div v-if="isH5" class="dialog-box-h5-footer">
+          <p @click="toggleShow">
+            <img src="../../../assets/icon/close-image.png" />
+          </p>
+          <p @click.stop="downloadImage(data.message)">
+            <img src="../../../assets/icon/downaload-image.png" />
+          </p>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts">
+import {
+  defineComponent,
+  watchEffect,
+  reactive,
+  toRefs,
+  computed,
+  ref,
+  nextTick
+} from 'vue';
+import { handleSkeletonSize } from '../utils/utils';
+export default defineComponent({
+  props: {
+    data: {
+      type: Object,
+      default: () => ({})
+    },
+    isH5: {
+      type: Boolean,
+      default: false
+    }
+  },
+  emits: ['uploading', 'previewImage'],
+  setup(props: any, ctx: any) {
+    const data = reactive({
+      data: {
+        progress: 0
+      },
+      show: false
+    });
+
+    const skeleton: any = ref();
+
+    const isWidth = computed(() => {
+      // eslint-disable-next-line no-unsafe-optional-chaining
+      const { width = 0, height = 0 } = (data.data as any)?.message?.payload
+        ?.imageInfoArray[0];
+      return width >= height;
+    });
+
+    watchEffect(() => {
+      data.data = props.data;
+      if (!data.data) return;
+      nextTick(() => {
+        if (!data.data.progress) {
+          const { width = 0, height = 0 } = data.data as any;
+          if (width === 0 || height === 0) return;
+          const containerWidth =
+            document.getElementById('app')?.clientWidth || 0;
+          const max = props.isH5 ? Math.min(containerWidth - 180, 300) : 300;
+          const size = handleSkeletonSize(width, height, max, max);
+          skeleton?.value?.style &&
+            (skeleton.value.style.width = `${size.width}px`);
+          skeleton?.value?.style &&
+            (skeleton.value.style.height = `${size.height}px`);
+        } else {
+          ctx.emit('uploading');
+        }
+      });
+    });
+
+    const toggleShow = () => {
+      if (!data.data.progress) {
+        ctx.emit('previewImage', (data.data as any).message);
+      }
+    };
+    const downloadImage = (message: any) => {
+      const targetImage = document.createElement('a');
+      const downloadImageName = message.payload.imageInfoArray[0].instanceID;
+      targetImage.setAttribute('download', downloadImageName);
+      const image = new Image();
+      image.src = message.payload.imageInfoArray[0].url;
+      image.setAttribute('crossOrigin', 'Anonymous');
+      image.onload = () => {
+        targetImage.href = getImageDataURL(image);
+        targetImage.click();
+      };
+    };
+
+    const getImageDataURL = (image: any) => {
+      const canvas = document.createElement('canvas');
+      canvas.width = image.width;
+      canvas.height = image.height;
+      const ctx = canvas.getContext('2d');
+      ctx?.drawImage(image, 0, 0, image.width, image.height);
+      const extension = image.src
+        .substring(image.src.lastIndexOf('.') + 1)
+        .toLowerCase();
+      return canvas.toDataURL(`image/${extension}`, 1);
+    };
+
+    return {
+      ...toRefs(data),
+      toggleShow,
+      skeleton,
+      isWidth,
+      downloadImage
+    };
+  }
+});
+</script>
+<style lang="scss" scoped>
+@import url('../../../styles/common.scss');
+@import url('../../../styles/icon.scss');
+.message-image {
+  position: relative;
+  overflow: hidden;
+  opacity: 1;
+  .message-img {
+    max-width: min(calc(100vw - 180px), 300px);
+    max-height: min(calc(100vw - 180px), 300px);
+    width: inherit;
+    height: inherit;
+  }
+  .progress {
+    position: absolute;
+    box-sizing: border-box;
+    width: 100%;
+    height: 100%;
+    padding: 0 20px;
+    left: 0;
+    top: 0;
+    background: rgba(#000000, 0.5);
+    display: flex;
+    align-items: center;
+    progress {
+      color: #006eff;
+      appearance: none;
+      border-radius: 0.25rem;
+      background: rgba(#ffffff, 1);
+      width: 100%;
+      height: 0.5rem;
+      &::-webkit-progress-value {
+        background-color: #006eff;
+        border-radius: 0.25rem;
+      }
+      &::-webkit-progress-bar {
+        border-radius: 0.25rem;
+        background: rgba(#ffffff, 1);
+      }
+      &::-moz-progress-bar {
+        color: #006eff;
+        background: #006eff;
+        border-radius: 0.25rem;
+      }
+    }
+  }
+}
+.dialog {
+  position: fixed;
+  z-index: 12;
+  width: 100vw;
+  height: 100vh;
+  background: rgba(#000000, 0.3);
+  top: 0;
+  left: 0;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  header {
+    display: flex;
+    justify-content: flex-end;
+    background: rgba(0, 0, 0, 0.49);
+    width: 100%;
+    box-sizing: border-box;
+    padding: 10px 10px;
+  }
+  &-box {
+    display: flex;
+    flex: 1;
+    max-height: 100%;
+    padding: 6rem;
+    box-sizing: border-box;
+    justify-content: center;
+    align-items: center;
+  }
+}
+.dialog-box-h5 {
+  width: 100%;
+  height: 100%;
+  background: #000000;
+  padding: 30px 0;
+  display: flex;
+  flex-direction: column;
+  &-footer {
+    position: fixed;
+    bottom: 10px;
+    display: flex;
+    width: 90vw;
+    justify-content: space-between;
+    p {
+      width: 3.88rem;
+      height: 3.88rem;
+    }
+    img {
+      width: 100%;
+    }
+    i {
+      padding: 20px;
+    }
+  }
+}
+
+.isWidth {
+  width: 100%;
+}
+.isHeight {
+  height: 100%;
+}
+</style>

+ 40 - 0
src/TUIKit/TUIComponents/container/TUIChat/components/message-location.vue

@@ -0,0 +1,40 @@
+<template>
+  <a class="message-location" :href="data.href" target="_blank" title="点击查看详情">
+    <span class="el-icon-location-outline">{{data.description}}</span>
+    <img :src="data.url" />
+  </a>
+</template>
+
+<script lang="ts">
+import { defineComponent, watchEffect, reactive, toRefs } from 'vue';
+
+export default defineComponent({
+  props: {
+    data: {
+      type: Object,
+      default: () => ({}),
+    },
+  },
+  setup(props:any, ctx:any) {
+    const data = reactive({
+      data: {},
+    });
+
+    watchEffect(() => {
+      data.data = props.data;
+    });
+
+    return {
+      ...toRefs(data),
+    };
+  },
+});
+</script>
+<style lang="scss" scoped>
+@import url('../../../styles/common.scss');
+@import url('../../../styles/icon.scss');
+.message-location {
+  display: flex;
+  flex-direction: column;
+}
+</style>

Деякі файли не було показано, через те що забагато файлів було змінено