Compare commits
6
Commits
064578f294
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b6abb53570 | ||
|
|
3a58ce675e | ||
|
|
e4c6fd5f9f | ||
|
|
dc734198bd | ||
|
|
0ce763cc79 | ||
|
|
5de8de007b |
@@ -0,0 +1,3 @@
|
|||||||
|
# 必须要以VUE_APP_打头,不然识别不了
|
||||||
|
VUE_APP_HTTP_BASE_URL=/api
|
||||||
|
VUE_APP_WS_URL=ws://localhost:3000
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
VUE_APP_HTTP_BASE_URL=http://localhost:3000/api
|
||||||
|
VUE_APP_WS_URL=ws://localhost:3000
|
||||||
@@ -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 脚本,但未看到专门的单元测试或端到端测试。可优先为认证守卫、消息收发分流、未读计数和历史分页补充测试。
|
||||||
@@ -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,21 +533,20 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$nextTick(() => {
|
|
||||||
this.scrollToBottom();
|
|
||||||
});
|
|
||||||
|
|
||||||
// 发送新消息成功之后,需要把新消息显示到聊天框中(由于要拿数据库里的写入时间,所以要等websocket返回成功再刷新)
|
// 发送新消息成功之后,需要把新消息显示到聊天框中(由于要拿数据库里的写入时间,所以要等websocket返回成功再刷新)
|
||||||
} else if (data.type == "sendSuccess") {
|
} else if (data.type == "sendSuccess") {
|
||||||
const message = data.message;
|
const message = data.message;
|
||||||
@@ -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
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -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("请输入账号和密码");
|
||||||
|
|||||||
@@ -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
@@ -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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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
@@ -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: "服务器异常,请稍后重试"
|
||||||
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user