Compare commits

..
6 Commits
Author SHA1 Message Date
Seven b6abb53570 对请求的结构、报错提示在接口部分做统一处理 2026-09-30 16:22:05 +08:00
Seven 3a58ce675e dom更新bug修复 2026-09-30 16:06:36 +08:00
Seven e4c6fd5f9f 未读显示bug修复 2026-09-30 15:50:06 +08:00
Seven dc734198bd 优化加载更多消息功能 2026-09-30 15:19:49 +08:00
Seven 0ce763cc79 删除vuex中的username 2026-09-30 15:11:17 +08:00
Seven 5de8de007b 配置开发环境和生产环境的地址 2026-09-30 14:22:12 +08:00
10 changed files with 76 additions and 49 deletions
+3
View File
@@ -0,0 +1,3 @@
# 必须要以VUE_APP_打头,不然识别不了
VUE_APP_HTTP_BASE_URL=/api
VUE_APP_WS_URL=ws://localhost:3000
+2
View File
@@ -0,0 +1,2 @@
VUE_APP_HTTP_BASE_URL=http://localhost:3000/api
VUE_APP_WS_URL=ws://localhost:3000
+5 -5
View File
@@ -189,12 +189,12 @@ HTTP 401 会清理认证状态并导航至登录页。用户主动退出会先
以下内容是根据当前代码观察到的限制,供后续迭代使用,并非已实现功能: 以下内容是根据当前代码观察到的限制,供后续迭代使用,并非已实现功能:
1. **运行配置集中化**:HTTP base URL 与 WebSocket 地址目前分散/硬编码,应按开发、测试、生产环境配置,减少部署时遗漏。 1. **运行配置集中化**:HTTP base URL 与 WebSocket 地址目前分散/硬编码,应按开发、测试、生产环境配置,减少部署时遗漏。(已解决)
2. **认证状态持久性**:token 会持久化,但 username 不持久化;刷新后需由用户信息接口恢复展示状态。还应评估 localStorage 对 token 的安全取舍及 token 过期刷新方案。 2. **认证状态持久性**:token 会持久化,但 username 不持久化;刷新后需由用户信息接口恢复展示状态。还应评估 localStorage 对 token 的安全取舍及 token 过期刷新方案。(username已删除,token问题待解决)
3. **WebSocket 健壮性**:当前没有断线重连、心跳、待发送队列或连接状态 UI;发送前应明确连接不可用的反馈,并评估 ACK 超时及重复消息处理。 3. **WebSocket 健壮性**:当前没有断线重连、心跳、待发送队列或连接状态 UI;发送前应明确连接不可用的反馈,并评估 ACK 超时及重复消息处理。
4. **会话切换与分页状态**:`loadEnd` 等分页标志需要在联系人切换时明确重置;异步消息请求也应避免旧联系人响应覆盖新联系人视图。 4. **会话切换与分页状态**:`loadEnd` 等分页标志需要在联系人切换时明确重置;异步消息请求也应避免旧联系人响应覆盖新联系人视图。(已解决)
5. **实时会话一致性**:收到当前联系人消息时会更新消息区,但当前实现没有同步刷新最近会话预览;接收新消息的未读判断及空会话创建逻辑也值得通过业务用例确认。 5. **实时会话一致性**:收到当前联系人消息时会更新消息区,但当前实现没有同步刷新最近会话预览;接收新消息的未读判断及空会话创建逻辑也值得通过业务用例确认。(已解决)
6. **响应契约与错误处理**:页面依赖响应中存在 `data` 字段。建议与服务端定义统一响应 DTO,并区分网络错误、认证错误、业务错误;当前错误主要由页面提示。 6. **响应契约与错误处理**:页面依赖响应中存在 `data` 字段。建议与服务端定义统一响应 DTO,并区分网络错误、认证错误、业务错误;当前错误主要由页面提示。(已解决)
7. **UI 菜单与能力范围**:侧栏存在设置入口,但当前点击后主要改变菜单状态,未形成独立设置页;工具栏的表情和附件按钮也尚未绑定完整功能。 7. **UI 菜单与能力范围**:侧栏存在设置入口,但当前点击后主要改变菜单状态,未形成独立设置页;工具栏的表情和附件按钮也尚未绑定完整功能。
8. **移动端适配与无障碍**:聊天页最小宽度为 980px;联系人条目以鼠标点击交互为主。若支持移动设备或键盘操作,需补充响应式布局、语义按钮和焦点管理。 8. **移动端适配与无障碍**:聊天页最小宽度为 980px;联系人条目以鼠标点击交互为主。若支持移动设备或键盘操作,需补充响应式布局、语义按钮和焦点管理。
9. **自动化验证**:当前仓库配置了 lint/build 脚本,但未看到专门的单元测试或端到端测试。可优先为认证守卫、消息收发分流、未读计数和历史分页补充测试。 9. **自动化验证**:当前仓库配置了 lint/build 脚本,但未看到专门的单元测试或端到端测试。可优先为认证守卫、消息收发分流、未读计数和历史分页补充测试。
+26 -25
View File
@@ -266,6 +266,8 @@ export default {
contactMessages: [], contactMessages: [],
// 首次加载 // 首次加载
firstLoad: true, firstLoad: true,
// 每次请求加载的消息条数
pageSize: 10,
// 正在加载更多消息 // 正在加载更多消息
loadingMore: false, loadingMore: false,
// 是否所有消息都加载完毕 // 是否所有消息都加载完毕
@@ -304,9 +306,6 @@ export default {
changeMenu(menu) { changeMenu(menu) {
this.activeMenu = menu; this.activeMenu = menu;
this.currentContact = null; this.currentContact = null;
if (menu === "contacts") {
this.getContactList();
}
}, },
// 退出登录 // 退出登录
@@ -314,20 +313,21 @@ export default {
// 关闭websocket连接,清除 token 和用户信息 // 关闭websocket连接,清除 token 和用户信息
closeWebSocket(); closeWebSocket();
this.$store.commit("SET_TOKEN", ""); this.$store.commit("SET_TOKEN", "");
this.$store.commit("SET_USERNAME", ""); this.$router.push("/login");
}, },
// 获取当前登录用户信息 // 获取当前登录用户信息
getCurrentUser() { getCurrentUser() {
// 获取当前登录用户信息 // 获取当前登录用户信息
getUserInfo() getUserInfo()
.then((response) => { .then((data) => {
this.currentUser = response.data; this.currentUser = data;
this.getRecentMessageList(); this.getRecentMessageList();
this.getContactList();
}) })
.catch((error) => { .catch((error) => {
console.error(error); console.error(error);
this.$message.error("获取用户信息失败,请重新登录"); this.$message.error(error.message);
}); });
}, },
@@ -335,23 +335,25 @@ export default {
getRecentMessageList() { getRecentMessageList() {
if (!this.currentUser) return; if (!this.currentUser) return;
getRecentMessages() getRecentMessages()
.then((response) => { .then((data) => {
this.recentChats = response.data.recentChats || []; this.recentChats = data.recentChats || [];
this.selectFirstContact(); this.selectFirstContact();
}) })
.catch((error) => { .catch((error) => {
console.error(error); console.error(error);
this.$message.error("获取消息列表失败"); this.$message.error(error.message);
}); });
}, },
// 选择联系人 // 选择联系人
async selectContact(contact) { async selectContact(contact) {
this.currentContact = contact; this.currentContact = contact;
this.loadEnd = false; //重置加载消息结束标志
this.contactMessages = []; this.contactMessages = [];
// 每次选择联系人的时候需要首次加载消息 // 每次选择联系人的时候需要首次加载消息
this.firstLoad = true; this.firstLoad = true;
await this.getContactMessageList(contact.contactName, 10, 0); await this.getContactMessageList(contact.contactName, this.pageSize, 0);
// 查看联系人后清除未读数量 // 查看联系人后清除未读数量
contact.unreadCount = 0; contact.unreadCount = 0;
@@ -374,14 +376,14 @@ export default {
async getContactMessageList(contactName, limit, offset) { async getContactMessageList(contactName, limit, offset) {
try { try {
// 获取当前联系人的消息列表 // 获取当前联系人的消息列表
const response = await getContactMessages({ const data = await getContactMessages({
contactName: contactName, contactName: contactName,
limit: limit, limit: limit,
offset: offset, offset: offset,
}); });
const contactMessages = []; const contactMessages = [];
const messages = response.data.messages || []; const messages = data.messages || [];
messages.forEach((item) => { messages.forEach((item) => {
contactMessages.push({ contactMessages.push({
...item, ...item,
@@ -397,22 +399,22 @@ export default {
console.log("contactMessages", contactMessages); console.log("contactMessages", contactMessages);
} catch (error) { } catch (error) {
console.error(error); console.error(error);
this.$message.error("获取消息列表失败"); this.$message.error(error.message);
} }
}, },
// 获取联系人列表 // 获取联系人列表
getContactList() { getContactList() {
getContactInfo() getContactInfo()
.then((response) => { .then((data) => {
this.contactList = response.data.contacts.map((contact) => ({ this.contactList = data.contacts.map((contact) => ({
...contact, ...contact,
unreadCount: contact.unreadCount || 0, unreadCount: contact.unreadCount || 0,
})); }));
}) })
.catch((error) => { .catch((error) => {
console.error(error); console.error(error);
this.$message.error("获取联系人信息失败"); this.$message.error(error.message);
}); });
}, },
@@ -488,7 +490,7 @@ export default {
contactName: contactName, contactName: contactName,
content: newMessage.content, content: newMessage.content,
createdAt: newMessage.createdAt, createdAt: newMessage.createdAt,
unreadCount: 1, unreadCount: 1
}); });
} }
}, },
@@ -531,22 +533,21 @@ export default {
from: "other", from: "other",
time: message.createdAt, time: message.createdAt,
}); });
this.$nextTick(() => {
this.scrollToBottom();
});
} else { } else {
// 不是当前联系人,不加入 contactMessages // 不是当前联系人,不加入 contactMessages
// 如果是当前联系人列表中的成员,更新最近聊天列表、未读数量 // 如果是当前联系人列表中的成员,更新最近聊天列表、未读数量
const contactIndex = this.contactList.findIndex( const contactIndex = this.contactList.findIndex(
(item) => item.contactName === senderName (item) => item.contactName === senderName
); );
console.log("contactIndex="+contactIndex)
if (contactIndex === -1) return; if (contactIndex === -1) return;
this.updateRecentChat("receive", senderName, message); this.updateRecentChat("receive", senderName, message);
} }
// 发送新消息成功之后,需要把新消息显示到聊天框中(由于要拿数据库里的写入时间,所以要等websocket返回成功再刷新)
this.$nextTick(() => {
this.scrollToBottom();
});
// 发送新消息成功之后,需要把新消息显示到聊天框中(由于要拿数据库里的写入时间,所以要等websocket返回成功再刷新)
} else if (data.type == "sendSuccess") { } else if (data.type == "sendSuccess") {
const message = data.message; const message = data.message;
this.contactMessages.push({ this.contactMessages.push({
@@ -620,7 +621,7 @@ export default {
try { try {
await this.getContactMessageList( await this.getContactMessageList(
this.currentContact.contactName, this.currentContact.contactName,
10, this.pageSize,
this.contactMessages.length this.contactMessages.length
); );
+3 -4
View File
@@ -54,15 +54,14 @@ export default {
username: this.username, username: this.username,
password: this.password, password: this.password,
}) })
.then((response) => { .then((data) => {
const token = response.data.token; const token = data.token;
this.$store.commit("SET_TOKEN", token); this.$store.commit("SET_TOKEN", token);
this.$store.commit("SET_USERNAME", this.username);
this.$router.push("/chat"); this.$router.push("/chat");
}) })
.catch((error) => { .catch((error) => {
console.error(error); console.error(error);
this.$message.error("账号或密码错误"); this.$message.error(error.message);
}); });
} else { } else {
this.$message.error("请输入账号和密码"); this.$message.error("请输入账号和密码");
+1 -1
View File
@@ -69,7 +69,7 @@ export default {
this.$router.push('/login') this.$router.push('/login')
}).catch((error) => { }).catch((error) => {
console.error(error) console.error(error)
this.$message.error('注册失败,请重试') this.$message.error(error.message)
}) })
} }
} }
+1 -5
View File
@@ -5,8 +5,7 @@ Vue.use(Vuex)
const store = new Vuex.Store({ const store = new Vuex.Store({
state: { state: {
token: localStorage.getItem('token') || '', token: localStorage.getItem('token') || ''
username: ''
}, },
mutations: { mutations: {
// 设置token和用户名 // 设置token和用户名
@@ -17,9 +16,6 @@ const store = new Vuex.Store({
REMOVE_TOKEN(state) { REMOVE_TOKEN(state) {
state.token = ''; state.token = '';
localStorage.removeItem('token'); localStorage.removeItem('token');
},
SET_USERNAME(state, username) {
state.username = username;
} }
} }
}) })
+2
View File
@@ -0,0 +1,2 @@
export const HTTP_BASE_URL = process.env.VUE_APP_HTTP_BASE_URL;
export const WS_URL = process.env.VUE_APP_WS_URL;
+31 -6
View File
@@ -1,6 +1,7 @@
import axios from 'axios'; import axios from 'axios';
import store from '../store'; import store from '../store';
import router from '../router'; import router from '../router';
import { HTTP_BASE_URL } from "./config"
let redirectingToLogin = false; let redirectingToLogin = false;
@@ -25,8 +26,7 @@ function redirectToLogin() {
// 配置请求 // 配置请求
const request = axios.create({ const request = axios.create({
// baseURL: process.env.REACT_APP_API_BASE_URL, baseURL: HTTP_BASE_URL,
baseURL: "/api",
timeout: 10000, timeout: 10000,
}); });
@@ -47,16 +47,41 @@ request.interceptors.request.use(
request.interceptors.response.use( request.interceptors.response.use(
(response) => { (response) => {
return response.data; const result = response.data;
// HTTP 请求成功,但业务失败
if (result.code !== 200) {
return Promise.reject({
type: "business",
message: result.message
});
}
return result.data;
}, },
(error) => { (error) => {
if (!error.response) {
// 网络错误
return Promise.reject({
type: "network",
message: "网络连接失败,请检查网络"
});
}
if (error.response && error.response.status === 401) { if (error.response && error.response.status === 401) {
// 如果返回401,说明token过期或无效,清除token并跳转到登录页 // 如果返回401,说明token过期或无效,清除token并跳转到登录页
store.commit('SET_TOKEN', ''); store.commit('SET_TOKEN', '');
store.commit('SET_USERNAME', '');
redirectToLogin(); redirectToLogin();
}
return Promise.reject(error); return Promise.reject({
type: "auth",
message: "登录已过期,请重新登录"
});
};
return Promise.reject({
type: "server",
message: "服务器异常,请稍后重试"
});
} }
); );
+2 -3
View File
@@ -1,7 +1,6 @@
let ws = null; import { WS_URL } from "./config";
// WebSocket连接地址 let ws = null;
const WS_URL = "ws://localhost:3000";
// 建立WebSocket连接 // 建立WebSocket连接
export function connectWebSocket(token, onMessage, onOpen, onClose, onError) { export function connectWebSocket(token, onMessage, onOpen, onClose, onError) {