diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..e15919f
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,17 @@
+# EditorConfig is awesome: https://EditorConfig.org
+
+# top-most EditorConfig file
+root = true
+
+[*]
+indent_style = space
+indent_size = 4
+tab_width = 4
+end_of_line = lf
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
+
+[*.md]
+trim_trailing_whitespace = false
+insert_final_newline = false
\ No newline at end of file
diff --git a/pages/chat/ChatInputArea.vue b/pages/chat/ChatInputArea.vue
index 4a60a56..c59d55c 100644
--- a/pages/chat/ChatInputArea.vue
+++ b/pages/chat/ChatInputArea.vue
@@ -26,7 +26,8 @@
maxlength="300"
/>
-
+
+
@@ -36,7 +37,9 @@ import { ref, watch, nextTick, onMounted, onUnmounted } from 'vue'
const props = defineProps({
inputMessage: String,
- holdKeyboard: Boolean
+ holdKeyboard: Boolean,
+ isSessionActive: Boolean,
+ stopRequest: Function
})
const emit = defineEmits(['update:inputMessage', 'send', 'noHideKeyboard', 'keyboardShow', 'keyboardHide'])
@@ -46,6 +49,7 @@ const inputMessage = ref(props.inputMessage || '')
const isFocused = ref(false)
const keyboardHeight = ref(0)
+
// 保持和父组件同步
watch(() => props.inputMessage, (val) => {
inputMessage.value = val
@@ -65,16 +69,24 @@ onMounted(() => {
})
const sendMessage = () => {
- if (!inputMessage.value.trim()) return;
- emit('send', inputMessage.value)
- inputMessage.value = ''
- emit('update:inputMessage', inputMessage.value)
-
- // 发送后保持焦点(可选)
- if (props.holdKeyboard && textareaRef.value) {
- nextTick(() => {
- textareaRef.value.focus()
- })
+ if (props.isSessionActive) {
+ // 如果会话进行中,调用停止请求函数
+ if (props.stopRequest) {
+ props.stopRequest();
+ }
+ } else {
+ // 否则发送新消息
+ if (!inputMessage.value.trim()) return;
+ emit('send', inputMessage.value)
+ inputMessage.value = ''
+ emit('update:inputMessage', inputMessage.value)
+
+ // 发送后保持焦点(可选)
+ if (props.holdKeyboard && textareaRef.value) {
+ nextTick(() => {
+ textareaRef.value.focus()
+ })
+ }
}
}
@@ -128,7 +140,7 @@ defineExpose({
margin: 0 12px;
/* 确保输入框在安全区域内 */
margin-bottom: 8px;
-
+
.input-container-voice {
display: flex;
align-items: center;
@@ -137,13 +149,13 @@ defineExpose({
height: 44px;
flex-shrink: 0;
align-self: flex-end;
-
+
image {
width: 22px;
height: 22px;
}
}
-
+
.input-container-send {
display: flex;
align-items: center;
@@ -152,13 +164,13 @@ defineExpose({
height: 44px;
flex-shrink: 0;
align-self: flex-end;
-
+
image {
width: 28px;
height: 28px;
}
}
-
+
.textarea {
flex: 1;
max-height: 92px;
@@ -169,4 +181,4 @@ defineExpose({
align-items: center;
}
}
-
\ No newline at end of file
+
diff --git a/pages/chat/ChatMainList.vue b/pages/chat/ChatMainList.vue
index d6c81a8..979c833 100644
--- a/pages/chat/ChatMainList.vue
+++ b/pages/chat/ChatMainList.vue
@@ -2,7 +2,7 @@
-
+
-
+
-
-
-
+
+
-
+
@@ -36,39 +36,41 @@
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
@@ -79,7 +81,7 @@
import { ref } from 'vue'
import { defineEmits } from 'vue'
import { onLoad } from '@dcloudio/uni-app';
- import { SCROLL_TO_BOTTOM, RECOMMEND_POSTS_TITLE, SEND_COMMAND_TEXT } from '@/constant/constant'
+ import { SCROLL_TO_BOTTOM, RECOMMEND_POSTS_TITLE, SEND_COMMAND_TEXT } from '@/constant/constant'
import { MessageRole, MessageType, CompName } from '../../model/ChatModel';
import ChatTopWelcome from './ChatTopWelcome.vue';
@@ -92,44 +94,46 @@
import ChatMoreTips from './ChatMoreTips.vue';
import ChatInputArea from './ChatInputArea.vue'
import CommandWrapper from '@/components/CommandWrapper/index.vue'
-
+
import QuickBookingComponent from '../module/booking/QuickBookingComponent.vue'
import DiscoveryCardComponent from '../module/discovery/DiscoveryCardComponent.vue';
import ActivityListComponent from '../module/banner/ActivityListComponent.vue';
import RecommendPostsComponent from '../module/recommend/RecommendPostsComponent.vue';
import AttachListComponent from '../module/attach/AttachListComponent.vue';
-
+
import CreateServiceOrder from '@/components/CreateServiceOrder/index.vue'
-
+
import { agentChatStream } from '@/request/api/AgentChatStream';
import { mainPageData } from '@/request/api/MainPageDataApi';
import { conversationMsgList, recentConversation } from '@/request/api/ConversationApi';
-
-
+
+
/// 导航栏相关
const statusBarHeight = ref(20);
/// 输入框组件引用
const inputAreaRef = ref(null);
-
+
const timer = ref(null)
/// focus时,点击页面的时候不收起键盘
- const holdKeyboard = ref(false)
+ const holdKeyboard = ref(false)
/// 是否在键盘弹出,点击界面时关闭键盘
- const holdKeyboardFlag = ref(true)
+ const holdKeyboardFlag = ref(true)
/// 键盘高度
const keyboardHeight = ref(0)
/// 是否显示键盘
const isKeyboardShow = ref(false)
-
+
///(控制滚动位置)
const scrollTop = ref(99999);
-
+
/// 会话列表
const chatMsgList = ref([])
/// 输入口的输入消息
const inputMessage = ref('')
-
+ /// 加载中
+ let currentAIMsgIndex = 0
+
/// 从个渠道获取如二维,没有的时候就返回首页的数据
const sceneId = ref('')
/// agentId 首页接口中获取
@@ -140,13 +144,15 @@
const mainPageDataModel = ref({})
// 会话进行中标志
- let isSessionActive = false;
+ const isSessionActive = ref(false);
+ // 请求任务引用
+ const requestTaskRef = ref(null);
/// 指令
let commonType = ''
-
-
-
+
+
+
// 打开抽屉
const emits = defineEmits(['openDrawer'])
const openDrawer = () => {
@@ -164,12 +170,12 @@
holdKeyboardFlag.value = true
}, 100)
}
-
+
// 点击输入框、发送按钮时,不收键盘
const handleNoHideKeyboard = () => {
holdKeyboardFlag.value = false
}
-
+
// 键盘弹起事件
const handleKeyboardShow = (height) => {
keyboardHeight.value = height
@@ -180,7 +186,7 @@
scrollToBottom()
}, 150)
}
-
+
// 键盘收起事件
const handleKeyboardHide = () => {
keyboardHeight.value = 0
@@ -202,7 +208,7 @@
scrollToBottom()
}, 100)
}
-
+
/// 发送普通消息
const handleReply = (text) => {
sendMessage(text)
@@ -229,7 +235,7 @@
if (!inputText.trim()) return;
handleNoHideKeyboard()
sendMessage(inputText)
- if(!isSessionActive) {
+ if(!isSessionActive.value) {
inputMessage.value = ''
}
// 发送消息后保持键盘状态
@@ -248,14 +254,14 @@
}
});
});
-
+
onMounted( async() => {
getMainPageData()
await loadRecentConversation()
loadConversationMsgList()
addNoticeListener()
})
-
+
const addNoticeListener = () => {
uni.$on(SCROLL_TO_BOTTOM, (value) => {
setTimeout(() => {
@@ -279,7 +285,7 @@
}
})
}
-
+
/// 获取最近一次的会话id
const loadRecentConversation = async() => {
const res = await recentConversation()
@@ -287,7 +293,7 @@
conversationId.value = res.data.conversationId
}
}
-
+
/// 加载历史消息的数据
let historyCurrentPageNum = 1
const loadConversationMsgList = async() => {
@@ -305,7 +311,7 @@
scrollToBottom()
}
}
-
+
/// 初始化数据 首次数据加载的时候
const initData = () => {
const msg = {
@@ -315,18 +321,18 @@
}
chatMsgList.value.push(msg)
}
-
-
+
+
/// 发送消息的参数拼接
const sendMessage = (message, isInstruct = false) => {
- if (isSessionActive) {
+ if (isSessionActive.value) {
uni.showToast({
title: '请等待当前回复完成',
icon: 'none'
});
return;
}
- isSessionActive = true;
+ isSessionActive.value = true;
const newMsg = {
msgId: `msg_${chatMsgList.value.length}`,
msgType: MessageRole.ME,
@@ -337,10 +343,10 @@
}
}
chatMsgList.value.push(newMsg)
- sendChat(message, isInstruct)
+ sendChat(message, isInstruct)
console.log("发送的新消息:",JSON.stringify(newMsg))
}
-
+
/// 打字机效果实现的变量
let loadingTimer = null;
let typeWriterTimer = null;
@@ -355,7 +361,7 @@
messageType: isInstruct ? 1 : 0,
messageContent: isInstruct ? commonType : message
}
-
+
// 插入AI消息
const aiMsg = {
msgId: `msg_${chatMsgList.value.length}`,
@@ -368,7 +374,8 @@
}
chatMsgList.value.push(aiMsg)
const aiMsgIndex = chatMsgList.value.length - 1
-
+ currentAIMsgIndex = aiMsgIndex
+
// 动态加载中动画
let dotCount = 1;
loadingTimer && clearInterval(loadingTimer);
@@ -376,16 +383,16 @@
dotCount = dotCount % 3 + 1;
chatMsgList.value[aiMsgIndex].msg = '加载中' + '.'.repeat(dotCount);
}, 400);
-
+
aiMsgBuffer = '';
isTyping = false;
if (typeWriterTimer) {
clearTimeout(typeWriterTimer);
typeWriterTimer = null;
}
-
+
// 流式接收内容
- agentChatStream(args, (chunk) => {
+ const { promise, requestTask } = agentChatStream(args, (chunk) => {
console.log('分段内容:', chunk)
if (chunk && chunk.error) {
chatMsgList.value[aiMsgIndex].msg = '请求错误,请重试';
@@ -393,7 +400,7 @@
loadingTimer = null;
isTyping = false;
typeWriterTimer = null;
- isSessionActive = false; // 出错也允许再次发送
+ isSessionActive.value = false; // 出错也允许再次发送
console.error('流式错误:', chunk.message, chunk.detail);
return;
}
@@ -406,7 +413,7 @@
}
// 把新内容追加到缓冲区
aiMsgBuffer += chunk.content;
-
+
// 启动打字机(只启动一次)
if (!isTyping) {
isTyping = true;
@@ -423,7 +430,7 @@
loadingTimer = null;
isTyping = false;
typeWriterTimer = null;
-
+
// 补全:如果消息内容还停留在'加载中.'或为空,则给出友好提示
const msg = chatMsgList.value[aiMsgIndex].msg;
console.log('msg:', msg)
@@ -433,33 +440,41 @@
chatMsgList.value[aiMsgIndex].msg = '';
}
}
- // 如果有组件
+ // 如果有组件
if(chunk.toolCall) {
console.log('chunk.toolCall:', chunk.toolCall)
chatMsgList.value[aiMsgIndex].toolCall = chunk.toolCall
}
-
+
// 如果有问题,则设置问题
if(chunk.question && chunk.question.length > 0) {
chatMsgList.value[aiMsgIndex].question = chunk.question
}
-
- isSessionActive = false;
+
+ isSessionActive.value = false;
scrollToBottom();
}
}, 50);
}
- }).catch(e => {
- isSessionActive = false; // 出错也允许再次发送
- console.log('error:', e)
+ })
+
+ // 存储请求任务
+ requestTaskRef.value = requestTask;
+
+ // 可选:处理Promise完成/失败, 已经在回调中处理数据,此处无需再处理
+ promise.then(data => {
+ console.log('请求完成');
+ }).catch(err => {
+ isSessionActive.value = false; // 出错也允许再次发送
+ console.log('error:', err)
});
-
+
// 打字机函数
function typeWriter() {
if (aiMsgBuffer.length > 0) {
chatMsgList.value[aiMsgIndex].msg += aiMsgBuffer[0];
aiMsgBuffer = aiMsgBuffer.slice(1);
-
+
nextTick(() => {
scrollToBottom();
});
@@ -468,12 +483,35 @@
// 等待新内容到来,不结束
typeWriterTimer = setTimeout(typeWriter, 30);
}
- }
+ }
}
-
-
+
+
+ // 停止请求函数
+ const stopRequest = () => {
+ if (requestTaskRef.value && requestTaskRef.value.abort) {
+ requestTaskRef.value.abort();
+ // 重置状态
+ isSessionActive.value = false;
+ const msg = chatMsgList.value[currentAIMsgIndex].msg;
+ if (!msg || msg === '加载中.' || msg.startsWith('加载中')) {
+ chatMsgList.value[currentAIMsgIndex].msg = '已终止请求,请重试';
+ }
+ // 清除计时器
+ if (loadingTimer) {
+ clearInterval(loadingTimer);
+ loadingTimer = null;
+ }
+ if (typeWriterTimer) {
+ clearTimeout(typeWriterTimer);
+ typeWriterTimer = null;
+ }
+ setTimeoutScrollToBottom()
+ }
+ }
+
\ No newline at end of file
+
diff --git a/request/api/AgentChatStream.js b/request/api/AgentChatStream.js
index f01cd78..0cba5cf 100644
--- a/request/api/AgentChatStream.js
+++ b/request/api/AgentChatStream.js
@@ -7,16 +7,17 @@ const API = '/agent/assistant/chat';
* 获取AI聊天流式信息(仅微信小程序支持)
* @param {Object} params 请求参数
* @param {Function} onChunk 回调,每收到一段数据触发
- * @returns {Promise}
+ * @returns {Object} 包含Promise和requestTask的对象
*/
function agentChatStream(params, onChunk) {
- return new Promise((resolve, reject) => {
+ let requestTask;
+ const promise = new Promise((resolve, reject) => {
const token = uni.getStorageSync('token');
let hasError = false;
console.log("发送请求内容: ", params)
// #ifdef MP-WEIXIN
- const requestTask = uni.request({
+ requestTask = uni.request({
url: BASE_URL + API, // 替换为你的接口地址
method: 'POST',
data: params,
@@ -28,22 +29,21 @@ function agentChatStream(params, onChunk) {
},
responseType: 'arraybuffer',
success(res) {
- resolve(res.data);
+ resolve(res.data);
},
fail(err) {
console.log("====> ", JSON.stringify(err))
- reject(err);
+ reject(err);
},
- complete(res) {
+ complete(res) {
if(res.statusCode !== 200) {
- console.log("====> ", JSON.stringify(res))
-
- if (onChunk) {
- onChunk({ error: true, message: '服务器错误', detail: res });
- }
- reject(res);
- }
- }
+ console.log("====> ", JSON.stringify(res))
+ if (onChunk) {
+ onChunk({ error: true, message: '服务器错误', detail: res });
+ }
+ reject(res);
+ }
+ }
});
requestTask.onHeadersReceived(res => {
@@ -75,18 +75,19 @@ function agentChatStream(params, onChunk) {
});
// #endif
});
+
+ return {
+ promise,
+ requestTask
+ };
}
// window.atob兼容性处理
const weAtob = (string) => {
- const b64re =
- /^(?:[A-Za-z\d+/]{4})*?(?:[A-Za-z\d+/]{2}(?:==)?|[A-Za-z\d+/]{3}=?)?$/;
- const b64 =
- 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
-
+ const b64re = /^(?:[A-Za-z\d+/]{4})*?(?:[A-Za-z\d+/]{2}(?:==)?|[A-Za-z\d+/]{3}=?)?$/;
+ const b64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
// 去除空白字符
string = String(string).replace(/[\t\n\f\r ]+/g, '');
-
// 验证 Base64 编码
if (!b64re.test(string)) {
throw new TypeError(
@@ -150,4 +151,4 @@ function parseSSEChunk(raw) {
return results;
}
-export { agentChatStream }
\ No newline at end of file
+export { agentChatStream }
\ No newline at end of file
diff --git a/static/input_send_icon.png b/static/input_send_icon.png
index c8599b7..f09163e 100644
Binary files a/static/input_send_icon.png and b/static/input_send_icon.png differ
diff --git a/static/input_stop_icon.png b/static/input_stop_icon.png
new file mode 100644
index 0000000..e5227d2
Binary files /dev/null and b/static/input_stop_icon.png differ