Message.vue 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  1. <template>
  2. <div ref="messageContainer" style="height: 100%;overflow-y: auto;">
  3. <div class="chat-list" v-for="(item, index) in list" :key="index" >
  4. <!-- 靠左 message -->
  5. <!-- TODO 芋艿:类型判断 -->
  6. <div class="left-message message-item" v-if="item.type !== 'user'">
  7. <div class="avatar">
  8. <el-avatar
  9. src="https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png"
  10. />
  11. </div>
  12. <div class="message">
  13. <div>
  14. <el-text class="time">{{ formatDate(item.createTime) }}</el-text>
  15. </div>
  16. <div class="left-text-container" ref="markdownViewRef">
  17. <MarkdownView class="left-text" :content="item.content" />
  18. </div>
  19. <div class="left-btns">
  20. <el-button class="btn-cus" link @click="noCopy(item.content)">
  21. <img class="btn-image" src="@/assets/ai/copy.svg"/>
  22. </el-button>
  23. <el-button class="btn-cus" link @click="onDelete(item.id)">
  24. <img class="btn-image" src="@/assets/ai/delete.svg" style="height: 17px; "/>
  25. </el-button>
  26. </div>
  27. </div>
  28. </div>
  29. <!-- 靠右 message -->
  30. <div class="right-message message-item" v-if="item.type === 'user'">
  31. <div class="avatar">
  32. <el-avatar
  33. src="https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png"
  34. />
  35. </div>
  36. <div class="message">
  37. <div>
  38. <el-text class="time">{{ formatDate(item.createTime) }}</el-text>
  39. </div>
  40. <div class="right-text-container">
  41. <div class="right-text">{{ item.content }}</div>
  42. </div>
  43. <div class="right-btns">
  44. <el-button class="btn-cus" link @click="noCopy(item.content)">
  45. <img class="btn-image" src="@/assets/ai/copy.svg"/>
  46. </el-button>
  47. <el-button class="btn-cus" link @click="onDelete(item.id)">
  48. <img class="btn-image" src="@/assets/ai/delete.svg" style="height: 17px;margin-right: 12px;"/>
  49. </el-button>
  50. </div>
  51. </div>
  52. </div>
  53. </div>
  54. </div>
  55. </template>
  56. <script setup lang="ts">
  57. import {formatDate} from "@/utils/formatTime";
  58. import MarkdownView from "@/components/MarkdownView/index.vue";
  59. import {ChatMessageApi, ChatMessageVO} from "@/api/ai/chat/message";
  60. import {useClipboard} from "@vueuse/core";
  61. import {PropType} from "vue";
  62. const {copy} = useClipboard() // 初始化 copy 到粘贴板
  63. // 判断 消息列表 滚动的位置(用于判断是否需要滚动到消息最下方)
  64. const messageContainer: any = ref(null)
  65. const isScrolling = ref(false) //用于判断用户是否在滚动
  66. // 定义 props
  67. const props = defineProps({
  68. list: {
  69. type: Array as PropType<ChatMessageVO[]>,
  70. required: true
  71. }
  72. })
  73. // ============ 处理对话滚动 ==============
  74. const scrollToBottom = async (isIgnore?: boolean) =>{
  75. await nextTick(() => {
  76. //注意要使用nexttick以免获取不到dom
  77. if (isIgnore || !isScrolling.value) {
  78. messageContainer.value.scrollTop =
  79. messageContainer.value.scrollHeight - messageContainer.value.offsetHeight
  80. }
  81. })
  82. }
  83. function handleScroll() {
  84. const scrollContainer = messageContainer.value
  85. const scrollTop = scrollContainer.scrollTop
  86. const scrollHeight = scrollContainer.scrollHeight
  87. const offsetHeight = scrollContainer.offsetHeight
  88. if ((scrollTop + offsetHeight) < (scrollHeight - 100)) {
  89. // 用户开始滚动并在最底部之上,取消保持在最底部的效果
  90. isScrolling.value = true
  91. } else {
  92. // 用户停止滚动并滚动到最底部,开启保持到最底部的效果
  93. isScrolling.value = false
  94. }
  95. }
  96. /**
  97. * 复制
  98. */
  99. const noCopy = async (content) => {
  100. copy(content)
  101. ElMessage({
  102. message: '复制成功!',
  103. type: 'success'
  104. })
  105. }
  106. /**
  107. * 删除
  108. */
  109. const onDelete = async (id) => {
  110. // 删除 message
  111. await ChatMessageApi.delete(id)
  112. ElMessage({
  113. message: '删除成功!',
  114. type: 'success'
  115. })
  116. // 回调
  117. emits('onDeleteSuccess')
  118. }
  119. // 监听 list
  120. const { list, conversationId } = toRefs(props)
  121. watch(list, async (newValue, oldValue) => {
  122. console.log('watch list', list)
  123. })
  124. // 提供方法给 parent 调用
  125. defineExpose({scrollToBottom})
  126. //
  127. const emits = defineEmits(['onDeleteSuccess'])
  128. //
  129. onMounted(async () => {
  130. messageContainer.value.addEventListener('scroll', handleScroll)
  131. })
  132. </script>
  133. <style scoped lang="scss">
  134. .message-container {
  135. position: relative;
  136. //top: 0;
  137. //bottom: 0;
  138. //left: 0;
  139. //right: 0;
  140. //width: 100%;
  141. //height: 100%;
  142. overflow-y: scroll;
  143. //padding: 0 15px;
  144. //z-index: -1;
  145. }
  146. // 中间
  147. .chat-list {
  148. display: flex;
  149. flex-direction: column;
  150. overflow-y: hidden;
  151. padding: 0 20px;
  152. .message-item {
  153. margin-top: 50px;
  154. }
  155. .left-message {
  156. display: flex;
  157. flex-direction: row;
  158. }
  159. .right-message {
  160. display: flex;
  161. flex-direction: row-reverse;
  162. justify-content: flex-start;
  163. }
  164. .avatar {
  165. //height: 170px;
  166. //width: 170px;
  167. }
  168. .message {
  169. display: flex;
  170. flex-direction: column;
  171. text-align: left;
  172. margin: 0 15px;
  173. .time {
  174. text-align: left;
  175. line-height: 30px;
  176. }
  177. .left-text-container {
  178. position: relative;
  179. display: flex;
  180. flex-direction: column;
  181. overflow-wrap: break-word;
  182. background-color: rgba(228, 228, 228, 0.8);
  183. box-shadow: 0 0 0 1px rgba(228, 228, 228, 0.8);
  184. border-radius: 10px;
  185. padding: 10px 10px 5px 10px;
  186. .left-text {
  187. color: #393939;
  188. font-size: 0.95rem;
  189. }
  190. }
  191. .right-text-container {
  192. display: flex;
  193. flex-direction: row-reverse;
  194. .right-text {
  195. font-size: 0.95rem;
  196. color: #fff;
  197. display: inline;
  198. background-color: #267fff;
  199. color: #fff;
  200. box-shadow: 0 0 0 1px #267fff;
  201. border-radius: 10px;
  202. padding: 10px;
  203. width: auto;
  204. overflow-wrap: break-word;
  205. }
  206. }
  207. .left-btns {
  208. display: flex;
  209. flex-direction: row;
  210. margin-top: 8px;
  211. }
  212. .right-btns {
  213. display: flex;
  214. flex-direction: row-reverse;
  215. margin-top: 8px;
  216. }
  217. }
  218. // 复制、删除按钮
  219. .btn-cus {
  220. display: flex;
  221. background-color: transparent;
  222. align-items: center;
  223. .btn-image {
  224. height: 20px;
  225. }
  226. }
  227. .btn-cus:hover {
  228. cursor: pointer;
  229. background-color: #f6f6f6;
  230. }
  231. }
  232. </style>