feat(聊天): 重构聊天功能,使用Vuex管理消息状态

- 将聊天消息和用户信息移至Vuex集中管理
- 实现打开或创建会话的统一入口方法openOrCreateDialogue
- 更新消息列表获取和发送消息的逻辑
- 添加持久化存储支持保存聊天用户信息
This commit is contained in:
yangzhe 2025-12-09 16:11:45 +08:00
parent 9638bd10a1
commit 60dff46ffe
3 changed files with 192 additions and 92 deletions

View File

@ -2,7 +2,7 @@
<view class="chat-page"> <view class="chat-page">
<!-- 顶部导航 --> <!-- 顶部导航 -->
<header-bar <header-bar
:title="userName" :title="vuex_msgUser.receiverName"
leftIcon="arrow-left" leftIcon="arrow-left"
@leftClick="handleLeftClick" @leftClick="handleLeftClick"
></header-bar> ></header-bar>
@ -16,12 +16,11 @@
scroll-with-animation scroll-with-animation
> >
<view <view
v-for="(message, index) in messageList" v-for="(message, index) in vuex_msgList"
:key="message.id" :key="message.id"
:id="'msg-' + message.id" :id="'msg-' + message.id"
> >
<!-- 时间 --> <!-- 时间 -->
<!-- v-if="message.timeLabel !== 0" -->
<view class="message-time" v-if="isShowTime(index)"> <view class="message-time" v-if="isShowTime(index)">
{{ formatShowTime(message.sendDate) }} {{ formatShowTime(message.sendDate) }}
</view> </view>
@ -98,10 +97,6 @@ export default {
return { return {
baseUrl: "", baseUrl: "",
//
userId: "",
userName: "",
// //
myAvatar: "/static/avatar/default-avatar.png", myAvatar: "/static/avatar/default-avatar.png",
otherAvatar: "/static/avatar/default-avatar.png", otherAvatar: "/static/avatar/default-avatar.png",
@ -116,7 +111,6 @@ export default {
isRead: false, isRead: false,
interactMode: 0, interactMode: 0,
messageType: 0, messageType: 0,
timeLabel: 1,
}, },
{ {
id: "02306fc3-c821-4a23-ad66-0bd77d154105", id: "02306fc3-c821-4a23-ad66-0bd77d154105",
@ -127,8 +121,6 @@ export default {
isRead: false, isRead: false,
interactMode: 1, interactMode: 1,
messageType: 0, messageType: 0,
timeLabel: 0,
displayTime: "",
}, },
{ {
id: "9cac8661-bf09-4b63-ab15-1a0a36b91110", id: "9cac8661-bf09-4b63-ab15-1a0a36b91110",
@ -138,7 +130,6 @@ export default {
isRead: false, isRead: false,
interactMode: 0, interactMode: 0,
messageType: 0, messageType: 0,
timeLabel: 1,
}, },
{ {
id: "02306fc3-c821-4a23-ad66-0bd788854105", id: "02306fc3-c821-4a23-ad66-0bd788854105",
@ -148,8 +139,6 @@ export default {
isRead: false, isRead: false,
interactMode: 1, interactMode: 1,
messageType: 0, messageType: 0,
timeLabel: 0,
displayTime: "",
}, },
], ],
@ -158,21 +147,19 @@ export default {
// //
scrollToView: "", scrollToView: "",
PageIndex: 1,
PageSize: 20,
}; };
}, },
onLoad(options) { onLoad(options) {
console.log(this.vuex_msgList);
this.baseUrl = this.$u.http.config.baseUrl; this.baseUrl = this.$u.http.config.baseUrl;
//
this.userId = options.userId || "";
this.userName = options.name || "用户";
console.log("[聊天详情] 用户ID:", this.userId);
console.log("[聊天详情] 用户名:", this.userName);
// //
this.loadHistoryMessages(); this.getMsgList();
// //
this.$nextTick(() => { this.$nextTick(() => {
@ -196,6 +183,18 @@ export default {
uni.navigateBack(); uni.navigateBack();
}, },
// this.vuex_msgUser = {
// dialogueManagementId: "08de36ed-8bc0-4b79-86e7-0128010ccc4b",
// receiverId: "08de33d5-b517-4801-8474-4a4ad5642691",
// receiverName: "",
// receiverHeadSculptureUrl: "tx.jpg",
// title: "",
// startTime: "2025-12-09T14:38:21.295504",
// lastMessageTime: "0001-01-01T00:00:00",
// isOverhead: false,
// unReadCount: 0,
// };
// //
handleSend() { handleSend() {
if (!this.messageValue) { if (!this.messageValue) {
@ -204,10 +203,10 @@ export default {
// //
const message = { const message = {
dialogueManagementId: "3fa85f64-5717-4562-b3fc-2c963f66afa6", dialogueManagementId: this.vuex_msgUser.dialogueManagementId,
receiverId: "3fa85f64-5717-4562-b3fc-2c963f66afa6", receiverId: this.vuex_msgUser.receiverId,
messageType: 0,
message: this.messageValue, message: this.messageValue,
messageType: 0,
filePath: "", filePath: "",
ip: "", ip: "",
}; };
@ -217,16 +216,30 @@ export default {
// //
sendMsgFn(message) { sendMsgFn(message) {
SendMessage_PrivateApi(message) this.$u.api
.SendMessage_PrivateApi(message)
.then((res) => { .then((res) => {
// console.log(res, "发送消息成功");
this.messageList.push(message); if (res.succeed) {
// //
this.messageValue = ""; const msgUserData = {
// dialogueManagementId: this.vuex_msgUser.dialogueManagementId,
this.$nextTick(() => { senderId: this.vuex_user.Id,
this.scrollToBottom(); receiverId: this.vuex_msgUser.receiverId,
}); sendDate: new Date().toISOString(),
message: this.messageValue,
messageType: 0,
filePath: "",
};
this.$store.commit("push_Msg", msgUserData);
//
this.messageValue = "";
//
this.$nextTick(() => {
this.scrollToBottom();
});
}
}) })
.catch((error) => { .catch((error) => {
return msg.warning("发送失败"); return msg.warning("发送失败");
@ -249,22 +262,35 @@ export default {
}); });
}, },
// //
loadHistoryMessages() { getMsgList() {
// TODO: this.$u.api
// this.$u.api.getChatHistory({ .GetChatHistoryDataApi({
// userId: this.userId "Item1.DialogueManagementId": this.vuex_msgUser.dialogueManagementId,
// }).then(res => { PageIndex: this.PageIndex,
// this.messageList = res.data PageSize: this.PageSize,
// }) })
.then((res) => {
const msgList = res.data.item1.reverse();
// interactMode mock
console.log("[聊天详情] 加载历史消息"); for (var i = 0; i < msgList.length; i++) {
msgList[i].interactMode = msgList[i].interactMode || 0;
// if (this.PageIndex != 1) {
// this.$store.commit("unshift_Msg", msgList[i]);
// } else {
// this.$store.commit("push_Msg", msgList[i]);
// }
}
this.$store.commit("push_MsgList", msgList);
});
}, },
// //
scrollToBottom() { scrollToBottom() {
if (this.messageList.length > 0) { if (this.vuex_msgList.length > 0) {
const lastMsg = this.messageList[this.messageList.length - 1]; const lastMsg = this.vuex_msgList[this.vuex_msgList.length - 1];
this.scrollToView = "msg-" + lastMsg.id; this.scrollToView = "msg-" + lastMsg.id;
} }
}, },
@ -281,8 +307,8 @@ export default {
if (index == 0) { if (index == 0) {
return true; return true;
} }
let isTime = new Date(this.messageList[index].sendDate).getTime(); // let isTime = new Date(this.vuex_msgList[index].sendDate).getTime(); //
const time = new Date(this.messageList[index - 1].sendDate).getTime(); // const time = new Date(this.vuex_msgList[index - 1].sendDate).getTime(); //
// 30 // 30
return isTime - time > 30 * 60 * 1000; return isTime - time > 30 * 60 * 1000;
}, },

View File

@ -104,10 +104,18 @@ export default {
uni.navigateBack(); uni.navigateBack();
}, },
handleAskQuestion(teacher) { handleAskQuestion(teacher) {
// console.log("点击咨询:", teacher);
uni.navigateTo({
url: `/pages/home/teacherInfo/index?teacherId=${teacher.id}`, var msgUserData = {
}); avatar: teacher.headSculptureUrl,
friendId: teacher.id,
friendName: teacher.name,
};
//
this.$store.dispatch("openOrCreateDialogue", msgUserData);
return;
return; return;
// //
@ -118,6 +126,7 @@ export default {
} else { } else {
} }
}, },
// () // ()
handleMessageSubmit(data) { handleMessageSubmit(data) {
console.log("提交留言:", data.content, "教师:", data.teacher?.name); console.log("提交留言:", data.content, "教师:", data.teacher?.name);

View File

@ -11,7 +11,12 @@ try {
} catch (e) {} } catch (e) {}
// 需要永久存储且下次APP启动需要取出的在state中的变量名 // 需要永久存储且下次APP启动需要取出的在state中的变量名
let saveStateKeys = ["vuex_user", "vuex_token", "vuex_userType"]; let saveStateKeys = [
"vuex_user",
"vuex_token",
"vuex_userType",
"vuex_msgUser",
];
// 保存变量到本地存储中 // 保存变量到本地存储中
const saveLifeData = function (key, value) { const saveLifeData = function (key, value) {
@ -45,7 +50,7 @@ const store = new Vuex.Store({
// 本地占位的聊天用户(未在服务端建立的会话,临时展示) // 本地占位的聊天用户(未在服务端建立的会话,临时展示)
vuex_localMsgUserList: [], vuex_localMsgUserList: [],
// 当前聊天用户对象 // 当前聊天用户对象
vuex_msgUser: null, vuex_msgUser: lifeData.vuex_msgUser ? lifeData.vuex_msgUser : {},
// 消息窗口滚动位置 // 消息窗口滚动位置
vuex_msgScrollTop: 0, vuex_msgScrollTop: 0,
// 自定义tabbar数据 // 自定义tabbar数据
@ -118,7 +123,7 @@ const store = new Vuex.Store({
// ===== 消息中心:列表、当前聊天、消息推送等 ===== // ===== 消息中心:列表、当前聊天、消息推送等 =====
// 设置会话列表(服务端返回) // 设置会话列表(服务端返回)
setUserMsgList(state, list) { set_UserMsgList(state, list) {
state.vuex_userMsgList = Array.isArray(list) ? list.slice() : []; state.vuex_userMsgList = Array.isArray(list) ? list.slice() : [];
if (state.vuex_msgUser && state.vuex_msgUser.friendId) { if (state.vuex_msgUser && state.vuex_msgUser.friendId) {
state.vuex_userMsgList = state.vuex_userMsgList.map((item) => { state.vuex_userMsgList = state.vuex_userMsgList.map((item) => {
@ -130,7 +135,7 @@ const store = new Vuex.Store({
} }
}, },
// 插入一个本地占位的聊天用户 // 插入一个本地占位的聊天用户
addLocalMsgUser(state, user) { add_LocalMsgUser(state, user) {
const existsLocal = (state.vuex_localMsgUserList || []).some( const existsLocal = (state.vuex_localMsgUserList || []).some(
(v) => v && v.friendId === user.friendId (v) => v && v.friendId === user.friendId
); );
@ -156,7 +161,7 @@ const store = new Vuex.Store({
}); });
}, },
// 设置当前聊天用户 // 设置当前聊天用户
setMsgUser(state, user) { set_MsgUser(state, user) {
state.vuex_msgUser = user || null; state.vuex_msgUser = user || null;
if (state.vuex_msgUser && state.vuex_msgUser.friendId) { if (state.vuex_msgUser && state.vuex_msgUser.friendId) {
state.vuex_userMsgList = (state.vuex_userMsgList || []).map((item) => { state.vuex_userMsgList = (state.vuex_userMsgList || []).map((item) => {
@ -167,23 +172,28 @@ const store = new Vuex.Store({
}); });
} }
}, },
// 现在没有id先覆盖整个list
push_MsgList(state, list) {
state.vuex_msgList = list || [];
},
// 推送一条新消息(去重) // 推送一条新消息(去重)
pushMsg(state, msg) { push_Msg(state, msg) {
if (!msg || !msg.id) return; // 注现在的消息没有id暂时注释
const exists = (state.vuex_msgList || []).some( // if (!msg || !msg.id) return;
(item) => item && item.id === msg.id // const exists = (state.vuex_msgList || []).some(
); // (item) => item && item.id === msg.id
if (!exists) { // );
state.vuex_msgList.push(msg); // if (!exists) {
} state.vuex_msgList.push(msg);
// }
}, },
// 插入历史消息到头部 // 插入历史消息到头部
unshiftMsg(state, msg) { unshift_Msg(state, msg) {
if (!msg) return; if (!msg) return;
state.vuex_msgList.unshift(msg); state.vuex_msgList.unshift(msg);
}, },
// 更新置顶状态(本地) // 更新置顶状态(本地)
updateTopState(state, { friendId, isTop }) { update_TopState(state, { friendId, isTop }) {
state.vuex_userMsgList = (state.vuex_userMsgList || []).map((item) => { state.vuex_userMsgList = (state.vuex_userMsgList || []).map((item) => {
if (item && item.friendId === friendId) { if (item && item.friendId === friendId) {
return { ...item, isTop: !!isTop }; return { ...item, isTop: !!isTop };
@ -192,11 +202,11 @@ const store = new Vuex.Store({
}); });
}, },
// 更新消息窗口滚动位置 // 更新消息窗口滚动位置
setMsgScrollTop(state, top) { set_MsgScrollTop(state, top) {
state.vuex_msgScrollTop = Number(top) || 0; state.vuex_msgScrollTop = Number(top) || 0;
}, },
// 清空消息相关状态(登出/切换账号) // 清空消息相关状态(登出/切换账号)
clearMessageState(state) { clear_MessageState(state) {
state.vuex_msgList = []; state.vuex_msgList = [];
state.vuex_userMsgList = []; state.vuex_userMsgList = [];
state.vuex_localMsgUserList = []; state.vuex_localMsgUserList = [];
@ -220,36 +230,39 @@ const store = new Vuex.Store({
saveLifeData("vuex_token", state.vuex_token); saveLifeData("vuex_token", state.vuex_token);
saveLifeData("vuex_teacherInfo", state.vuex_teacherInfo); saveLifeData("vuex_teacherInfo", state.vuex_teacherInfo);
saveLifeData("vuex_user", state.vuex_user); saveLifeData("vuex_user", state.vuex_user);
saveLifeData("vuex_msgUser", state.vuex_msgUser);
}, },
}, },
actions: { actions: {
// 获取聊天列表(最近联系人) // 获取聊天列表(最近联系人)
async getUserlist({ commit, state }) { async getUserlist({ commit, state }) {
this.$u.api.GetDialogueListApi().then((res) => { // 在 Vuex action 中没有组件 this上下文不包含 $u
const baseUrl = // 通过 Vue.prototype.$u 使用 uView 的 http 实例
(Vue.prototype.$u && return Vue.prototype.$u.api.GetDialogueListApi().then((res) => {
Vue.prototype.$u.http && const list = (res && res.data.item1 ? res.data.item1 : []).map(
Vue.prototype.$u.http.config && (item) => {
Vue.prototype.$u.http.config.baseUrl) || const unReadCount =
""; state.vuex_msgUser &&
const list = (res && res.data ? res.data : []).map((item) => { state.vuex_msgUser.friendId === item.friendId
const unReadCount = ? 0
state.vuex_msgUser && state.vuex_msgUser.friendId === item.friendId : item.unReadCount || 0;
? 0 return {
: item.unReadCount || 0; ...item,
return { // avatar: item.avatar,
...item, unReadCount,
avatar: baseUrl ? baseUrl + item.avatar : item.avatar, };
unReadCount, }
}; );
}); commit("set_UserMsgList", list);
commit("setUserMsgList", list); return list;
}); });
}, },
// 设置当前聊天用户并入队本地占位 // 设置当前聊天用户并入队本地占位
setMsgUser({ commit, dispatch }, user) { setMsgUser({ commit, dispatch }, user) {
commit("setMsgUser", user); console.log("setMsgUser执行了", user);
commit("addLocalMsgUser", user);
commit("set_MsgUser", user);
commit("add_LocalMsgUser", user);
// 刷新列表,清零未读 // 刷新列表,清零未读
dispatch("getUserlist"); dispatch("getUserlist");
}, },
@ -260,10 +273,10 @@ const store = new Vuex.Store({
) { ) {
const params = { userId, friendId, PageIndex, PageSize }; const params = { userId, friendId, PageIndex, PageSize };
this.$u.api.GetChatHistoryDataApi(params).then((res) => { Vue.prototype.$u.api.GetChatHistoryDataApi(params).then((res) => {
const list = res && res.data ? res.data : []; const list = res && res.data ? res.data : [];
// 将最新消息插入到消息列表尾部(或头部) // 将最新消息插入到消息列表尾部(或头部)
list.forEach((msg) => commit("pushMsg", msg)); list.forEach((msg) => commit("push_Msg", msg));
}); });
}, },
// 设置消息置顶(服务端 + 本地) // 设置消息置顶(服务端 + 本地)
@ -272,14 +285,66 @@ const store = new Vuex.Store({
if (user.isTop) { if (user.isTop) {
return; return;
} else { } else {
this.$u.api Vue.prototype.$u.api
.OverheadOneDialogueApi({ dialogueManagementId: user.friendId }) .OverheadOneDialogueApi({ dialogueManagementId: user.friendId })
.then((res) => { .then((res) => {
commit("updateTopState", { friendId: user.friendId, isTop: true }); commit("update_TopState", { friendId: user.friendId, isTop: true });
dispatch("getUserlist"); dispatch("getUserlist");
}); });
} }
}, },
// 统一入口:打开或创建会话
// 使用位置:所有需要进入聊天的入口调用该方法,避免各页面重复逻辑
// 1) 获取最新会话列表
// 2) 查找是否存在 friendId 对应的会话
// 3) 有则设置当前会话并(可选)跳转;无则创建,成功后递归重试
async openOrCreateDialogue({ commit, state, dispatch }, user) {
const { friendId, friendName, avatar, chatType = 0 } = user || {};
const attempt =
user && typeof user.attempt === "number" ? user.attempt : 1;
if (!friendId) return Promise.reject(new Error("缺少 friendId"));
// 第一步:获取列表(复用现有 action保证 state 同步更新)
const list = await dispatch("getUserlist");
// 第二步:查找是否存在该会话(兼容后端字段 friendId/receiverId
const target = (list || []).find(
(i) => i && (i.receiverId === friendId || i.friendId === friendId)
);
if (target) {
commit("set_MsgUser", target);
// 跳转到对话页,参数按需调整
uni.navigateTo({
url: `/pages/chat/index`,
});
return target;
}
// 第二次调用时,无论成功与否均在第三步(创建)之前结束
if (attempt >= 2) {
return false;
}
// 第三步:不存在则创建,成功后重试本流程
const res = await Vue.prototype.$u.api.AddDialogueApi({
receiverId: friendId,
onlineConsultationType: 0, // 0招生在线1迎新在线可按业务传入
});
if (res && res.succeed === true) {
// 创建成功后,重新执行流程(再次获取列表并进入会话)
return dispatch("openOrCreateDialogue", {
friendId,
friendName,
avatar,
chatType,
attempt: attempt + 1,
});
}
return Promise.reject(new Error(res?.error || "创建会话失败"));
},
}, },
}); });