Chat.vue 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. <script setup lang="ts">
  2. import Message from './message/Message.vue';
  3. import { useProjectSetting } from '@/hooks/setting/useProjectSetting';
  4. import { computed, ref } from 'vue';
  5. import { useChatStore } from './store/useChatStore';
  6. import { useScroll } from './store/useScroll';
  7. import { useDialog } from 'naive-ui';
  8. import SvgIcon from '@/components/SvgIcon/index.vue';
  9. const dialog = useDialog();
  10. const chatStore = useChatStore();
  11. const { scrollRef, contentRef, scrollToBottom } = useScroll();
  12. const { isMobile } = useProjectSetting();
  13. const loading = ref<boolean>(false);
  14. const message = ref('');
  15. let controller = new AbortController();
  16. const footerClass = computed(() => {
  17. let classes = ['p-4'];
  18. if (isMobile.value) {
  19. classes = ['sticky', 'left-0', 'bottom-0', 'right-0', 'p-2', 'pr-3', 'overflow-hidden'];
  20. }
  21. return classes;
  22. });
  23. const chatIsLoading = computed(() => {
  24. return chatStore.chatIsLoading;
  25. });
  26. const buttonDisabled = computed(() => {
  27. return loading.value;
  28. });
  29. // 初始化加载数据
  30. chatStore.loadData();
  31. const dataSources = computed(() => {
  32. // 获取当前聊天窗口的数据
  33. scrollToBottom();
  34. return chatStore.messages;
  35. });
  36. function handleEnter(event: KeyboardEvent) {
  37. if (!isMobile.value) {
  38. if (event.key === 'Enter' && !event.shiftKey) {
  39. event.preventDefault();
  40. handleSubmit();
  41. }
  42. } else {
  43. if (event.key === 'Enter' && event.ctrlKey) {
  44. event.preventDefault();
  45. handleSubmit();
  46. }
  47. }
  48. }
  49. async function handleSubmit() {}
  50. function handleStop() {
  51. if (loading.value) {
  52. controller.abort();
  53. loading.value = false;
  54. }
  55. }
  56. // 删除
  57. function handleDelete(item: any) {
  58. if (loading.value) {
  59. return;
  60. }
  61. dialog.warning({
  62. title: '删除消息',
  63. content: '确认删除消息',
  64. positiveText: '是',
  65. negativeText: '否',
  66. onPositiveClick: () => {
  67. chatStore.delMessage(item);
  68. },
  69. });
  70. }
  71. // 清除
  72. function handleClear() {
  73. if (loading.value) {
  74. return;
  75. }
  76. dialog.warning({
  77. title: '清除聊天',
  78. content: '确认清除聊天',
  79. positiveText: '是',
  80. negativeText: '否',
  81. onPositiveClick: async () => {
  82. console.log('清除聊天');
  83. },
  84. });
  85. }
  86. </script>
  87. <template>
  88. <div class="flex flex-col w-full h-full">
  89. <!-- 聊天记录窗口 -->
  90. <main class="flex-1 overflow-hidden">
  91. <div ref="contentRef" class="h-full overflow-hidden overflow-y-auto">
  92. <div v-if="chatIsLoading" class="w-full h-full flex items-center justify-center">
  93. <n-spin :show="chatIsLoading" size="large" />
  94. </div>
  95. <div
  96. v-else
  97. ref="scrollRef"
  98. class="w-full max-w-screen-xl m-auto"
  99. :class="[isMobile ? 'p-2' : 'p-5']"
  100. >
  101. <Message
  102. v-for="(item, index) of dataSources"
  103. :key="index"
  104. :date-time="item.createTime"
  105. :text="item.message"
  106. :inversion="item.role !== 'assistant'"
  107. :error="item.isError"
  108. :loading="loading"
  109. @delete="handleDelete(item)"
  110. />
  111. <div class="sticky bottom-0 left-0 flex justify-center">
  112. <NButton v-if="loading" type="warning" @click="handleStop">
  113. <template #icon>
  114. <SvgIcon icon="ri:stop-circle-line" />
  115. </template>
  116. Stop Responding
  117. </NButton>
  118. </div>
  119. </div>
  120. </div>
  121. </main>
  122. <!-- 底部 -->
  123. <footer :class="footerClass">
  124. <div class="w-full max-w-screen-xl m-auto pl-8 pr-8 pb-0">
  125. <div class="flex items-center justify-between space-x-2">
  126. <NInput
  127. ref="inputRef"
  128. v-model:value="message"
  129. type="textarea"
  130. :autosize="{ minRows: 3, maxRows: isMobile ? 4 : 8 }"
  131. @keypress="handleEnter"
  132. class="custom-input"
  133. >
  134. <template #suffix>
  135. <div class="flex items-center gap-2">
  136. <NButton
  137. type="default"
  138. size="small"
  139. :disabled="buttonDisabled"
  140. @click="handleSubmit"
  141. >
  142. <SvgIcon icon="ph:file-plus-duotone" />
  143. </NButton>
  144. <NButton
  145. type="primary"
  146. size="small"
  147. :disabled="buttonDisabled"
  148. @click="handleSubmit"
  149. >
  150. <SvgIcon icon="ri:send-plane-fill" />
  151. </NButton>
  152. </div>
  153. </template>
  154. </NInput>
  155. </div>
  156. </div>
  157. </footer>
  158. </div>
  159. </template>
  160. <style scoped lang="less">
  161. ::v-deep(.custom-input) {
  162. .n-input-wrapper {
  163. padding-right: 10px;
  164. }
  165. .n-input__suffix {
  166. align-items: end;
  167. padding-bottom: 6px;
  168. }
  169. }
  170. </style>