index.vue 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321
  1. <template>
  2. <view class="container">
  3. <scroll-view class="chat-messages" scroll-y="true" :scroll-with-animation="true"
  4. :scroll-into-view="scrollToView"
  5. :style="{ width: $scrollViewWidth + 'px', padding: $viewPadding + 'px', maxHeight: $viewMaxHeight + 'px' }">
  6. <!-- 循环渲染消息 -->
  7. <view v-for="(message, index) in messages" :key="index" :id="'msg' + index" style="clear: both;">
  8. <view :class="['message', message.sender === 'user' ? 'user-message' : 'ai-message']">
  9. <text :user-select="true" :selectable="true">{{ message.content }}</text>
  10. </view>
  11. </view>
  12. </scroll-view>
  13. <view class="chat-input">
  14. <textarea @linechange="chatMsgMaxHeightChange" auto-height v-model="inputMessage" placeholder="输入消息..."
  15. class="input-field" />
  16. <button class="send-button" :disabled="!button_state" @click="sendMessage">发送</button>
  17. </view>
  18. </view>
  19. <view class="fab_button">
  20. <view class="history_button">
  21. <uni-fab :pattern="{ icon: 'list' }" :popMenu="false" horizontal="right" vertical="bottom"
  22. @fabClick="showHistorySession()"></uni-fab>
  23. </view>
  24. </view>
  25. <view class="drawer_container">
  26. <uni-drawer ref="showLeftDrawer" :width="220">
  27. <view>
  28. <scroll-view scroll-y="true" :style="{height: '100vh'}">
  29. <uni-list>
  30. <uni-list-item title="新建会话" :clickable="true" @click="createSession()">
  31. <template v-slot:footer>
  32. <uni-icons type="plus-filled" size="24"></uni-icons>
  33. </template>
  34. </uni-list-item>
  35. <view v-for="(session,index) in sessionList" :key="index">
  36. <uni-list-item :title="session.sessionName" :clickable="true"
  37. @click="changeSession(session.sessionId)" :ellipsis="2">
  38. <template v-slot:footer>
  39. <uni-icons type="compose" size="24"
  40. @click.stop.prevent="toEditSession(session)"></uni-icons>
  41. </template>
  42. </uni-list-item>
  43. </view>
  44. </uni-list>
  45. </scroll-view>
  46. </view>
  47. </uni-drawer>
  48. </view>
  49. <view class="edit_popup">
  50. <uni-popup ref="editPopup" type="center" :mask-click="true">
  51. <uni-card title="编辑会话信息">
  52. <uni-forms ref="editForm" :modelValue="editSession" :rules="rules">
  53. <uni-forms-item label="会话名" name="sessionName">
  54. <uni-easyinput type="text" v-model="editSession.sessionName" placeholder="请输入会话名" />
  55. </uni-forms-item>
  56. <uni-forms-item>
  57. <button type="primary" :disabled="!editButtonState" @click="submitEditSession()">确认</button>
  58. </uni-forms-item>
  59. <uni-forms-item>
  60. <button type="warn" :disabled="!editButtonState" @click="deleteSession()">删除</button>
  61. </uni-forms-item>
  62. </uni-forms>
  63. </uni-card>
  64. </uni-popup>
  65. </view>
  66. </template>
  67. <script setup>
  68. import {
  69. ref,
  70. nextTick,
  71. onMounted,
  72. reactive
  73. } from 'vue';
  74. import $modal from '@/plugins/modal.js'
  75. import { useUserStore } from '@/store/user.js'
  76. import { sendMessageToAI, getSessionList, getMessageList, setSessionName, delSession } from '@/api/AI.js'
  77. onMounted(() => {
  78. // 在组件挂载后获取窗口高度
  79. const res = uni.getWindowInfo();
  80. // 计算滚动视图宽度
  81. $scrollViewWidth.value = res.windowWidth - $viewPadding.value * 2;
  82. $windowHeight.value = res.windowHeight;
  83. $viewMaxHeight.value = res.windowHeight - 100;
  84. initHistorySession()
  85. });
  86. // 弹出会话列表侧边栏
  87. const showLeftDrawer = ref(null)
  88. function showHistorySession() {
  89. showLeftDrawer.value.open()
  90. }
  91. // 初始化会话列表
  92. const userStore = useUserStore()
  93. const sessionList = reactive([])
  94. function initHistorySession() {
  95. sessionList.length = 0
  96. getSessionList(userStore.user.useId).then(({ returnParams }) => {
  97. sessionList.push(...returnParams)
  98. })
  99. }
  100. // 新建会话
  101. function createSession() {
  102. messages.length = 0
  103. thisSessionId.value = 0
  104. showLeftDrawer.value.close()
  105. }
  106. // 切换会话
  107. function changeSession(sessionId) {
  108. thisSessionId.value = sessionId
  109. initHistoryMessage(sessionId)
  110. }
  111. // 修改会话信息
  112. const editSession = ref({})
  113. const editPopup = ref(null)
  114. const editButtonState = ref(true)
  115. function toEditSession(session) {
  116. showLeftDrawer.value.close()
  117. editSession.value = session
  118. editPopup.value.open()
  119. }
  120. function submitEditSession() {
  121. editButtonState.value = false
  122. setSessionName(editSession.value).then(res => {
  123. initHistorySession()
  124. editPopup.value.close()
  125. editButtonState.value = true
  126. })
  127. }
  128. function deleteSession() {
  129. editButtonState.value = false
  130. $modal.confirm("确认删除").then(res => {
  131. delSession(editSession.value.sessionId).then(res => {
  132. initHistorySession()
  133. createSession()
  134. editPopup.value.close()
  135. editButtonState.value = true
  136. })
  137. })
  138. .catch(res => {
  139. editButtonState.value = true
  140. })
  141. }
  142. const messages = reactive([{
  143. sender: 'assistant',
  144. content: '您好,我是ai助手'
  145. }
  146. ]);
  147. function initHistoryMessage(sessionId) {
  148. getMessageList(sessionId).then(res => {
  149. const returnParams = res.returnParams
  150. messages.length = 0
  151. for (let i = 0; i < returnParams.length; i++) {
  152. const item = returnParams[i]
  153. messages.push({
  154. sender: 'user',
  155. content: item.sendContent
  156. })
  157. messages.push({
  158. sender: 'assistant',
  159. content: item.receiveContent
  160. })
  161. }
  162. })
  163. }
  164. const thisSessionId = ref(0);
  165. const inputMessage = ref('');
  166. const scrollToView = ref('');// 滚动到特定消息的标识
  167. const button_state = ref(true)
  168. // 发送消息函数
  169. function sendMessage() {
  170. button_state.value = false
  171. if (inputMessage.value.trim() === '') {
  172. button_state.value = true
  173. return;
  174. }
  175. messages.push({
  176. sender: 'user',
  177. content: inputMessage.value
  178. });
  179. const message = {
  180. sessionId: thisSessionId.value,
  181. prompt: inputMessage.value,
  182. userId: userStore.user.useId,
  183. sort: messages.filter(item => item.sender == 'user').length
  184. }
  185. sendMessageToAI(message).then((res) => {
  186. const returnParams = res.returnParams
  187. if (thisSessionId.value == 0) { // 添加新会话
  188. thisSessionId.value = returnParams.sessionId // 新会话ID
  189. sessionList.unshift({
  190. sessionName: returnParams.sessionName,
  191. sessionId: returnParams.sessionId
  192. })
  193. } else {
  194. initHistorySession()
  195. }
  196. inputMessage.value = ''; // 清空输入框
  197. if (thisSessionId.value != returnParams.sessionId) {
  198. button_state.value = true;
  199. return
  200. }
  201. let content = ""
  202. const index = messages.length // 新增消息索引
  203. messages.push({ //
  204. sender: 'assistant',
  205. content: content
  206. });
  207. scrollToBottom(); // 滚动到底部
  208. let i = 0
  209. const charLength = 3 // 每次显示字数
  210. const messageLength = returnParams.receiveContent.length
  211. const interval = setInterval(() => { //模拟流式输出
  212. if (i < messageLength) {
  213. let sliceLength = i + charLength > messageLength?messageLength:i + charLength // 当前显示字数
  214. messages[index].content += returnParams.receiveContent.slice(i, sliceLength); // 每次输出 3 个字符
  215. i += charLength;
  216. } else {
  217. clearInterval(interval);
  218. button_state.value = true;
  219. }
  220. }, 50); // 50ms 一次
  221. })
  222. };
  223. // 滚动到底部的函数
  224. function scrollToBottom() {
  225. nextTick(() => {
  226. scrollToView.value = 'msg' + (messages.length - 1); // 更新滚动到的消息
  227. });
  228. };
  229. const $scrollViewWidth = ref(0); // 滚动视图宽度
  230. const $viewMaxHeight = ref(0); // 滚动视图最大高度
  231. const $viewPadding = ref(10); // 滚动视图内边距
  232. const $windowHeight = ref(0); // 窗口高度
  233. // 输入框行数变化时更新最大高度
  234. function chatMsgMaxHeightChange(e) {
  235. // console.log($windowHeight,e.detail.height);
  236. $viewMaxHeight.value = $windowHeight.value - e.detail.height - 70;
  237. }
  238. </script>
  239. <style lang="scss" scoped>
  240. .container {
  241. display: flex;
  242. flex-direction: column;
  243. height: 100vh;
  244. position: relative;
  245. }
  246. .chat-messages {
  247. flex: 1;
  248. background-color: #f3f3f3;
  249. overflow-y: auto;
  250. }
  251. .message {
  252. max-width: 70%;
  253. margin: 5px 0;
  254. padding: 8px;
  255. border-radius: 10px;
  256. display: inline-block;
  257. word-wrap: break-word;
  258. }
  259. .user-message {
  260. background-color: #DCF8C6;
  261. align-self: flex-end;
  262. float: right;
  263. }
  264. .ai-message {
  265. background-color: #ECECEC;
  266. align-self: flex-start;
  267. }
  268. .chat-input {
  269. display: flex;
  270. padding: 10px;
  271. background-color: #FFFFFF;
  272. border-top: 1px solid #E0E0E0;
  273. position: absolute;
  274. bottom: 0;
  275. left: 0;
  276. right: 0;
  277. }
  278. .input-field {
  279. flex: 1;
  280. border: 1px solid #E0E0E0;
  281. border-radius: 20px;
  282. padding: 12px;
  283. margin-right: 10px;
  284. }
  285. .send-button {
  286. border: none;
  287. border-radius: 20px;
  288. padding: 0 20px;
  289. background-color: #007AFF;
  290. color: white;
  291. height: 46px;
  292. }
  293. .fab_button {
  294. ::v-deep .history_button {
  295. .uni-fab__circle {
  296. bottom: 100px !important;
  297. }
  298. }
  299. }
  300. </style>