feat(cases): 规范化案例模块接口调用及新增AI会话初始化功能

- 新增 initChat 方法用于获取AI会话ID
- 新增 handleStop方法 停止当前会话功能
This commit is contained in:
chong.yanning@ebiz-digits.com
2025-12-17 09:50:58 +08:00
committed by liu.zixi
parent 99760c9940
commit a1d5d0f079
4 changed files with 1026 additions and 662 deletions

View File

@@ -1,14 +1,14 @@
/* 案例模块的相关处理*/ /* 案例模块的相关处理*/
import ajax from '@/utils/xajax.js' import ajax from "@/utils/xajax.js";
import ajaxs from '@/api/ajax.js' import ajaxs from "@/api/ajax.js";
/**首页查询 /**首页查询
* pageSize * pageSize
* orderField * orderField
* orderAsc * orderAsc
*/ */
const indexList = function (query) { const indexList = function (query) {
return ajax.post('/xboe/m/boe/cases/case-random', query); return ajax.post("/xboe/m/boe/cases/case-random", query);
} };
/* /*
*案例分页搜索查询 是否推荐 *案例分页搜索查询 是否推荐
*@param(String) keyWord 关键词 *@param(String) keyWord 关键词
@@ -17,14 +17,14 @@ const indexList = function (query) {
*@param(Boolean) breCommend 是否推荐 true或者false *@param(Boolean) breCommend 是否推荐 true或者false
*@param(String) orgDomain 组织机构 *@param(String) orgDomain 组织机构
*@param(String) majorType 专业分类 *@param(String) majorType 专业分类
* @param(String) majorDomain 专业领域 * @param(String) majorDomain 专业领域
*/ */
const queryList = function (query) { const queryList = function (query) {
return ajax.post('/xboe/m/boe/cases/queryList', query); return ajax.post("/xboe/m/boe/cases/queryList", query);
} };
const queryListV2 = function (query) { const queryListV2 = function (query) {
return ajax.postJson('/xboe/m/boe/cases/queryListV2', query); return ajax.postJson("/xboe/m/boe/cases/queryListV2", query);
} };
/* 案例分页搜索查询 是否置顶 /* 案例分页搜索查询 是否置顶
*@param(String) keyWord 关键词 *@param(String) keyWord 关键词
@param(Boolean) isTop 是否置顶 true或false @param(Boolean) isTop 是否置顶 true或false
@@ -33,32 +33,32 @@ const queryListV2 = function (query) {
* @param(String) majorDomain 专业领域 * @param(String) majorDomain 专业领域
*/ */
const isTopList = function (query) { const isTopList = function (query) {
return ajax.post('/xboe/m/boe/cases/isTopList', query); return ajax.post("/xboe/m/boe/cases/isTopList", query);
} };
/* 设置置顶 取消置顶 /* 设置置顶 取消置顶
@param(String) id @param(String) id
@param(Integer) isTop 是否置顶 0未置顶1已置顶 @param(Integer) isTop 是否置顶 0未置顶1已置顶
*/ */
const updateTop = function (id, isTop) { const updateTop = function (id, isTop) {
return ajax.get(`/xboe/m/boe/cases/updateTop?id=${id}&isTop=${isTop}`); return ajax.get(`/xboe/m/boe/cases/updateTop?id=${id}&isTop=${isTop}`);
} };
/* 删除案例 /* 删除案例
*param(String) id 案例的id *param(String) id 案例的id
*/ */
const del = function (id) { const del = function (id) {
return ajax.get('/xboe/m/boe/cases/delete?id=' + id); return ajax.get("/xboe/m/boe/cases/delete?id=" + id);
} };
/*案例详情 /*案例详情
@param(String) id 案例的id @param(String) id 案例的id
@param(Boolean) addView 是否增加浏览量 @param(Boolean) addView 是否增加浏览量
*/ */
const detail = function (id, addView) { const detail = function (id, addView) {
let pars = 'id=' + id; let pars = "id=" + id;
if (addView) { if (addView) {
pars += '&addView=' + addView pars += "&addView=" + addView;
} }
return ajax.get('/xboe/m/boe/cases/detail?' + pars); return ajax.get("/xboe/m/boe/cases/detail?" + pars);
} };
/*推荐 /*推荐
*@param (String) id 案例的id *@param (String) id 案例的id
*@param (String) title 案例的标题 *@param (String) title 案例的标题
@@ -66,61 +66,65 @@ const detail = function (id, addView) {
*@param (String) recommentThat 推荐说明 *@param (String) recommentThat 推荐说明
*/ */
const savaRecommend = function (data) { const savaRecommend = function (data) {
return ajax.post('/xboe/m/boe/recommend/save', data); return ajax.post("/xboe/m/boe/recommend/save", data);
} };
/* 推荐列表 /* 推荐列表
*没有参数 *没有参数
*/ */
const query = function () { const query = function () {
return ajax.get('/xboe/m/boe/recommend/query'); return ajax.get("/xboe/m/boe/recommend/query");
} };
/* /*
*作者排行榜 *作者排行榜
@param pageSize 长度 @param pageSize 长度
*/ */
const usernameList = function (pageSize) { const usernameList = function (pageSize) {
return ajax.get(`/xboe/m/boe/cases/usernamelist?pageSize=${pageSize}`); return ajax.get(`/xboe/m/boe/cases/usernamelist?pageSize=${pageSize}`);
} };
/* /*
人气榜 人气榜
@param pageSize 长度 @param pageSize 长度
*/ */
const queryPraises = function (pageSize) { const queryPraises = function (pageSize) {
return ajax.get(`/xboe/m/boe/cases/query-praises?pageSize=${pageSize}`); return ajax.get(`/xboe/m/boe/cases/query-praises?pageSize=${pageSize}`);
} };
const queryPraisesNew = function (pageSize,rankType) { const queryPraisesNew = function (pageSize, rankType) {
return ajax.get(`/xboe/m/boe/cases/queryPopularity?pageSize=${pageSize}&rankType=${rankType}`); return ajax.get(
} `/xboe/m/boe/cases/queryPopularity?pageSize=${pageSize}&rankType=${rankType}`
);
};
const queryRecommendRank = function (pageSize) { const queryRecommendRank = function (pageSize) {
return ajax.get(`/xboe/m/boe/cases/queryRecommendRank?pageSize=${pageSize}`); return ajax.get(`/xboe/m/boe/cases/queryRecommendRank?pageSize=${pageSize}`);
} };
/* /*
好评榜 好评榜
@param pageSize 长度 @param pageSize 长度
*/ */
const queryComments = function (pageSize) { const queryComments = function (pageSize) {
return ajax.get(`/xboe/m/boe/cases/query-comments?pageSize=${pageSize}`); return ajax.get(`/xboe/m/boe/cases/query-comments?pageSize=${pageSize}`);
} };
const queryCommentsNew = function (pageSize,rankType) { const queryCommentsNew = function (pageSize, rankType) {
return ajax.get(`/xboe/m/boe/cases/queryHighOpinion?pageSize=${pageSize}&rankType=${rankType}`); return ajax.get(
} `/xboe/m/boe/cases/queryHighOpinion?pageSize=${pageSize}&rankType=${rankType}`
);
};
/** /**
* 专业分类 * 专业分类
* */ * */
const majorTypes = function () { const majorTypes = function () {
return ajax.get('/xboe/m/boe/cases/majorTypes'); return ajax.get("/xboe/m/boe/cases/majorTypes");
} };
/** /**
* 详情新*/ * 详情新*/
const details = function (id, addView) { const details = function (id, addView) {
let pars = 'id=' + id; let pars = "id=" + id;
if (addView) { if (addView) {
pars += '&addView=' + addView pars += "&addView=" + addView;
} }
return ajax.get('/xboe/m/boe/cases/details?' + pars); return ajax.get("/xboe/m/boe/cases/details?" + pars);
} };
/** /**
* 二次查询 * 二次查询
@@ -129,8 +133,8 @@ const details = function (id, addView) {
* } * }
* */ * */
const ids = function (data) { const ids = function (data) {
return ajax.postJson('/xboe/m/boe/cases/ids', data); return ajax.postJson("/xboe/m/boe/cases/ids", data);
} };
/** /**
* 设置和取消优秀案例 * 设置和取消优秀案例
@@ -140,9 +144,8 @@ const ids = function (data) {
* } * }
* */ * */
const excellent = function (data) { const excellent = function (data) {
return ajax.post('/xboe/m/boe/cases/excellent', data) return ajax.post("/xboe/m/boe/cases/excellent", data);
} };
/** /**
*我的案例 *我的案例
@@ -152,8 +155,8 @@ const excellent = function (data) {
* } * }
* */ * */
const mylist = function (query) { const mylist = function (query) {
return ajax.post('/xboe/m/boe/cases/mylist', query); return ajax.post("/xboe/m/boe/cases/mylist", query);
} };
/** /**
* 导出 * 导出
@@ -163,21 +166,26 @@ const mylist = function (query) {
* } * }
* */ * */
const exportCases = function (query) { const exportCases = function (query) {
return ajax.post('/xboe/m/boe/cases/export', query); return ajax.post("/xboe/m/boe/cases/export", query);
} };
/** /**
* 导出案例后加的 * 导出案例后加的
* */ * */
const exports = function (query) { const exports = function (query) {
return ajax.post('/xboe/m/boe/cases/exportCase', query, { responseType: 'blob' }); return ajax.post("/xboe/m/boe/cases/exportCase", query, {
} responseType: "blob",
});
};
/** /**
* @param * @param
* 记录查看开始时间 * 记录查看开始时间
* caseRecommendId读取的案例的id * caseRecommendId读取的案例的id
*/ */
const startReadTimer = (caseRecommendId) => ajax.get(`/xboe/m/boe/cases/recommend/startRead?caseRecommendId=${caseRecommendId}`); const startReadTimer = (caseRecommendId) =>
ajax.get(
`/xboe/m/boe/cases/recommend/startRead?caseRecommendId=${caseRecommendId}`
);
/** /**
* @param * @param
@@ -187,34 +195,56 @@ const startReadTimer = (caseRecommendId) => ajax.get(`/xboe/m/boe/cases/recomm
* browseDuration [浏览时间秒] * browseDuration [浏览时间秒]
* } * }
*/ */
const endReadTimer = (data)=> ajax.postJson('/xboe/m/boe/cases/recommend/recordBrowseDuration',data) const endReadTimer = (data) =>
ajax.postJson("/xboe/m/boe/cases/recommend/recordBrowseDuration", data);
/**年份查询 /**年份查询
* *
*/ */
const caseYears = function (query = {}) { const caseYears = function (query = {}) {
return ajax.post('/xboe/m/boe/cases/caseYears', query); return ajax.post("/xboe/m/boe/cases/caseYears", query);
} };
//案例推荐榜单 //案例推荐榜单
const getQueryRecommendRank = function (params) { const getQueryRecommendRank = function (params) {
return ajax.get(`/xboe/m/boe/cases/queryRecommendRank?pageSize=${params.pageSize}`) return ajax.get(
} `/xboe/m/boe/cases/queryRecommendRank?pageSize=${params.pageSize}`
);
};
//案例上榜 //案例上榜
const riseIntoRank = (data) => ajax.post(`/xboe/m/boe/cases/riseIntoRank?caseId=${data.caseId}`) const riseIntoRank = (data) =>
ajax.post(`/xboe/m/boe/cases/riseIntoRank?caseId=${data.caseId}`);
//取消上榜 //取消上榜
const cancelRiseIntoRank = (data) => ajax.post(`/xboe/m/boe/cases/cancelRiseIntoRank?caseId=${data.caseId}`) const cancelRiseIntoRank = (data) =>
ajax.post(`/xboe/m/boe/cases/cancelRiseIntoRank?caseId=${data.caseId}`);
//调整榜单 //调整榜单
const adjustRank = (data) => ajax.post(`/xboe/m/boe/cases/adjustRank?caseIds=${data.caseIds}`) const adjustRank = (data) =>
ajax.post(`/xboe/m/boe/cases/adjustRank?caseIds=${data.caseIds}`);
//收藏案例 //收藏案例
const queryFavoriteCaseOfIndex = (params) => ajax.get(`/xboe/m/boe/cases/queryFavoriteCaseOfIndex?pageIndex=${params.pageIndex}&pageSize=${params.pageSize}&orderField=${params.orderField}&orderAsc=${params.orderAsc}`) const queryFavoriteCaseOfIndex = (params) =>
ajax.get(
`/xboe/m/boe/cases/queryFavoriteCaseOfIndex?pageIndex=${params.pageIndex}&pageSize=${params.pageSize}&orderField=${params.orderField}&orderAsc=${params.orderAsc}`
);
//浏览记录 //浏览记录
const queryViewRecord = (data) => ajaxs.post(`/statApi/xboe/m/stat/userdynamic/list?pageIndex=${data.pageIndex}&pageSize=${data.pageSize}&contentType=${data.contentType}&aid=${data.aid}&hidden=${data.hidden}&eventKey=${data.eventKey}`) const queryViewRecord = (data) =>
ajaxs.post(
`/statApi/xboe/m/stat/userdynamic/list?pageIndex=${data.pageIndex}&pageSize=${data.pageSize}&contentType=${data.contentType}&aid=${data.aid}&hidden=${data.hidden}&eventKey=${data.eventKey}`
);
//浏览量TOP榜单 //浏览量TOP榜单
const queryPopularityOfMajor = (params) => ajax.get(`/xboe/m/boe/cases/queryPopularityOfMajor?pageSize=${params.pageSize}&rankMonth=${params.rankMonth}&majorId=${params.majorId}`) const queryPopularityOfMajor = (params) =>
ajax.get(
`/xboe/m/boe/cases/queryPopularityOfMajor?pageSize=${params.pageSize}&rankMonth=${params.rankMonth}&majorId=${params.majorId}`
);
//浏览记录新 //浏览记录新
const browsingHistory = (params) => ajax.get(`/xboe/m/boe/cases/browsingHistory?pageIndex=${params.pageIndex}&pageSize=5`) const browsingHistory = (params) =>
ajax.get(
`/xboe/m/boe/cases/browsingHistory?pageIndex=${params.pageIndex}&pageSize=5`
);
//获取所有的专业月份 //获取所有的专业月份
const queryAllTimePopularityOfMajor = () => ajax.get('/xboe/m/boe/cases/queryAllTimePopularityOfMajor') const queryAllTimePopularityOfMajor = () =>
ajax.get("/xboe/m/boe/cases/queryAllTimePopularityOfMajor");
// 获取会话ID
const initChat = () => ajax.get("/xboe/m/boe/case/ai/initChat");
export default { export default {
queryAllTimePopularityOfMajor, queryAllTimePopularityOfMajor,
getQueryRecommendRank, getQueryRecommendRank,
@@ -249,5 +279,6 @@ export default {
exports, exports,
startReadTimer, startReadTimer,
endReadTimer, endReadTimer,
caseYears caseYears,
} initChat,
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

File diff suppressed because it is too large Load Diff

View File

@@ -1,363 +1,621 @@
<template> <template>
<div class="input-area"> <div class="input-area">
<el-input v-model="inputContent" type="textarea" class="input-placeholder" placeholder="有问题,尽管问" <el-input
@keyup.enter.native.prevent="handleSend" :disabled="disabled" :autosize="{ minRows: 2, maxRows: 4 }" v-model="inputContent"
resize="none"></el-input> type="textarea"
class="input-placeholder"
placeholder="有问题,尽管问"
@keyup.enter.native.prevent="handleSend"
:disabled="disabled"
:autosize="{ minRows: 2, maxRows: 4 }"
resize="none"
></el-input>
<div class="action-buttons"> <div class="action-buttons">
<el-button type="primary" size="small" class="start-btn" @click="handleNewConversation"> <el-button
type="primary"
size="small"
class="start-btn"
@click="handleNewConversation"
>
+ 开启新对话 + 开启新对话
</el-button> </el-button>
<el-button type="text" class="send-btn" @click="handleSend" :disabled="disabled"> <el-button
type="text"
class="send-btn"
@click="handleSend"
v-if="sendShow"
>
<i class="el-icon-s-promotion"></i> <i class="el-icon-s-promotion"></i>
</el-button> </el-button>
<el-button
type="text"
class="send-btn"
@click="handleStop"
v-if="stopShow"
>
<img class="stop" src="@/assets/images/case/stop.png" alt="" />
</el-button>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import { aiChat } from '@/api/boe/aiChat.js' import { aiChat } from "@/api/boe/aiChat.js";
import apiCase from "@/api/modules/cases.js";
export default { export default {
name: 'SendMessage', name: "SendMessage",
props: { props: {
value: { value: {
type: String, type: String,
default: '' default: "",
}, },
disabled: { disabled: {
type: Boolean, type: Boolean,
default: false default: false,
},
sendShow: {
type: Boolean,
default: true,
},
stopShow: {
type: Boolean,
default: false,
}, },
messageList: { messageList: {
type: Array, type: Array,
default: () => [] default: () => [],
}, },
suggestions: { suggestions: {
type: Array, type: Array,
default: () => [] default: () => [],
} },
conversationId: {
type: String,
default: "",
},
}, },
data() { data() {
return { return {
inputContent: this.value, inputContent: this.value,
conversationId: '' // 会话ID currentAbortController: null, // 添加abort controller引用
} isStopped: false, // 添加停止状态
typingTimer: null, // 添加这一行
thinkTypingTimer: null, // 添加这一行
};
}, },
watch: { watch: {
value(newVal) { value(newVal) {
this.inputContent = newVal this.inputContent = newVal;
} },
}, },
mounted() {
this.getConversationId();
},
methods: { methods: {
getConversationId() {
apiCase.initChat().then((res) => {
if (res.status == 200) {
const { result } = res;
// console.log(result);
this.conversationId = result;
sessionStorage.setItem("conversationId", this.conversationId);
}
});
},
handleSend(event) { handleSend(event) {
// 阻止事件的默认行为和冒泡 // 阻止事件的默认行为和冒泡
if (event) { if (event) {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
console.log('preventDefault'); console.log("preventDefault");
} }
console.log('handleSend'); console.log("handleSend");
if (!this.inputContent.trim() || this.disabled) return if (!this.inputContent.trim() || this.disabled) return;
// 重置停止状态,确保可以正常发送新消息
this.isStopped = false;
// 添加用户消息到列表 // 添加用户消息到列表
const userMessage = { const userMessage = {
isBot: false, isBot: false,
text: this.inputContent text: this.inputContent,
}; };
this.messageList.push(userMessage); this.messageList.push(userMessage);
// 显示加载状态 // 显示加载状态
this.$emit('loading', true); this.$emit("loading", true);
// 调用AI聊天接口 (暂时注释掉SSE使用模拟数据) // 调用AI聊天接口 (暂时注释掉SSE使用模拟数据)
this.callAIChat(this.inputContent); this.callAIChat(this.inputContent);
// 清空输入框 // 清空输入框
this.inputContent = '' this.inputContent = "";
}, },
handleStop() {
console.log(this.accumulatedContent, "accumulatedContent");
// 设置停止状态
this.isStopped = true;
// 清除打字机定时器
if (this.typingTimer) {
clearInterval(this.typingTimer);
this.typingTimer = null;
}
if (this.thinkTypingTimer) {
clearInterval(this.thinkTypingTimer);
this.thinkTypingTimer = null;
}
if (this.conversationId) {
// const stopAbortController = new AbortController();
fetch(
`/systemapi/xboe/m/boe/case/ai/stop?conversationId=${this.conversationId}`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
},
// signal: stopAbortController.signal,
}
)
.then((response) => {
if (response.ok) {
console.log("成功发送停止请求"); // 通知父组件需要重新获取会话ID
this.$emit("need-new-conversation-id");
} else {
console.error("停止请求失败:", response.status);
}
})
.catch((error) => {
console.error("停止请求出错:", error);
});
}
// 终止当前正在进行的聊天请求
if (this.currentAbortController) {
this.currentAbortController.abort();
}
// 立即发送加载完成事件,停止按钮状态切换
this.$emit("loading", false);
// 发送按钮状态更新事件
this.$emit("update-button-state", {
sendShow: true,
stopShow: false,
});
},
// 真实的SSE实现暂时注释掉 // 真实的SSE实现暂时注释掉
callAIChat(question) { callAIChat(question) {
// 重置停止状态
this.isStopped = false;
// 开始请求时显示加载状态
this.$emit("loading", true);
// 创建AbortController实例
this.currentAbortController = new AbortController();
// 创建新的AI消息对象 // 创建新的AI消息对象
const aiMessage = { const aiMessage = {
docId: '', docId: "",
isBot: true, isBot: true,
text: '', text: "",
status: null, status: null,
thinkText: '', thinkText: "",
caseRefers: [], // 添加caseRefers字段 caseRefers: [], // 添加caseRefers字段
textCompleted: false // 添加文字处理完成状态默认为false textCompleted: false, // 添加文字处理完成状态默认为false
}; };
this.messageList.push(aiMessage); this.messageList.push(aiMessage);
let hasFinished = false; // 新增
// 构造请求参数 // 构造请求参数
const requestData = { const requestData = {
conversationId: this.conversationId, conversationId: this.conversationId,
query: question query: question,
}; };
// 创建POST请求 // 创建POST请求
fetch('/systemapi/xboe/m/boe/case/ai/chat', { fetch("/systemapi/xboe/m/boe/case/ai/chat", {
method: 'POST', method: "POST",
headers: { headers: {
'Content-Type': 'application/json', "Content-Type": "application/json",
"accept": "text/event-stream", accept: "text/event-stream",
}, },
body: JSON.stringify(requestData) body: JSON.stringify(requestData),
}).then(r => { signal: this.currentAbortController.signal, // 添加signal
return r })
}).then(response => { .then((r) => {
// 处理流式响应 return r;
const reader = response.body.getReader(); })
const decoder = new TextDecoder('utf-8'); .then((response) => {
let buffer = ''; // 处理流式响应
let accumulatedContent = ''; // 累积的内容用于打字机效果 const reader = response.body.getReader();
let accumulatedThinkContent = ''; // 累积的思考内容 const decoder = new TextDecoder("utf-8");
let inThinkSection = false; // 是否在思考部分 let buffer = "";
let typingTimer = null; // 打字机定时器 let accumulatedContent = ""; // 累积的内容用于打字机效果
let thinkTypingTimer = null; // 思考内容打字机定时器 let accumulatedThinkContent = ""; // 累积的思考内容
let inThinkSection = false; // 是否在思考部分
// 逐字显示文本的函数 let typingTimer = null; // 打字机定时器
const typeText = (message, fullContent) => { let thinkTypingTimer = null; // 思考内容打字机定时器
// 如果已有定时器在运行,先清除它 // 逐字显示文本的函数
if (typingTimer) { const typeText = (message, fullContent) => {
clearInterval(typingTimer); // 如果已经停止,则直接显示全部内容
} if (this.isStopped) {
message.text = fullContent;
// 获取当前已显示的文本长度 this.$emit("update-message", message);
const currentLength = message.text.length;
// 获取完整文本
const targetLength = fullContent.length;
// 如果已经显示完整文本,不需要继续
if (currentLength >= targetLength) {
return;
}
const typingSpeed = 30; // 每个字符的间隔时间(毫秒)
typingTimer = setInterval(() => {
// 计算下一个要显示的字符索引
const nextIndex = message.text.length + 1;
if (nextIndex <= targetLength) {
message.text = fullContent.substring(0, nextIndex);
this.$emit('update-message', message);
} else {
clearInterval(typingTimer);
typingTimer = null;
// 当打字机效果完成时检查是否应该设置textCompleted为true
// 这应该在status 4交互完成时才设置
if (message.status === 4) {
if (nextIndex >= targetLength) {
message.textCompleted = true;
}
}
}
}, typingSpeed);
};
// 逐字显示思考内容的函数
const typeThinkText = (message, fullThinkContent) => {
// 如果已有定时器在运行,先清除它
if (thinkTypingTimer) {
clearInterval(thinkTypingTimer);
}
// 获取当前已显示的文本长度
const currentLength = message.thinkText.length;
// 获取完整文本
const targetLength = fullThinkContent.length;
// 如果已经显示完整文本,不需要继续
if (currentLength >= targetLength) {
return;
}
// 从当前显示位置开始继续显示(避免清空重显)
const startIndex = currentLength;
const typingSpeed = 20; // 每个字符的间隔时间(毫秒)
thinkTypingTimer = setInterval(() => {
// 计算下一个要显示的字符索引
const nextIndex = message.thinkText.length + 1;
if (nextIndex <= targetLength) {
message.thinkText = fullThinkContent.substring(0, nextIndex);
this.$emit('update-message', message);
} else {
clearInterval(thinkTypingTimer);
thinkTypingTimer = null;
}
}, typingSpeed);
};
// 添加一个检查是否所有文本都已完成显示的函数
const isTextDisplayCompleted = (message, fullContent) => {
return message.text.length >= fullContent.length;
};
// 读取流数据
const read = () => {
reader.read().then(({ done, value }) => {
if (done) {
// 当流结束时,等待打字机效果完成
const waitForTyping = () => {
if (!typingTimer) {
this.$emit('loading', false);
} else {
setTimeout(waitForTyping, 100);
}
};
waitForTyping();
return; return;
} }
// 解码数据 // 如果已有定时器在运行,先清除它
buffer += decoder.decode(value, { stream: true });
// 按行分割数据
const lines = buffer.split('\n');
buffer = lines.pop(); // 保留不完整的行
for (const line of lines) {
if (line.startsWith('data:')) {
try {
const jsonData = JSON.parse(line.substring(5));
// 根据status处理不同类型的数据
switch (jsonData.status) {
case 0: // 引用文件
// 处理引用文件信息
if (jsonData.fileRefer && jsonData.fileRefer.caseRefers) {
aiMessage.caseRefers = jsonData.fileRefer.caseRefers;
// 更新父组件的messageList
this.$emit('update-message', aiMessage);
}
// 从响应中获取并保存conversationId
if (jsonData.conversationId) {
this.conversationId = jsonData.conversationId;
sessionStorage.setItem('conversationId', jsonData.conversationId);
}
break;
case 1: // 流式对话内容
// 处理
const content = jsonData.content;
aiMessage.hasThink = false;
if (content.startsWith('<think>')) {
aiMessage.hasThink = true
inThinkSection = true;
accumulatedThinkContent = content.replace('<think>', '');
// 使用打字机效果显示think内容
typeThinkText(aiMessage, accumulatedThinkContent);
} else if (content.startsWith('</think>')) {
inThinkSection = false;
accumulatedThinkContent += content.replace('</think>', '');
// 使用打字机效果显示think内容
typeThinkText(aiMessage, accumulatedThinkContent);
} else if (inThinkSection) {
accumulatedThinkContent += content;
// 使用打字机效果显示think内容
typeThinkText(aiMessage, accumulatedThinkContent);
} else {
// 累积内容并使用打字机效果更新显示
accumulatedContent += content;
// 如果thinkText已经显示完整则继续使用打字机效果显示内容
if (aiMessage.hasThink) {
if (aiMessage.thinkText.length >= accumulatedThinkContent.length) {
typeText(aiMessage, accumulatedContent);
}
} else {
typeText(aiMessage, accumulatedContent);
}
}
// 不在这里直接更新,让打字机效果处理更新
break;
case 2: // 回答完成
// 不再在这里设置textCompleted状态
// 更新父组件的messageList
this.$emit('update-message', aiMessage);
// 从响应中获取并保存conversationId
break;
case 3: // 返回建议
// 这里可以处理建议问题
this.$emit('update-suggestions', jsonData.suggestions);
break;
case 4: // 交互完成
aiMessage.status = 4
// 从响应中获取并保存conversationId
this.$emit('loading', false);
// 检查文本是否已经完全显示如果是则设置textCompleted为true
if (isTextDisplayCompleted(aiMessage, accumulatedContent)) {
// aiMessage.textCompleted = true;
this.$emit('update-message', aiMessage);
}
break;
case 9:
if (jsonData.docId) {
aiMessage.docId = jsonData.docId
this.$emit('update-message', aiMessage);
}
console.log(jsonData)
break
default:
if (jsonData.docId) {
aiMessage.docId = jsonData.docId
this.$emit('update-message', aiMessage);
}
break
}
} catch (error) {
console.error('解析SSE数据错误:', error);
}
}
}
// 继续读取
read();
}).catch(error => {
console.error('SSE连接错误:', error);
// 出错时也设置文字处理完成状态
if (typingTimer) { if (typingTimer) {
clearInterval(typingTimer); clearInterval(typingTimer);
typingTimer = null;
} }
aiMessage.textCompleted = true;
this.$emit('loading', false);
aiMessage.text = '当前无法获取回答,请稍后重试';
// 更新父组件的messageList
this.$emit('update-message', aiMessage);
});
};
// 开始读取数据 // 获取当前已显示的文本长度
read(); const currentLength = message.text.length;
}).catch(error => { // 获取完整文本
console.error('请求失败:', error); const targetLength = fullContent.length;
// 出错时也设置文字处理完成状态
aiMessage.textCompleted = true; // 如果已经显示完整文本,不需要继续
this.$emit('loading', false); if (currentLength >= targetLength) {
aiMessage.text = '当前无法获取回答,请稍后重试'; return;
// 更新父组件的messageList }
this.$emit('update-message', aiMessage);
}); const typingSpeed = 30; // 每个字符的间隔时间(毫秒)
typingTimer = setInterval(() => {
// 如果已停止,立即停止渲染并保持当前内容
if (this.isStopped) {
clearInterval(typingTimer);
typingTimer = null;
// 保持当前已渲染的内容,不显示剩余内容
this.$emit("update-message", message);
return;
}
// 计算下一个要显示的字符索引
const nextIndex = message.text.length + 1;
if (nextIndex <= targetLength) {
message.text = fullContent.substring(0, nextIndex);
this.$emit("update-message", message);
} else {
clearInterval(typingTimer);
typingTimer = null;
// 当打字机效果完成时检查是否应该设置textCompleted为true
// 这应该在status 4交互完成时才设置
if (message.status === 4) {
if (nextIndex >= targetLength) {
message.textCompleted = true;
}
}
}
}, typingSpeed);
};
// 逐字显示思考内容的函数
const typeThinkText = (message, fullThinkContent) => {
// 如果已经停止,则直接显示当前已渲染的内容并清理定时器
if (this.isStopped) {
// 清理可能存在的定时器
if (thinkTypingTimer) {
clearInterval(thinkTypingTimer);
thinkTypingTimer = null;
}
// 保持当前已渲染的内容,不显示完整内容
this.$emit("update-message", message);
return;
}
// 如果已有定时器在运行,先清除它
if (thinkTypingTimer) {
clearInterval(thinkTypingTimer);
}
// 获取当前已显示的文本长度
const currentLength = message.thinkText.length;
// 获取完整文本
const targetLength = fullThinkContent.length;
// 如果已经显示完整文本,不需要继续
if (currentLength >= targetLength) {
return;
}
// 从当前显示位置开始继续显示(避免清空重显)
const startIndex = currentLength;
const typingSpeed = 20; // 每个字符的间隔时间(毫秒)
thinkTypingTimer = setInterval(() => {
// 如果已停止,立即停止渲染并保持当前内容
if (this.isStopped) {
clearInterval(thinkTypingTimer);
thinkTypingTimer = null;
// 保持当前已渲染的内容,不显示剩余内容
this.$emit("update-message", message);
return;
}
// 计算下一个要显示的字符索引
const nextIndex = message.thinkText.length + 1;
if (nextIndex <= targetLength) {
message.thinkText = fullThinkContent.substring(0, nextIndex);
this.$emit("update-message", message);
} else {
clearInterval(thinkTypingTimer);
thinkTypingTimer = null;
}
}, typingSpeed);
};
// 添加一个检查是否所有文本都已完成显示的函数
const isTextDisplayCompleted = (message, fullContent) => {
return message.text.length >= fullContent.length;
};
// 读取流数据
const read = () => {
// 如果已停止,直接返回
if (this.isStopped || hasFinished) {
this.$emit("loading", false);
return;
}
reader
.read()
.then(({ done, value }) => {
// 如果已停止,直接返回
if (this.isStopped || hasFinished) {
this.$emit("loading", false);
return;
}
if (done) {
// 当流结束时,等待打字机效果完成
const waitForTyping = () => {
if (!typingTimer) {
this.$emit("loading", false);
} else {
setTimeout(waitForTyping, 100);
}
};
waitForTyping();
return;
}
// 解码数据
buffer += decoder.decode(value, { stream: true });
// 按行分割数据
const lines = buffer.split("\n");
buffer = lines.pop(); // 保留不完整的行
for (const line of lines) {
// 如果已停止,跳出循环
if (this.isStopped) {
this.$emit("loading", false);
return;
}
if (line.startsWith("data:")) {
try {
const jsonData = JSON.parse(line.substring(5));
// 根据status处理不同类型的数据
switch (jsonData.status) {
case 0: // 引用文件
// 处理引用文件信息
if (
jsonData.fileRefer &&
jsonData.fileRefer.caseRefers
) {
aiMessage.caseRefers =
jsonData.fileRefer.caseRefers;
// 更新父组件的messageList
this.$emit("update-message", aiMessage);
}
// 从响应中获取并保存conversationId
// if (jsonData.conversationId) {
// this.conversationId = jsonData.conversationId;
// sessionStorage.setItem(
// "conversationId",
// jsonData.conversationId
// );
// }
break;
case 1: // 流式对话内容
if (!this.isStopped) {
// 处理
const content = jsonData.content;
aiMessage.hasThink = false;
if (content.startsWith("<think>")) {
aiMessage.hasThink = true;
inThinkSection = true;
accumulatedThinkContent = content.replace(
"<think>",
""
);
// 使用打字机效果显示think内容
typeThinkText(aiMessage, accumulatedThinkContent);
} else if (content.startsWith("</think>")) {
inThinkSection = false;
accumulatedThinkContent += content.replace(
"</think>",
""
);
// 使用打字机效果显示think内容
typeThinkText(aiMessage, accumulatedThinkContent);
} else if (inThinkSection) {
accumulatedThinkContent += content;
// 使用打字机效果显示think内容
typeThinkText(aiMessage, accumulatedThinkContent);
} else {
// 累积内容并使用打字机效果更新显示
accumulatedContent += content;
// 如果thinkText已经显示完整则继续使用打字机效果显示内容
if (aiMessage.hasThink) {
if (
aiMessage.thinkText.length >=
accumulatedThinkContent.length
) {
typeText(aiMessage, accumulatedContent);
}
} else {
typeText(aiMessage, accumulatedContent);
}
}
// 发送事件更新按钮状态:显示停止按钮
this.$emit("update-button-state", {
sendShow: false,
stopShow: true,
});
}
// 不在这里直接更新,让打字机效果处理更新
break;
case 2: // 回答完成
// 不再在这里设置textCompleted状态
// 更新父组件的messageList
// 只有在未停止状态下才更新
if (!this.isStopped) {
// 更新父组件的messageList
this.$emit("update-message", aiMessage);
// 发送事件更新按钮状态:显示停止按钮
this.$emit("update-button-state", {
sendShow: false,
stopShow: true,
});
}
// 从响应中获取并保存conversationId
break;
case 3: // 返回建议
// 只有在未停止状态下才处理建议
if (!this.isStopped) {
// 这里可以处理建议问题
this.$emit(
"update-suggestions",
jsonData.suggestions
);
// 发送事件更新按钮状态:显示停止按钮
this.$emit("update-button-state", {
sendShow: false,
stopShow: true,
});
}
break;
case 4: // 交互完成
// 只有在未停止状态下才处理完成状态
if (!this.isStopped) {
aiMessage.status = 4;
// 从响应中获取并保存conversationId
// this.$emit("loading", false);
// 检查文本是否已经完全显示如果是则设置textCompleted为true
if (
isTextDisplayCompleted(
aiMessage,
accumulatedContent
)
) {
// aiMessage.textCompleted = true;
this.$emit("update-message", aiMessage);
// 发送事件更新按钮状态:显示发送按钮
hasFinished = true; // 标记完成
this.$emit("update-button-state", {
sendShow: true,
stopShow: false,
});
}
}
break;
case 9:
// 只有在未停止状态下才处理docId
if (!this.isStopped && jsonData.docId) {
aiMessage.docId = jsonData.docId;
this.$emit("update-message", aiMessage);
}
// 显示发送按钮,隐藏停止按钮
// this.$emit("loading", false);
console.log(jsonData);
// 发送事件更新按钮状态:显示发送按钮
hasFinished = true; // 标记完成
this.$emit("update-button-state", {
sendShow: true,
stopShow: false,
});
break;
default:
if (!this.isStopped && jsonData.docId) {
aiMessage.docId = jsonData.docId;
this.$emit("update-message", aiMessage);
}
break;
}
} catch (error) {
console.error("解析SSE数据错误:", error);
}
}
}
// 继续读取(除非已停止)
if (!this.isStopped) {
read();
}
// 继续读取
// read();
})
.catch((error) => {
// 处理AbortError
if (error.name === "AbortError") {
console.log("请求已被用户取消");
// aiMessage.text = "请求已被取消";
aiMessage.textCompleted = true;
this.$emit("update-message", aiMessage);
} else {
console.error("SSE连接错误:", error);
// 出错时也设置文字处理完成状态
if (typingTimer) {
clearInterval(typingTimer);
typingTimer = null;
}
aiMessage.textCompleted = true;
aiMessage.text = "当前无法获取回答,请稍后重试";
// 更新父组件的messageList
this.$emit("update-message", aiMessage);
}
this.$emit("loading", false);
});
};
// 继续读取(除非已停止)
if (!this.isStopped) {
read();
}
})
.catch((error) => {
// 处理AbortError
if (error.name === "AbortError") {
console.log("请求已被用户取消");
aiMessage.text = "请求已被取消";
aiMessage.textCompleted = true;
this.$emit("update-message", aiMessage);
} else {
console.error("请求失败:", error);
// 出错时也设置文字处理完成状态
aiMessage.textCompleted = true;
aiMessage.text = "当前无法获取回答,请稍后重试";
// 更新父组件的messageList
this.$emit("update-message", aiMessage);
}
// 只有在非停止状态下才发送 loading 事件
if (!this.isStopped) {
this.$emit("loading", false);
}
});
}, },
handleNewConversation() { handleNewConversation() {
this.conversationId = '' // this.conversationId = "";
this.$emit('new-conversation') this.$emit("new-conversation");
} this.getConversationId();
} },
} },
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.input-area { .input-area {
background-color: white; background-color: white;
@@ -405,5 +663,10 @@ export default {
} }
} }
} }
.stop {
width: 18px;
height: 18px;
display: block;
}
} }
</style> </style>