test.vue 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  1. <template>
  2. <view class="container">
  3. <!-- 输入区域 -->
  4. <view class="input-area">
  5. <input v-model="question" placeholder="请输入问题" :disabled="isLoading"
  6. @confirm="startStreamAnswer(question)" />
  7. <button @click="startStreamAnswer(question)" :disabled="isLoading || !question.trim()"
  8. :class="{ 'loading': isLoading }">
  9. {{ isLoading ? '回答中...' : '发送' }}
  10. </button>
  11. </view>
  12. <!-- 回答区域 -->
  13. <view class="answer-area" v-if="answer || isLoading">
  14. <view class="answer-content">
  15. <text>{{ displayText }}</text>
  16. <!-- 打字机光标 -->
  17. <text v-if="!isComplete" class="cursor">|</text>
  18. </view>
  19. <!-- 加载状态 -->
  20. <view v-if="isLoading" class="loading-status">
  21. <view class="loading-dots">
  22. <text>.</text>
  23. <text>.</text>
  24. <text>.</text>
  25. </view>
  26. </view>
  27. <!-- 错误提示 -->
  28. <view v-if="error" class="error-message">
  29. <text>{{ error }}</text>
  30. <button @click="startStreamAnswer(question)" class="retry-btn">
  31. 重试
  32. </button>
  33. </view>
  34. </view>
  35. </view>
  36. </template>
  37. <script>
  38. // 在页面或组件中
  39. export default {
  40. data() {
  41. return {
  42. question: '', // 用户输入的问题:给我一段200字的数学老师的个人介绍
  43. answer: '', // 完整的回答
  44. displayText: '', // 用于显示的文本
  45. isComplete: false, // 是否完成
  46. isLoading: false, // 是否正在加载
  47. error: null, // 错误信息
  48. retryCount: 0, // 重试次数
  49. maxRetries: 3, // 最大重试次数
  50. timer: null, // 定时器
  51. lastResponseTime: 0, // 最后一次响应时间
  52. timeout: 30000, // 超时时间(30秒)
  53. typingSpeed: 50, // 打字速度(毫秒)
  54. socketTask: null, // WebSocket 连接
  55. }
  56. },
  57. methods: {
  58. // 开始流式请求
  59. async startStreamAnswer(content) {
  60. if (!content.trim()) {
  61. uni.showToast({
  62. title: '请输入问题',
  63. icon: 'none'
  64. });
  65. return;
  66. }
  67. // 重置状态
  68. this.resetState();
  69. this.isLoading = true;
  70. try {
  71. const response = await fetch(this.$apiHost + '/Work/streamAnswer?content=' + encodeURIComponent(content));
  72. console.log("response",response);
  73. const reader = response.body.getReader();
  74. const decoder = new TextDecoder();
  75. while (true) {
  76. const { value, done } = await reader.read();
  77. if (done) break;
  78. const text = decoder.decode(value);
  79. if (text === '[DONE]') {
  80. this.completeAnswer();
  81. break;
  82. }
  83. // 直接更新显示文本
  84. this.displayText += text;
  85. this.answer += text;
  86. this.retryCount = 0;
  87. this.lastResponseTime = Date.now();
  88. }
  89. } catch (error) {
  90. this.handleError(error);
  91. }
  92. },
  93. // 重置状态
  94. resetState() {
  95. this.answer = '';
  96. this.displayText = '';
  97. this.isComplete = false;
  98. this.error = null;
  99. this.retryCount = 0;
  100. this.lastResponseTime = Date.now();
  101. this.stopStreamAnswer();
  102. },
  103. // 处理错误
  104. handleError(error) {
  105. console.error('请求错误:', error);
  106. if (this.retryCount < this.maxRetries) {
  107. this.retryCount++;
  108. this.retryRequest();
  109. } else {
  110. this.error = '请求失败,请稍后重试';
  111. this.isLoading = false;
  112. uni.showToast({
  113. title: this.error,
  114. icon: 'none'
  115. });
  116. }
  117. },
  118. // 重试请求
  119. retryRequest() {
  120. uni.showToast({
  121. title: `正在重试 (${this.retryCount}/${this.maxRetries})`,
  122. icon: 'none'
  123. });
  124. setTimeout(() => {
  125. this.startStreamAnswer(this.question);
  126. }, 1000 * this.retryCount);
  127. },
  128. // 完成回答
  129. completeAnswer() {
  130. this.isComplete = true;
  131. this.isLoading = false;
  132. this.stopStreamAnswer();
  133. },
  134. // 停止请求
  135. stopStreamAnswer() {
  136. if (this.timer) {
  137. clearTimeout(this.timer);
  138. this.timer = null;
  139. }
  140. if (this.socketTask) {
  141. this.socketTask.close();
  142. this.socketTask = null;
  143. }
  144. },
  145. // 检查超时
  146. checkTimeout() {
  147. if (Date.now() - this.lastResponseTime > this.timeout) {
  148. this.handleError(new Error('请求超时'));
  149. }
  150. }
  151. },
  152. // 组件创建时
  153. created() {
  154. console.log('组件已创建');
  155. // 启动超时检查
  156. this.timer = setInterval(() => {
  157. if (this.isLoading) {
  158. this.checkTimeout();
  159. }
  160. }, 1000);
  161. },
  162. // 组件销毁时
  163. beforeDestroy() {
  164. this.stopStreamAnswer();
  165. }
  166. }
  167. </script>
  168. <style scoped lang="scss">
  169. .container {
  170. padding: 20rpx;
  171. }
  172. .input-area {
  173. display: flex;
  174. margin-bottom: 20rpx;
  175. gap: 20rpx;
  176. input {
  177. flex: 1;
  178. height: 100rpx;
  179. padding: 20rpx;
  180. border: 1px solid #ddd;
  181. border-radius: 8rpx;
  182. background-color: #fff;
  183. }
  184. button {
  185. padding: 0 30rpx;
  186. background-color: #007AFF;
  187. color: white;
  188. border-radius: 8rpx;
  189. &.loading {
  190. background-color: #ccc;
  191. }
  192. }
  193. }
  194. .answer-area {
  195. padding: 20rpx;
  196. background-color: #f5f5f5;
  197. border-radius: 10rpx;
  198. min-height: 200rpx;
  199. }
  200. .answer-content {
  201. line-height: 1.6;
  202. }
  203. .cursor {
  204. display: inline-block;
  205. animation: blink 1s infinite;
  206. }
  207. .loading-status {
  208. margin-top: 20rpx;
  209. text-align: center;
  210. }
  211. .loading-dots {
  212. display: inline-block;
  213. text {
  214. animation: dots 1.5s infinite;
  215. opacity: 0;
  216. &:nth-child(2) {
  217. animation-delay: 0.5s;
  218. }
  219. &:nth-child(3) {
  220. animation-delay: 1s;
  221. }
  222. }
  223. }
  224. .error-message {
  225. margin-top: 20rpx;
  226. color: #ff4d4f;
  227. text-align: center;
  228. }
  229. .retry-btn {
  230. margin-top: 10rpx;
  231. padding: 10rpx 20rpx;
  232. background-color: #ff4d4f;
  233. color: white;
  234. border-radius: 6rpx;
  235. }
  236. @keyframes blink {
  237. 0%,
  238. 100% {
  239. opacity: 1;
  240. }
  241. 50% {
  242. opacity: 0;
  243. }
  244. }
  245. @keyframes dots {
  246. 0%,
  247. 100% {
  248. opacity: 0;
  249. }
  250. 50% {
  251. opacity: 1;
  252. }
  253. }
  254. </style>