123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290 |
- <template>
- <view class="container">
- <!-- 输入区域 -->
- <view class="input-area">
- <input v-model="question" placeholder="请输入问题" :disabled="isLoading"
- @confirm="startStreamAnswer(question)" />
- <button @click="startStreamAnswer(question)" :disabled="isLoading || !question.trim()"
- :class="{ 'loading': isLoading }">
- {{ isLoading ? '回答中...' : '发送' }}
- </button>
- </view>
- <!-- 回答区域 -->
- <view class="answer-area" v-if="answer || isLoading">
- <view class="answer-content">
- <text>{{ displayText }}</text>
- <!-- 打字机光标 -->
- <text v-if="!isComplete" class="cursor">|</text>
- </view>
- <!-- 加载状态 -->
- <view v-if="isLoading" class="loading-status">
- <view class="loading-dots">
- <text>.</text>
- <text>.</text>
- <text>.</text>
- </view>
- </view>
- <!-- 错误提示 -->
- <view v-if="error" class="error-message">
- <text>{{ error }}</text>
- <button @click="startStreamAnswer(question)" class="retry-btn">
- 重试
- </button>
- </view>
- </view>
- </view>
- </template>
- <script>
- // 在页面或组件中
- export default {
- data() {
- return {
- question: '', // 用户输入的问题:给我一段200字的数学老师的个人介绍
- answer: '', // 完整的回答
- displayText: '', // 用于显示的文本
- isComplete: false, // 是否完成
- isLoading: false, // 是否正在加载
- error: null, // 错误信息
- retryCount: 0, // 重试次数
- maxRetries: 3, // 最大重试次数
- timer: null, // 定时器
- lastResponseTime: 0, // 最后一次响应时间
- timeout: 30000, // 超时时间(30秒)
- typingSpeed: 50, // 打字速度(毫秒)
- socketTask: null, // WebSocket 连接
- }
- },
- methods: {
- // 开始流式请求
- async startStreamAnswer(content) {
- if (!content.trim()) {
- uni.showToast({
- title: '请输入问题',
- icon: 'none'
- });
- return;
- }
- // 重置状态
- this.resetState();
- this.isLoading = true;
- try {
- const response = await fetch(this.$apiHost + '/Work/streamAnswer?content=' + encodeURIComponent(content));
- console.log("response",response);
- const reader = response.body.getReader();
- const decoder = new TextDecoder();
- while (true) {
- const { value, done } = await reader.read();
- if (done) break;
-
- const text = decoder.decode(value);
- if (text === '[DONE]') {
- this.completeAnswer();
- break;
- }
-
- // 直接更新显示文本
- this.displayText += text;
- this.answer += text;
- this.retryCount = 0;
- this.lastResponseTime = Date.now();
- }
- } catch (error) {
- this.handleError(error);
- }
- },
- // 重置状态
- resetState() {
- this.answer = '';
- this.displayText = '';
- this.isComplete = false;
- this.error = null;
- this.retryCount = 0;
- this.lastResponseTime = Date.now();
- this.stopStreamAnswer();
- },
- // 处理错误
- handleError(error) {
- console.error('请求错误:', error);
- if (this.retryCount < this.maxRetries) {
- this.retryCount++;
- this.retryRequest();
- } else {
- this.error = '请求失败,请稍后重试';
- this.isLoading = false;
- uni.showToast({
- title: this.error,
- icon: 'none'
- });
- }
- },
- // 重试请求
- retryRequest() {
- uni.showToast({
- title: `正在重试 (${this.retryCount}/${this.maxRetries})`,
- icon: 'none'
- });
- setTimeout(() => {
- this.startStreamAnswer(this.question);
- }, 1000 * this.retryCount);
- },
- // 完成回答
- completeAnswer() {
- this.isComplete = true;
- this.isLoading = false;
- this.stopStreamAnswer();
- },
- // 停止请求
- stopStreamAnswer() {
- if (this.timer) {
- clearTimeout(this.timer);
- this.timer = null;
- }
- if (this.socketTask) {
- this.socketTask.close();
- this.socketTask = null;
- }
- },
- // 检查超时
- checkTimeout() {
- if (Date.now() - this.lastResponseTime > this.timeout) {
- this.handleError(new Error('请求超时'));
- }
- }
- },
- // 组件创建时
- created() {
- console.log('组件已创建');
- // 启动超时检查
- this.timer = setInterval(() => {
- if (this.isLoading) {
- this.checkTimeout();
- }
- }, 1000);
- },
- // 组件销毁时
- beforeDestroy() {
- this.stopStreamAnswer();
- }
- }
- </script>
- <style scoped lang="scss">
- .container {
- padding: 20rpx;
- }
- .input-area {
- display: flex;
- margin-bottom: 20rpx;
- gap: 20rpx;
- input {
- flex: 1;
- height: 100rpx;
- padding: 20rpx;
- border: 1px solid #ddd;
- border-radius: 8rpx;
- background-color: #fff;
- }
- button {
- padding: 0 30rpx;
- background-color: #007AFF;
- color: white;
- border-radius: 8rpx;
- &.loading {
- background-color: #ccc;
- }
- }
- }
- .answer-area {
- padding: 20rpx;
- background-color: #f5f5f5;
- border-radius: 10rpx;
- min-height: 200rpx;
- }
- .answer-content {
- line-height: 1.6;
- }
- .cursor {
- display: inline-block;
- animation: blink 1s infinite;
- }
- .loading-status {
- margin-top: 20rpx;
- text-align: center;
- }
- .loading-dots {
- display: inline-block;
- text {
- animation: dots 1.5s infinite;
- opacity: 0;
- &:nth-child(2) {
- animation-delay: 0.5s;
- }
- &:nth-child(3) {
- animation-delay: 1s;
- }
- }
- }
- .error-message {
- margin-top: 20rpx;
- color: #ff4d4f;
- text-align: center;
- }
- .retry-btn {
- margin-top: 10rpx;
- padding: 10rpx 20rpx;
- background-color: #ff4d4f;
- color: white;
- border-radius: 6rpx;
- }
- @keyframes blink {
- 0%,
- 100% {
- opacity: 1;
- }
- 50% {
- opacity: 0;
- }
- }
- @keyframes dots {
- 0%,
- 100% {
- opacity: 0;
- }
- 50% {
- opacity: 1;
- }
- }
- </style>
|