Message.vue 6.6 KB

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