Compare commits

..
41 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
Seven 064578f294 把发消息写入数据库的时间都交给数据库来创建,只读取 2026-09-29 17:01:05 +08:00
Seven 88cf0abc96 完善websocket的ack消息确认 2026-09-29 11:14:47 +08:00
Seven d84048d60a 解决重复跳转login的bug 2026-09-28 16:53:10 +08:00
Seven afedec626f ai编写项目设计书,解决重复跳转login的bug 2026-09-28 14:13:36 +08:00
Seven c3a6d2bc5b 优化时间显示 2026-09-24 11:18:49 +08:00
Seven 2f15cebc4c 添加联系人的在线状态显示 2026-09-24 10:33:01 +08:00
Seven 0f8c3d7b51 优化退回登录页面功能 2026-09-23 17:03:19 +08:00
Seven 761b7da0fc 增加ack确认消息机制 2026-09-23 15:45:13 +08:00
Seven 41354a3090 修改气泡样式,让宽度自适应 2026-09-23 11:01:06 +08:00
Seven b812582597 使用websocket发消息和监听接收消息,未写入数据库 2026-09-23 10:21:59 +08:00
Seven 3d17fd4788 解决登录和未读显示的bug 2026-09-23 09:50:04 +08:00
Seven f8625ab36e 把更新消息集成为一个方法,优化其功能 2026-09-22 17:13:13 +08:00
Seven fc563ab4bc 添加websocket连接 2026-09-22 16:27:39 +08:00
Seven 4beed1531f 优化发送消息功能 2026-09-22 14:17:54 +08:00
Seven 5da8d6df59 消息分页接口和UI优化 2026-09-22 11:02:17 +08:00
Seven b1181964f0 聊天数据分页加载 2026-09-21 17:24:59 +08:00
Seven 2ac985bbe6 聊天框数据根据最新时间逐步往上显示 2026-09-21 16:35:45 +08:00
Seven 5445fbdd22 发送消息时自动更新最近联系人消息列表 2026-09-21 14:52:46 +08:00
Seven 6cebdc7c71 切换联系人时的UI优化和消息获取 2026-09-21 11:20:36 +08:00
Seven b0bb320862 优化最近聊天列表的搜索功能 2026-09-21 11:07:17 +08:00
Seven 57dadadd1b 文件重命名 2026-09-21 11:05:32 +08:00
Seven eb1063d3f0 优化接口,实现分页查询 2026-09-21 10:58:59 +08:00
Seven b13194ca8b 发送消息功能 2026-09-16 22:45:03 +08:00
Seven f956e60279 更改数据库数据格式 2026-09-16 22:26:52 +08:00
Seven 9cdeeec91c 导航菜单切换 2026-09-14 22:29:53 +08:00
Seven dd00aff742 修复render时的报错 2026-09-14 21:44:44 +08:00
Seven 25c684d16b 优化联系人消息获取 2026-09-14 21:31:34 +08:00
Seven b959d1dcc5 修改聊天界面,增加联系人信息获取接口 2026-09-13 15:31:56 +08:00
Seven f27b9bec46 ai编写聊天界面 2026-09-12 16:31:41 +08:00
Seven 60d7ecea5f 登录后获取信息,用于判断登录后是否过期 2026-09-12 15:51:53 +08:00
Seven fe47a38a13 新增页面 2026-09-09 22:34:32 +08:00
Seven c9dce2c828 注册接口联调完成 2026-08-30 18:15:21 +08:00
Seven 9931de77de 登录接口联调完成 2026-08-30 15:24:27 +08:00
Seven 68ea2af952 添加请求接口 2026-08-25 22:19:22 +08:00
Seven ef50160ef5 优化登录跳转规则 2026-08-24 22:22:15 +08:00
17 changed files with 1759 additions and 74 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
+200
View File
@@ -0,0 +1,200 @@
# Chat Demo 前端设计书
## 1. 文档说明
本文档依据当前仓库中的前端代码,说明系统目标、技术选型、模块边界、代码结构、主要数据流和交互设计。项目是一个 Vue 单页聊天演示前端;当前仓库不包含服务端实现,因此服务端接口及消息协议以客户端实际调用和解析的内容为准。
## 2. 项目概述
系统提供账号注册、登录和一对一即时聊天页面。用户登录后可以查看最近会话、搜索最近联系人、浏览联系人在线状态、查看历史消息、加载更早消息,并通过 WebSocket 收发消息。聊天页面采用三栏布局:功能导航、会话/联系人列表、当前聊天内容。
前端职责包括:
- 通过 REST API 完成注册、登录以及用户、联系人、会话和历史消息数据读取。
- 保存登录令牌,并在后续 HTTP 请求中携带令牌。
- 使用 Vue Router 限制聊天路由的访问。
- 建立 WebSocket 连接,发送聊天消息、处理服务端推送并回传消息确认 ACK。
- 管理页面内的会话、消息、未读数、在线用户和输入状态。
## 3. 技术选型
| 分类 | 技术/模块 | 用途 |
| --- | --- | --- |
| UI 框架 | Vue 2.6 | 页面组件与响应式视图,采用 Options API |
| 路由 | Vue Router 3 | `/login`、`/register`、`/chat` 页面切换及导航守卫 |
| 状态管理 | Vuex 3 | 保存 token 和当前用户名 |
| UI 组件 | Element UI 2 | 登录/注册表单、输入框、按钮、提示消息 |
| HTTP | Axios 1 | REST 请求、认证请求头、统一响应与 401 处理 |
| 实时通信 | 浏览器原生 WebSocket | 即时消息、在线用户列表和 ACK |
| 构建工具 | Vue CLI 5、Babel 7 | 本地开发、构建、ES 转译和 Element UI 组件按需样式处理 |
项目未使用 TypeScript、独立 API 状态库或 WebSocket 第三方库;聊天页面自身维护主要业务状态。
## 4. 总体架构
```mermaid
flowchart LR
Browser[浏览器] --> Entry[src/main.js]
Entry --> App[src/App.vue]
Entry --> Router[src/router.js]
Entry --> Store[src/store/index.js]
Router --> Pages[登录 / 注册 / 聊天页面]
Pages --> API[src/utils/api.js]
API --> Request[src/utils/request.js]
Request -->|HTTP /api| Backend[后端 localhost:3000]
Pages --> WS[src/utils/websocket.js]
WS -->|WebSocket ws://localhost:3000| Backend
Request --> Store
Router --> Store
```
页面层负责交互、视图状态和流程编排;`api.js` 将业务接口映射为 HTTP 方法;`request.js` 统一管理 Axios 配置和拦截器;`websocket.js` 封装浏览器 WebSocket 的创建、发送、关闭和回调。Vuex 在入口、路由和请求层之间共享认证状态。
## 5. 代码结构与模块职责
```text
.
├── public/
│ └── index.html # SPA HTML 宿主
├── src/
│ ├── main.js # Vue、路由、store、Element UI 与全局 CSS 初始化
│ ├── App.vue # 根组件,通过 router-view 渲染当前页面
│ ├── router.js # 路由表与 token 路由守卫
│ ├── store/
│ │ └── index.js # token、username 与 mutations
│ ├── pages/
│ │ ├── login/
│ │ │ ├── LoginPage.vue # 登录表单和登录后跳转
│ │ │ └── RegisterPage.vue # 注册表单和密码确认
│ │ └── index/
│ │ └── ChatPage.vue # 聊天布局、会话状态及实时消息处理
│ ├── utils/
│ │ ├── api.js # 登录、注册、用户和聊天 REST API
│ │ ├── request.js # Axios 实例、认证头和响应拦截器
│ │ └── websocket.js # WebSocket 生命周期和 JSON 消息封装
│ └── assets/css/
│ └── style_public.css # 全局重置与基础样式
├── babel.config.js # Vue CLI Babel preset 与 Element UI 按需样式
├── jsconfig.json # JavaScript 配置及 @ -> src 别名
├── vue.config.js # Vue CLI 开发服务器代理
└── package.json # 依赖和开发/构建/lint 命令
```
### 5.1 应用启动与路由
`src/main.js` 加载 Element UI 及其主题样式、全局公共 CSS,并将 router 和 store 注入 Vue 根实例。`App.vue` 只提供 `router-view` 容器。路由采用 history 模式:根路径跳转 `/login`;`/login`、`/register` 和 `/chat` 分别对应登录、注册和聊天页面。`/chat` 标记 `requiresAuth`,导航守卫通过 `store.state.token` 判断是否允许进入。
### 5.2 登录、注册与认证状态
登录页收集用户名和密码,调用 `login()`;成功后从响应中读取 token,提交 `SET_TOKEN`、`SET_USERNAME` 并跳转聊天页。注册页在前端检查两次密码一致后调用 `register()`,成功后返回登录页。
Vuex 当前包含:
| 状态 | 初始值/持久化 | 用途 |
| --- | --- | --- |
| `token` | 初始化时从 `localStorage` 读取;`SET_TOKEN` 同步写入 localStorage | 路由鉴权及请求认证 |
| `username` | 空字符串,登录时写入 Vuex,不持久化 | 当前会话中的用户标识 |
HTTP 请求拦截器从 localStorage 读取 token 并设置 `Authorization` 请求头。响应状态为 401 时清理 token 和用户名并跳转登录页。退出登录时关闭 WebSocket、清空 Vuex token/username 并跳转登录。
### 5.3 REST API 封装
`src/utils/api.js` 提供以下调用:
| 函数 | 方法与路径 | 当前用途 |
| --- | --- | --- |
| `login(data)` | `POST /login` | 登录并取得 token |
| `register(data)` | `POST /register` | 创建账号 |
| `getUserInfo()` | `GET /userInfo` | 读取当前用户资料 |
| `getContactInfo()` | `GET /contactsInfo` | 读取联系人列表 |
| `getRecentMessages()` | `GET /recentChats` | 读取最近会话及预览信息 |
| `getContactMessages(data)` | `POST /contactMessagesInfo` | 分页读取指定联系人的消息 |
| `sendMessage(data)` | `POST /sendMessage` | HTTP 发送接口;当前聊天页未使用,实际发送走 WebSocket |
`src/utils/request.js` 创建 Axios 实例,默认 `baseURL` 为 `/api`,超时为 10 秒。开发服务器将 `/api` 转发到 `http://localhost:3000`。响应拦截器返回 `response.data`;页面代码随后通过 `response.data` 读取业务字段,因此当前前后端约定应在 HTTP 响应外层提供 `data` 字段(例如 Axios 响应体为 `{ "data": { ... } }`)。由于服务端代码不在本仓库,实际响应格式仍需与后端核对。
### 5.4 聊天页面与状态
`ChatPage.vue` 以一个组件承载聊天主界面及其业务流程,关键状态如下:
| 状态 | 含义 |
| --- | --- |
| `currentUser` | 当前用户资料 |
| `contactList` | 联系人列表 |
| `recentChats` | 最近会话列表,包含最近消息预览与未读数 |
| `onlineList` | 服务端推送的在线用户名列表 |
| `currentContact` | 当前正在查看的联系人 |
| `contactMessages` | 当前会话中展示的消息数组 |
| `searchText`、`messageText` | 联系人搜索关键字和编辑中的消息 |
| `activeMenu` | messages、contacts、settings 侧栏菜单状态 |
| `firstLoad`、`loadingMore`、`loadEnd` | 历史消息初次加载、分页加载和加载结束标记 |
组件创建时并行触发当前用户资料、最近会话和 WebSocket 初始化。最近会话加载完成后,如果列表非空,会自动选择第一条会话。切换至联系人菜单时另行请求联系人列表。选择联系人时清空当前消息并请求首批 10 条,查看后将该联系人未读数清零。
历史消息使用 `limit=10`,`offset` 取当前消息条数。加载更早消息时将返回数据倒序后置于现有列表之前,并依据加载前后的滚动区域高度调整 `scrollTop`,以尽量保持用户正在阅读的位置。发送或收到当前会话消息后,视图滚动到底部。
### 5.5 WebSocket 实时通信
`src/utils/websocket.js` 持有模块级 WebSocket 实例,连接地址固定为 `ws://localhost:3000`。聊天页面从 Vuex 读取 token,去除可能存在的 `Bearer ` 前缀并 URL 编码,通过查询参数 `token` 传递。连接建立后以 JSON 收发消息,并将解析后的数据交给页面回调。
客户端当前使用/处理的消息类型:
| 类型 | 方向 | 处理方式 |
| --- | --- | --- |
| `sendMessage` | 客户端 -> 服务端 | 包含 `messageId`、`senderName`、`receiverName`、`content`、`createdAt`;成功写入本地当前消息列表并更新最近会话 |
| `newMessage` | 服务端 -> 客户端 | 从 `message` 读取消息;当前联系人消息加入聊天列表,其他联系人的消息更新最近会话和未读数 |
| `ack` | 客户端 -> 服务端 | 收到 `newMessage` 后按 `messageId` 确认 |
| `sendError` | 服务端 -> 客户端 | 提示发送失败 |
| `userOnline` | 服务端 -> 客户端 | 使用 `userList` 更新在线列表 |
上述字段是前端代码所依赖的协议,不代表后端已实现或经过端到端验证。`connectWebSocket` 提供 open、close、error 回调;聊天页实现了 open 和 error 处理,未提供 close 回调。发送时要求连接处于 OPEN 状态;否则封装仅写控制台,不抛出异常,因此页面的 try/catch 不能据此判断 WebSocket 是否真正发送成功。
### 5.6 UI 与样式
聊天页面采用固定宽度左侧导航栏(78px)、联系人面板(300px)和弹性聊天面板。聊天面板包含会话标题、消息滚动区、输入区。消息区区分本人和对方消息气泡,并展示时间;未读数和联系人在线状态通过独立样式表达。页面 CSS 以 `ChatPage.vue` 内的 scoped 样式为主,公共 CSS 提供基础重置。
登录和注册页使用 Element UI 表单控件,页面样式局部定义,窄屏时缩小表单内边距。聊天容器设置 `min-width: 980px`,当前小屏行为是保留最小宽度并可能产生横向溢出,不属于完整的移动端适配。
## 6. 主要业务流程
### 6.1 登录进入聊天
1. 用户提交用户名和密码,页面调用 `POST /api/login`。
2. 成功响应中的 token 写入 Vuex 和 localStorage,用户名写入 Vuex。
3. 路由跳转 `/chat`;导航守卫检测到 token 后放行。
4. 聊天页读取用户资料、最近会话,并携带 token 建立 WebSocket。
5. 最近会话返回后默认打开第一位联系人并拉取最近 10 条消息。
### 6.2 发送与接收消息
1. 用户点击发送或按 Enter,页面去除首尾空白并检查消息及联系人。
2. 页面组装 `sendMessage` JSON,通过 WebSocket 发送,并乐观地将消息加入当前视图。
3. 服务端推送 `newMessage` 时,页面按发送者是否为当前联系人分流:更新当前消息列表,或更新会话预览和未读数。
4. 页面向服务端发送 `ack`,确认消息已接收。
### 6.3 认证失效或退出
HTTP 401 会清理认证状态并导航至登录页。用户主动退出会先关闭 WebSocket,再清理 Vuex 状态并返回登录页。当前认证守卫主要检查 token 是否存在,并不验证 token 有效性;有效性最终由服务端响应决定。
## 7. 构建与运行配置
- `npm install`:安装依赖。
- `npm run serve`:启动 Vue CLI 开发服务器。
- `npm run build`:生成生产构建。
- `npm run lint`:执行 ESLint 检查。
- 开发环境 `/api` 代理目标为 `http://localhost:3000`。
- WebSocket URL 在 `src/utils/websocket.js` 中固定为 `ws://localhost:3000`;部署到其他环境前需要配置化,并在 HTTPS 部署时切换到安全 WebSocket (`wss://`)。
## 8. 当前边界与改进建议
以下内容是根据当前代码观察到的限制,供后续迭代使用,并非已实现功能:
1. **运行配置集中化**:HTTP base URL 与 WebSocket 地址目前分散/硬编码,应按开发、测试、生产环境配置,减少部署时遗漏。(已解决)
2. **认证状态持久性**:token 会持久化,但 username 不持久化;刷新后需由用户信息接口恢复展示状态。还应评估 localStorage 对 token 的安全取舍及 token 过期刷新方案。(username已删除,token问题待解决)
3. **WebSocket 健壮性**:当前没有断线重连、心跳、待发送队列或连接状态 UI;发送前应明确连接不可用的反馈,并评估 ACK 超时及重复消息处理。
4. **会话切换与分页状态**:`loadEnd` 等分页标志需要在联系人切换时明确重置;异步消息请求也应避免旧联系人响应覆盖新联系人视图。(已解决)
5. **实时会话一致性**:收到当前联系人消息时会更新消息区,但当前实现没有同步刷新最近会话预览;接收新消息的未读判断及空会话创建逻辑也值得通过业务用例确认。(已解决)
6. **响应契约与错误处理**:页面依赖响应中存在 `data` 字段。建议与服务端定义统一响应 DTO,并区分网络错误、认证错误、业务错误;当前错误主要由页面提示。(已解决)
7. **UI 菜单与能力范围**:侧栏存在设置入口,但当前点击后主要改变菜单状态,未形成独立设置页;工具栏的表情和附件按钮也尚未绑定完整功能。
8. **移动端适配与无障碍**:聊天页最小宽度为 980px;联系人条目以鼠标点击交互为主。若支持移动设备或键盘操作,需补充响应式布局、语义按钮和焦点管理。
9. **自动化验证**:当前仓库配置了 lint/build 脚本,但未看到专门的单元测试或端到端测试。可优先为认证守卫、消息收发分流、未读计数和历史分页补充测试。
+131 -18
View File
@@ -8,10 +8,12 @@
"name": "chat-demo", "name": "chat-demo",
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"axios": "^1.19.0",
"core-js": "^3.8.3", "core-js": "^3.8.3",
"element-ui": "^2.15.14", "element-ui": "^2.15.14",
"vue": "^2.6.14", "vue": "^2.6.14",
"vue-router": "^3.6.5" "vue-router": "^3.6.5",
"vuex": "^3.6.2"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.12.16", "@babel/core": "^7.12.16",
@@ -3224,6 +3226,18 @@
"node": ">= 10.0.0" "node": ">= 10.0.0"
} }
}, },
"node_modules/agent-base": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
"license": "MIT",
"dependencies": {
"debug": "4"
},
"engines": {
"node": ">= 6.0.0"
}
},
"node_modules/ajv": { "node_modules/ajv": {
"version": "6.15.0", "version": "6.15.0",
"resolved": "https://registry.npmmirror.com/ajv/-/ajv-6.15.0.tgz", "resolved": "https://registry.npmmirror.com/ajv/-/ajv-6.15.0.tgz",
@@ -3446,6 +3460,12 @@
"babel-runtime": "6.x" "babel-runtime": "6.x"
} }
}, },
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/at-least-node": { "node_modules/at-least-node": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmmirror.com/at-least-node/-/at-least-node-1.0.0.tgz", "resolved": "https://registry.npmmirror.com/at-least-node/-/at-least-node-1.0.0.tgz",
@@ -3493,6 +3513,18 @@
"postcss": "^8.1.0" "postcss": "^8.1.0"
} }
}, },
"node_modules/axios": {
"version": "1.19.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.19.0.tgz",
"integrity": "sha512-ht/iuYZXEjFxLH/Hkezgd7m6JKlHHXEUSneaDz8uZe1Gj5QZtCnpyDsckvAiEnT89OEbCLmnte4R4sn7P0EKFw==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.16.0",
"form-data": "^4.0.6",
"https-proxy-agent": "^5.0.1",
"proxy-from-env": "^2.1.0"
}
},
"node_modules/babel-helper-vue-jsx-merge-props": { "node_modules/babel-helper-vue-jsx-merge-props": {
"version": "2.0.3", "version": "2.0.3",
"resolved": "https://registry.npmjs.org/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-2.0.3.tgz", "resolved": "https://registry.npmjs.org/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-2.0.3.tgz",
@@ -3910,7 +3942,6 @@
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"es-errors": "^1.3.0", "es-errors": "^1.3.0",
@@ -4245,6 +4276,18 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/commander": { "node_modules/commander": {
"version": "8.3.0", "version": "8.3.0",
"resolved": "https://registry.npmmirror.com/commander/-/commander-8.3.0.tgz", "resolved": "https://registry.npmmirror.com/commander/-/commander-8.3.0.tgz",
@@ -4846,7 +4889,6 @@
"version": "4.4.3", "version": "4.4.3",
"resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"ms": "^2.1.3" "ms": "^2.1.3"
@@ -5075,6 +5117,15 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/depd": { "node_modules/depd": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmmirror.com/depd/-/depd-2.0.0.tgz", "resolved": "https://registry.npmmirror.com/depd/-/depd-2.0.0.tgz",
@@ -5253,7 +5304,6 @@
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz", "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"call-bind-apply-helpers": "^1.0.1", "call-bind-apply-helpers": "^1.0.1",
@@ -5414,7 +5464,6 @@
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz", "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
@@ -5424,7 +5473,6 @@
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz", "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
@@ -5441,7 +5489,6 @@
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.2.tgz", "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
"integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"es-errors": "^1.3.0" "es-errors": "^1.3.0"
@@ -5450,6 +5497,21 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/es-set-tostringtag": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/escalade": { "node_modules/escalade": {
"version": "3.2.0", "version": "3.2.0",
"resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz", "resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz",
@@ -6371,7 +6433,6 @@
"version": "1.16.0", "version": "1.16.0",
"resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.16.0.tgz", "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.16.0.tgz",
"integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
"dev": true,
"funding": [ "funding": [
{ {
"type": "individual", "type": "individual",
@@ -6388,6 +6449,22 @@
} }
} }
}, },
"node_modules/form-data": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz",
"integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.4",
"mime-types": "^2.1.35"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/forwarded": { "node_modules/forwarded": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmmirror.com/forwarded/-/forwarded-0.2.0.tgz", "resolved": "https://registry.npmmirror.com/forwarded/-/forwarded-0.2.0.tgz",
@@ -6471,7 +6548,6 @@
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"dev": true,
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
@@ -6508,7 +6584,6 @@
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"call-bind-apply-helpers": "^1.0.2", "call-bind-apply-helpers": "^1.0.2",
@@ -6533,7 +6608,6 @@
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz", "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"dunder-proto": "^1.0.1", "dunder-proto": "^1.0.1",
@@ -6645,7 +6719,6 @@
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz", "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
@@ -6711,7 +6784,6 @@
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz", "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
@@ -6720,6 +6792,21 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/has-tostringtag": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hash-sum": { "node_modules/hash-sum": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmmirror.com/hash-sum/-/hash-sum-2.0.0.tgz", "resolved": "https://registry.npmmirror.com/hash-sum/-/hash-sum-2.0.0.tgz",
@@ -6731,7 +6818,6 @@
"version": "2.0.4", "version": "2.0.4",
"resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.4.tgz", "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.4.tgz",
"integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"function-bind": "^1.1.2" "function-bind": "^1.1.2"
@@ -7007,6 +7093,19 @@
} }
} }
}, },
"node_modules/https-proxy-agent": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
"license": "MIT",
"dependencies": {
"agent-base": "6",
"debug": "4"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/human-signals": { "node_modules/human-signals": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmmirror.com/human-signals/-/human-signals-2.1.0.tgz", "resolved": "https://registry.npmmirror.com/human-signals/-/human-signals-2.1.0.tgz",
@@ -7911,7 +8010,6 @@
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
@@ -8035,7 +8133,6 @@
"version": "2.1.35", "version": "2.1.35",
"resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz", "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"mime-db": "1.52.0" "mime-db": "1.52.0"
@@ -8048,7 +8145,6 @@
"version": "1.52.0", "version": "1.52.0",
"resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz", "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.6" "node": ">= 0.6"
@@ -8331,7 +8427,6 @@
"version": "2.1.3", "version": "2.1.3",
"resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/multicast-dns": { "node_modules/multicast-dns": {
@@ -9759,6 +9854,15 @@
"node": ">= 0.10" "node": ">= 0.10"
} }
}, },
"node_modules/proxy-from-env": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
"integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
"license": "MIT",
"engines": {
"node": ">=10"
}
},
"node_modules/pseudomap": { "node_modules/pseudomap": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmmirror.com/pseudomap/-/pseudomap-1.0.2.tgz", "resolved": "https://registry.npmmirror.com/pseudomap/-/pseudomap-1.0.2.tgz",
@@ -11704,6 +11808,15 @@
"prettier": "^1.18.2 || ^2.0.0" "prettier": "^1.18.2 || ^2.0.0"
} }
}, },
"node_modules/vuex": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/vuex/-/vuex-3.6.2.tgz",
"integrity": "sha512-ETW44IqCgBpVomy520DT5jf8n0zoCac+sxWnn+hMe/CzaSejb/eVw2YToiXYX+Ex/AuHHia28vWTq4goAexFbw==",
"license": "MIT",
"peerDependencies": {
"vue": "^2.0.0"
}
},
"node_modules/watchpack": { "node_modules/watchpack": {
"version": "2.5.2", "version": "2.5.2",
"resolved": "https://registry.npmmirror.com/watchpack/-/watchpack-2.5.2.tgz", "resolved": "https://registry.npmmirror.com/watchpack/-/watchpack-2.5.2.tgz",
+3 -1
View File
@@ -8,10 +8,12 @@
"lint": "vue-cli-service lint" "lint": "vue-cli-service lint"
}, },
"dependencies": { "dependencies": {
"axios": "^1.19.0",
"core-js": "^3.8.3", "core-js": "^3.8.3",
"element-ui": "^2.15.14", "element-ui": "^2.15.14",
"vue": "^2.6.14", "vue": "^2.6.14",
"vue-router": "^3.6.5" "vue-router": "^3.6.5",
"vuex": "^3.6.2"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.12.16", "@babel/core": "^7.12.16",
+3 -5
View File
@@ -2,15 +2,13 @@ import Vue from 'vue'
import App from './App.vue' import App from './App.vue'
import router from './router' import router from './router'
import store from './store' import store from './store'
import { Button, Select, Input, InputNumber } from 'element-ui'; import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
import './assets/css/style_public.css' import './assets/css/style_public.css'
Vue.config.productionTip = false Vue.config.productionTip = false
Vue.use(Button) Vue.use(ElementUI)
Vue.use(Select)
Vue.use(Input)
Vue.use(InputNumber)
new Vue({ new Vue({
router, router,
File diff suppressed because it is too large Load Diff
-38
View File
@@ -1,38 +0,0 @@
<template>
<div class="hello">
<el-input placeholder="请输入用户名" v-model="username" clearable> </el-input>
<el-input placeholder="请输入密码" v-model="password" show-password></el-input>
</div>
</template>
<script>
export default {
name: "IndexPage",
props: {
},
data() {
return {
username: '',
password: ''
}
}
};
</script>
<style scoped>
h3 {
margin: 40px 0 0;
}
ul {
list-style-type: none;
padding: 0;
}
li {
display: inline-block;
margin: 0 10px;
}
a {
color: #42b983;
}
</style>
+35 -6
View File
@@ -36,19 +36,44 @@
</template> </template>
<script> <script>
import { login } from "@/utils/api";
export default { export default {
name: "LoginPage", name: "LoginPage",
data() { data() {
return { return {
username: '', username: "",
password: '' password: "",
} };
}, },
methods: { methods: {
handleLogin() { handleLogin() {
// 登录接口接入后在这里提交账号和密码。 // 登录接口接入后在这里提交账号和密码
// 这里模拟登录成功,直接跳转到聊天页面
if (this.username && this.password) {
login({
username: this.username,
password: this.password,
})
.then((data) => {
const token = data.token;
this.$store.commit("SET_TOKEN", token);
this.$router.push("/chat");
})
.catch((error) => {
console.error(error);
this.$message.error(error.message);
});
} else {
this.$message.error("请输入账号和密码");
}
},
},
mounted() {
// 如果已经登录过,直接跳转到聊天页面
if (this.$store.state.token) {
this.$router.push("/chat");
} }
} },
}; };
</script> </script>
@@ -56,7 +81,11 @@ export default {
.login-page { .login-page {
align-items: center; align-items: center;
background: #eef5f3; background: #eef5f3;
background-image: linear-gradient(135deg, rgba(255, 255, 255, 0.8), rgba(211, 235, 229, 0.75)); background-image: linear-gradient(
135deg,
rgba(255, 255, 255, 0.8),
rgba(211, 235, 229, 0.75)
);
display: flex; display: flex;
justify-content: center; justify-content: center;
min-height: 100vh; min-height: 100vh;
+11 -1
View File
@@ -44,6 +44,7 @@
</template> </template>
<script> <script>
import { register } from "@/utils/api";
export default { export default {
name: "RegisterPage", name: "RegisterPage",
data() { data() {
@@ -60,7 +61,16 @@ export default {
return return
} }
// 注册接口接入后在这里提交账号和密码。 register({
username: this.username,
password: this.password
}).then(() => {
this.$message.success('注册成功,请登录')
this.$router.push('/login')
}).catch((error) => {
console.error(error)
this.$message.error(error.message)
})
} }
} }
}; };
+30 -4
View File
@@ -1,9 +1,10 @@
import Vue from 'vue' import Vue from 'vue'
import VueRouter from 'vue-router' import VueRouter from 'vue-router'
import store from './store'
import LoginPage from './pages/login/LoginPage.vue' import LoginPage from './pages/login/LoginPage.vue'
import RegisterPage from './pages/login/RegisterPage.vue' import RegisterPage from './pages/login/RegisterPage.vue'
import IndexPage from './pages/index/IndexPage.vue' import ChatPage from './pages/index/ChatPage.vue'
Vue.use(VueRouter) Vue.use(VueRouter)
@@ -23,9 +24,12 @@ const routes = [
component: RegisterPage component: RegisterPage
}, },
{ {
path: '/index', path: '/chat',
name: 'index', name: 'chat',
component: IndexPage component: ChatPage,
meta: {
requiresAuth: true
}
} }
] ]
@@ -35,4 +39,26 @@ const router = new VueRouter({
routes routes
}) })
// 路由守卫,当访问需要登录的页面时,检查是否有token,如果没有则跳转到登录页
router.beforeEach((to, from, next) => {
const token = store.state.token;
if (to.meta.requiresAuth) {
// 登录页直接放行
if (to.path === "/login") {
next();
return;
}
// 其他页面没有 token,不允许进入
if (!token) {
next("/login");
return;
}
next();
} else {
next()
}
})
export default router export default router
+23
View File
@@ -0,0 +1,23 @@
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const store = new Vuex.Store({
state: {
token: localStorage.getItem('token') || ''
},
mutations: {
// 设置token和用户名
SET_TOKEN(state, token) {
state.token = token;
localStorage.setItem('token', token);
},
REMOVE_TOKEN(state) {
state.token = '';
localStorage.removeItem('token');
}
}
})
export default store
+61
View File
@@ -0,0 +1,61 @@
import request from "@/utils/request";
// 登录
export function login(data) {
return request({
url: '/login',
method: 'post',
data
})
}
// 注册账号
export function register(data) {
return request({
url: '/register',
method: 'post',
data
})
}
// 获取用户信息
export function getUserInfo() {
return request({
url: '/userInfo',
method: 'get'
})
}
// 获取用户的联系人信息
export function getContactInfo() {
return request({
url: '/contactsInfo',
method: 'get'
})
}
// 获取最近消息列表
export function getRecentMessages() {
return request({
url: '/recentChats',
method: 'get'
})
}
// 获取用户的联系人消息
export function getContactMessages(data) {
return request({
url: '/contactMessagesInfo',
method: 'post',
data
})
}
// 发送消息
export function sendMessage(data) {
return request({
url: '/sendMessage',
method: 'post',
data
})
}
+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;
+88
View File
@@ -0,0 +1,88 @@
import axios from 'axios';
import store from '../store';
import router from '../router';
import { HTTP_BASE_URL } from "./config"
let redirectingToLogin = false;
function redirectToLogin() {
if (redirectingToLogin || router.currentRoute.path === '/login') {
return;
}
redirectingToLogin = true;
router.replace('/login').then(
() => {
redirectingToLogin = false;
},
(navigationError) => {
redirectingToLogin = false;
if (navigationError.name !== 'NavigationDuplicated') {
console.error(navigationError);
}
}
);
}
// 配置请求
const request = axios.create({
baseURL: HTTP_BASE_URL,
timeout: 10000,
});
// 请求拦截器:每次请求都带上token
request.interceptors.request.use(
(config) => {
// 在请求头中添加token
const token = localStorage.getItem('token');
if (token) {
config.headers['Authorization'] = `${token}`;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
request.interceptors.response.use(
(response) => {
const result = response.data;
// HTTP 请求成功,但业务失败
if (result.code !== 200) {
return Promise.reject({
type: "business",
message: result.message
});
}
return result.data;
},
(error) => {
if (!error.response) {
// 网络错误
return Promise.reject({
type: "network",
message: "网络连接失败,请检查网络"
});
}
if (error.response && error.response.status === 401) {
// 如果返回401,说明token过期或无效,清除token并跳转到登录页
store.commit('SET_TOKEN', '');
redirectToLogin();
return Promise.reject({
type: "auth",
message: "登录已过期,请重新登录"
});
};
return Promise.reject({
type: "server",
message: "服务器异常,请稍后重试"
});
}
);
export default request;
+82
View File
@@ -0,0 +1,82 @@
import { WS_URL } from "./config";
let ws = null;
// 建立WebSocket连接
export function connectWebSocket(token, onMessage, onOpen, onClose, onError) {
// 如果已经连接,就不重复连接
if (ws && ws.readyState === WebSocket.OPEN) {
console.log("WebSocket已经连接");
return;
}
// 携带JWT token
ws = new WebSocket(`${WS_URL}?token=${token}`);
// 连接成功
ws.onopen = () => {
console.log("WebSocket连接成功");
if (onOpen) {
onOpen();
}
};
// 接收到服务器消息
ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
console.log("收到WebSocket消息:", data);
if (onMessage) {
onMessage(data);
}
} catch (error) {
console.error("WebSocket消息解析失败:", error);
}
};
// 连接关闭
ws.onclose = () => {
console.log("WebSocket连接关闭");
if (onClose) {
onClose();
}
ws = null;
};
// 连接错误
ws.onerror = (error) => {
console.error("WebSocket连接错误:", error);
if (onError) {
onError(error);
}
};
}
// 发送消息
export function sendWebSocketMessage(data) {
if (!ws || ws.readyState !== WebSocket.OPEN) {
console.error("WebSocket没有连接");
return;
}
ws.send(JSON.stringify(data));
}
// 关闭WebSocket
export function closeWebSocket() {
if (ws) {
ws.close();
ws = null;
}
}
// 获取当前WebSocket
export function getWebSocket() {
return ws;
}
+9 -1
View File
@@ -1,4 +1,12 @@
const { defineConfig } = require('@vue/cli-service') const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({ module.exports = defineConfig({
transpileDependencies: true transpileDependencies: true,
devServer: {
proxy: {
'/api': {
target: 'http://localhost:3000',
changeOrigin: true
}
}
}
}) })