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">
<!-- 顶部导航 -->
<header-bar
:title="userName"
:title="vuex_msgUser.receiverName"
leftIcon="arrow-left"
@leftClick="handleLeftClick"
></header-bar>
@ -16,12 +16,11 @@
scroll-with-animation
>
<view
v-for="(message, index) in messageList"
v-for="(message, index) in vuex_msgList"
:key="message.id"
:id="'msg-' + message.id"
>
<!-- 时间 -->
<!-- v-if="message.timeLabel !== 0" -->
<view class="message-time" v-if="isShowTime(index)">
{{ formatShowTime(message.sendDate) }}
</view>
@ -98,10 +97,6 @@ export default {
return {
baseUrl: "",
//
userId: "",
userName: "",
//
myAvatar: "/static/avatar/default-avatar.png",
otherAvatar: "/static/avatar/default-avatar.png",
@ -116,7 +111,6 @@ export default {
isRead: false,
interactMode: 0,
messageType: 0,
timeLabel: 1,
},
{
id: "02306fc3-c821-4a23-ad66-0bd77d154105",
@ -127,8 +121,6 @@ export default {
isRead: false,
interactMode: 1,
messageType: 0,
timeLabel: 0,
displayTime: "",
},
{
id: "9cac8661-bf09-4b63-ab15-1a0a36b91110",
@ -138,7 +130,6 @@ export default {
isRead: false,
interactMode: 0,
messageType: 0,
timeLabel: 1,
},
{
id: "02306fc3-c821-4a23-ad66-0bd788854105",
@ -148,8 +139,6 @@ export default {
isRead: false,
interactMode: 1,
messageType: 0,
timeLabel: 0,
displayTime: "",
},
],
@ -158,21 +147,19 @@ export default {
//
scrollToView: "",
PageIndex: 1,
PageSize: 20,
};
},
onLoad(options) {
console.log(this.vuex_msgList);
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(() => {
@ -196,6 +183,18 @@ export default {
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() {
if (!this.messageValue) {
@ -204,10 +203,10 @@ export default {
//
const message = {
dialogueManagementId: "3fa85f64-5717-4562-b3fc-2c963f66afa6",
receiverId: "3fa85f64-5717-4562-b3fc-2c963f66afa6",
messageType: 0,
dialogueManagementId: this.vuex_msgUser.dialogueManagementId,
receiverId: this.vuex_msgUser.receiverId,
message: this.messageValue,
messageType: 0,
filePath: "",
ip: "",
};
@ -217,16 +216,30 @@ export default {
//
sendMsgFn(message) {
SendMessage_PrivateApi(message)
this.$u.api
.SendMessage_PrivateApi(message)
.then((res) => {
//
this.messageList.push(message);
//
this.messageValue = "";
//
this.$nextTick(() => {
this.scrollToBottom();
});
console.log(res, "发送消息成功");
if (res.succeed) {
//
const msgUserData = {
dialogueManagementId: this.vuex_msgUser.dialogueManagementId,
senderId: this.vuex_user.Id,
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) => {
return msg.warning("发送失败");
@ -249,22 +262,35 @@ export default {
});
},
//
loadHistoryMessages() {
// TODO:
// this.$u.api.getChatHistory({
// userId: this.userId
// }).then(res => {
// this.messageList = res.data
// })
//
getMsgList() {
this.$u.api
.GetChatHistoryDataApi({
"Item1.DialogueManagementId": this.vuex_msgUser.dialogueManagementId,
PageIndex: this.PageIndex,
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() {
if (this.messageList.length > 0) {
const lastMsg = this.messageList[this.messageList.length - 1];
if (this.vuex_msgList.length > 0) {
const lastMsg = this.vuex_msgList[this.vuex_msgList.length - 1];
this.scrollToView = "msg-" + lastMsg.id;
}
},
@ -281,8 +307,8 @@ export default {
if (index == 0) {
return true;
}
let isTime = new Date(this.messageList[index].sendDate).getTime(); //
const time = new Date(this.messageList[index - 1].sendDate).getTime(); //
let isTime = new Date(this.vuex_msgList[index].sendDate).getTime(); //
const time = new Date(this.vuex_msgList[index - 1].sendDate).getTime(); //
// 30
return isTime - time > 30 * 60 * 1000;
},

View File

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

View File

@ -11,7 +11,12 @@ try {
} catch (e) {}
// 需要永久存储且下次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) {
@ -45,7 +50,7 @@ const store = new Vuex.Store({
// 本地占位的聊天用户(未在服务端建立的会话,临时展示)
vuex_localMsgUserList: [],
// 当前聊天用户对象
vuex_msgUser: null,
vuex_msgUser: lifeData.vuex_msgUser ? lifeData.vuex_msgUser : {},
// 消息窗口滚动位置
vuex_msgScrollTop: 0,
// 自定义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() : [];
if (state.vuex_msgUser && state.vuex_msgUser.friendId) {
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(
(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;
if (state.vuex_msgUser && state.vuex_msgUser.friendId) {
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) {
if (!msg || !msg.id) return;
const exists = (state.vuex_msgList || []).some(
(item) => item && item.id === msg.id
);
if (!exists) {
state.vuex_msgList.push(msg);
}
push_Msg(state, msg) {
// 注现在的消息没有id暂时注释
// if (!msg || !msg.id) return;
// const exists = (state.vuex_msgList || []).some(
// (item) => item && item.id === msg.id
// );
// if (!exists) {
state.vuex_msgList.push(msg);
// }
},
// 插入历史消息到头部
unshiftMsg(state, msg) {
unshift_Msg(state, msg) {
if (!msg) return;
state.vuex_msgList.unshift(msg);
},
// 更新置顶状态(本地)
updateTopState(state, { friendId, isTop }) {
update_TopState(state, { friendId, isTop }) {
state.vuex_userMsgList = (state.vuex_userMsgList || []).map((item) => {
if (item && item.friendId === friendId) {
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;
},
// 清空消息相关状态(登出/切换账号)
clearMessageState(state) {
clear_MessageState(state) {
state.vuex_msgList = [];
state.vuex_userMsgList = [];
state.vuex_localMsgUserList = [];
@ -220,36 +230,39 @@ const store = new Vuex.Store({
saveLifeData("vuex_token", state.vuex_token);
saveLifeData("vuex_teacherInfo", state.vuex_teacherInfo);
saveLifeData("vuex_user", state.vuex_user);
saveLifeData("vuex_msgUser", state.vuex_msgUser);
},
},
actions: {
// 获取聊天列表(最近联系人)
async getUserlist({ commit, state }) {
this.$u.api.GetDialogueListApi().then((res) => {
const baseUrl =
(Vue.prototype.$u &&
Vue.prototype.$u.http &&
Vue.prototype.$u.http.config &&
Vue.prototype.$u.http.config.baseUrl) ||
"";
const list = (res && res.data ? res.data : []).map((item) => {
const unReadCount =
state.vuex_msgUser && state.vuex_msgUser.friendId === item.friendId
? 0
: item.unReadCount || 0;
return {
...item,
avatar: baseUrl ? baseUrl + item.avatar : item.avatar,
unReadCount,
};
});
commit("setUserMsgList", list);
// 在 Vuex action 中没有组件 this上下文不包含 $u
// 通过 Vue.prototype.$u 使用 uView 的 http 实例
return Vue.prototype.$u.api.GetDialogueListApi().then((res) => {
const list = (res && res.data.item1 ? res.data.item1 : []).map(
(item) => {
const unReadCount =
state.vuex_msgUser &&
state.vuex_msgUser.friendId === item.friendId
? 0
: item.unReadCount || 0;
return {
...item,
// avatar: item.avatar,
unReadCount,
};
}
);
commit("set_UserMsgList", list);
return list;
});
},
// 设置当前聊天用户并入队本地占位
setMsgUser({ commit, dispatch }, user) {
commit("setMsgUser", user);
commit("addLocalMsgUser", user);
console.log("setMsgUser执行了", user);
commit("set_MsgUser", user);
commit("add_LocalMsgUser", user);
// 刷新列表,清零未读
dispatch("getUserlist");
},
@ -260,10 +273,10 @@ const store = new Vuex.Store({
) {
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 : [];
// 将最新消息插入到消息列表尾部(或头部)
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) {
return;
} else {
this.$u.api
Vue.prototype.$u.api
.OverheadOneDialogueApi({ dialogueManagementId: user.friendId })
.then((res) => {
commit("updateTopState", { friendId: user.friendId, isTop: true });
commit("update_TopState", { friendId: user.friendId, isTop: true });
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 || "创建会话失败"));
},
},
});